@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.
Files changed (64) hide show
  1. package/dist/{actions-sdr91cfp.js → actions-1hxdkcp6.js} +6 -6
  2. package/dist/{app-0mcf7mwz.js → app-vn2sy15m.js} +3 -3
  3. package/dist/{assistant-7f3cmabp.js → assistant-sj71nx88.js} +9 -9
  4. package/dist/{boot-reembed-a9btk2eh.js → boot-reembed-nc1dpmdj.js} +4 -4
  5. package/dist/{boot-reembed-47cfre4w.js → boot-reembed-nw9vp9ez.js} +3 -3
  6. package/dist/{boot-scrub-logs-hy8pqhej.js → boot-scrub-logs-g2mpp0cg.js} +2 -2
  7. package/dist/{cli-j9eqkarv.js → cli-1brke24m.js} +3 -3
  8. package/dist/{cli-vemx13wc.js → cli-1wzrmxaj.js} +3 -3
  9. package/dist/{cli-h2qhdwhr.js → cli-27pxayz8.js} +1 -1
  10. package/dist/{cli-a0w6tqyc.js → cli-3p0e74xv.js} +1 -1
  11. package/dist/{cli-xy83cmfc.js → cli-4wrdrmr3.js} +33 -6
  12. package/dist/{cli-vhpa5j27.js → cli-5m4da9px.js} +1 -1
  13. package/dist/{cli-9wv4e7qs.js → cli-6jf8xfnt.js} +7 -3
  14. package/dist/{cli-1zs2dm5w.js → cli-6z0y1339.js} +4 -4
  15. package/dist/{cli-mkn2ryba.js → cli-8gtrpqcw.js} +1 -1
  16. package/dist/{cli-7r40bgvr.js → cli-8kssf4wv.js} +4 -4
  17. package/dist/{cli-nmt32dxp.js → cli-9zz27c9g.js} +1 -1
  18. package/dist/{cli-m0k99m2s.js → cli-ae9hy8yh.js} +1 -1
  19. package/dist/{cli-entbgsd5.js → cli-b4hwyzev.js} +1 -1
  20. package/dist/{cli-3cfvx630.js → cli-b7ytq2z8.js} +2 -2
  21. package/dist/{cli-a8gkr14c.js → cli-bz6qha8p.js} +30 -4
  22. package/dist/{cli-tehpf2fm.js → cli-cr2wze6h.js} +1 -1
  23. package/dist/{cli-psz2xq0n.js → cli-er5kffrx.js} +5 -5
  24. package/dist/{cli-mmaz2btq.js → cli-fxk0as0h.js} +1 -1
  25. package/dist/{cli-kppbkzj4.js → cli-gg6zxrx1.js} +5 -5
  26. package/dist/{cli-cya9knf9.js → cli-hc7hxy2m.js} +2 -2
  27. package/dist/{cli-75b0wmer.js → cli-nt60dmfm.js} +2 -2
  28. package/dist/{cli-apdrt1ch.js → cli-tc19ec02.js} +1 -1
  29. package/dist/{cli-gx7gnvha.js → cli-xekgq1r8.js} +17 -17
  30. package/dist/cli.js +8 -8
  31. package/dist/{commands-4p93q1rf.js → commands-11d7nc0g.js} +2 -2
  32. package/dist/{db-hb0chrmg.js → db-0r0wz8a4.js} +2 -2
  33. package/dist/{handlers-pz3zc83m.js → handlers-v8th05yh.js} +9 -9
  34. package/dist/{hook-mye8ket2.js → hook-9n0np62b.js} +1 -1
  35. package/dist/{http-3nrx1tp1.js → http-pmaqk60c.js} +73 -37
  36. package/dist/{index-x9n06ze1.js → index-35x6ptdt.js} +8 -8
  37. package/dist/{index-17n7z8k8.js → index-9vma4xmv.js} +9 -9
  38. package/dist/{index-6sgpkhby.js → index-c9tx34ke.js} +6 -6
  39. package/dist/{index-rys8ctm6.js → index-vgvvr6yk.js} +7 -7
  40. package/dist/{keepalive-b6eax0y0.js → keepalive-pjd1kjv7.js} +4 -4
  41. package/dist/{lead-v1g4j7he.js → lead-czd1xgxr.js} +17 -17
  42. package/dist/{maintenance-xwf1vf0v.js → maintenance-xjqt3ne7.js} +4 -4
  43. package/dist/{onboard-vy0z8b11.js → onboard-whf6w0mr.js} +2 -2
  44. package/dist/{otel-impl-94sm94jm.js → otel-impl-9n4p19tt.js} +1 -1
  45. package/dist/{pricing-refresh-an9bxjcd.js → pricing-refresh-wg80me14.js} +4 -4
  46. package/dist/{seed-pricing-3t22x5ax.js → seed-pricing-garwba27.js} +3 -3
  47. package/dist/{setup-8hd3jk8s.js → setup-dktff2q2.js} +2 -2
  48. package/dist/{worker-s0xm8naj.js → worker-t26gwn0n.js} +17 -17
  49. package/openapi.json +1 -1
  50. package/package.json +1 -1
  51. package/plugin/skills/download-task-attachment/SKILL.md +59 -0
  52. package/src/be/seed/agent-fs-provision.ts +51 -4
  53. package/src/commands/runner.ts +40 -0
  54. package/src/commands/templates.ts +6 -1
  55. package/src/fs/local-fs-provider.ts +36 -4
  56. package/src/http/poll.ts +24 -2
  57. package/src/telemetry.ts +12 -2
  58. package/src/tests/agent-fs-provision-seeder.test.ts +204 -3
  59. package/src/tests/claude-adapter.test.ts +17 -3
  60. package/src/tests/fs-provider.test.ts +34 -0
  61. package/src/tests/http-api-integration.test.ts +51 -0
  62. package/src/tests/prompt-template-remaining.test.ts +25 -0
  63. package/src/tests/runner-attachments-section.test.ts +55 -0
  64. package/src/tests/telemetry-init.test.ts +23 -0
@@ -3,14 +3,14 @@ import {
3
3
  extractArgsJsonSchema,
4
4
  extractScriptSignature,
5
5
  typecheckScript
6
- } from "./cli-xy83cmfc.js";
6
+ } from "./cli-4wrdrmr3.js";
7
7
  import {
8
8
  getScript,
9
9
  upsertScriptByName,
10
10
  validateScriptImports
11
- } from "./cli-3cfvx630.js";
12
- import"./cli-cya9knf9.js";
13
- import"./cli-vhpa5j27.js";
11
+ } from "./cli-b7ytq2z8.js";
12
+ import"./cli-hc7hxy2m.js";
13
+ import"./cli-5m4da9px.js";
14
14
  import"./cli-q25ejbyn.js";
15
15
  import {
16
16
  computeContentHash,
@@ -19,11 +19,11 @@ import {
19
19
  getSkillByName,
20
20
  init_db,
21
21
  updateSkill
22
- } from "./cli-9wv4e7qs.js";
22
+ } from "./cli-6jf8xfnt.js";
23
23
  import"./cli-z2zcxes1.js";
24
24
  import"./cli-q21d49ac.js";
25
25
  import"./cli-anrj584m.js";
26
- import"./cli-apdrt1ch.js";
26
+ import"./cli-tc19ec02.js";
27
27
  import"./cli-4j2mrzag.js";
28
28
  import"./cli-p9swy5t3.js";
29
29
 
@@ -20,21 +20,21 @@ import {
20
20
  stopWaitPoller,
21
21
  subscribeWaitToBus,
22
22
  validateTemplateVariables
23
- } from "./cli-psz2xq0n.js";
23
+ } from "./cli-er5kffrx.js";
24
24
  import"./cli-f14fvzag.js";
25
- import"./cli-3cfvx630.js";
26
- import"./cli-cya9knf9.js";
27
- import"./cli-vhpa5j27.js";
25
+ import"./cli-b7ytq2z8.js";
26
+ import"./cli-hc7hxy2m.js";
27
+ import"./cli-5m4da9px.js";
28
28
  import"./cli-q25ejbyn.js";
29
- import"./cli-a0w6tqyc.js";
29
+ import"./cli-3p0e74xv.js";
30
30
  import"./cli-dh55d5fg.js";
31
31
  import {
32
32
  interpolate
33
- } from "./cli-9wv4e7qs.js";
33
+ } from "./cli-6jf8xfnt.js";
34
34
  import"./cli-z2zcxes1.js";
35
35
  import"./cli-q21d49ac.js";
36
36
  import"./cli-anrj584m.js";
37
- import"./cli-apdrt1ch.js";
37
+ import"./cli-tc19ec02.js";
38
38
  import"./cli-4j2mrzag.js";
39
39
  import {
40
40
  workflowEventBus
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  ensureTokenOrThrow
3
- } from "./cli-tehpf2fm.js";
4
- import"./cli-9wv4e7qs.js";
3
+ } from "./cli-cr2wze6h.js";
4
+ import"./cli-6jf8xfnt.js";
5
5
  import"./cli-z2zcxes1.js";
6
6
  import"./cli-q21d49ac.js";
7
7
  import"./cli-anrj584m.js";
8
- import"./cli-apdrt1ch.js";
8
+ import"./cli-tc19ec02.js";
9
9
  import"./cli-4j2mrzag.js";
10
10
  import {
11
11
  __require
@@ -54,7 +54,7 @@ async function notifySlack(text) {
54
54
  return;
55
55
  }
56
56
  try {
57
- const { getSlackApp } = await import("./app-0mcf7mwz.js");
57
+ const { getSlackApp } = await import("./app-vn2sy15m.js");
58
58
  const app = getSlackApp();
59
59
  if (!app) {
60
60
  console.warn("[OAuth Keepalive] Slack not available, cannot send notification");
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  runAgent
3
- } from "./cli-a8gkr14c.js";
3
+ } from "./cli-bz6qha8p.js";
4
4
  import"./cli-w6zp4qcx.js";
5
5
  import"./cli-m3enfr8q.js";
6
6
  import"./cli-amncg1dq.js";
@@ -15,7 +15,7 @@ import"./cli-d6n0thxw.js";
15
15
  import"./cli-hx1dcevq.js";
16
16
  import {
17
17
  getEnabledCapabilities
18
- } from "./cli-gx7gnvha.js";
18
+ } from "./cli-xekgq1r8.js";
19
19
  import"./cli-1z7048dy.js";
20
20
  import"./cli-dcfyyh3t.js";
21
21
  import"./cli-gz9sbyk3.js";
@@ -25,32 +25,32 @@ import"./cli-tng3131n.js";
25
25
  import"./cli-zm7njwzd.js";
26
26
  import"./cli-tghre7g6.js";
27
27
  import"./cli-tkmm5nf9.js";
28
- import"./cli-tehpf2fm.js";
29
- import"./cli-75b0wmer.js";
30
- import"./cli-entbgsd5.js";
31
- import"./cli-h2qhdwhr.js";
28
+ import"./cli-cr2wze6h.js";
29
+ import"./cli-nt60dmfm.js";
30
+ import"./cli-b4hwyzev.js";
31
+ import"./cli-27pxayz8.js";
32
32
  import"./cli-fad8m16k.js";
33
- import"./cli-1zs2dm5w.js";
34
- import"./cli-psz2xq0n.js";
33
+ import"./cli-6z0y1339.js";
34
+ import"./cli-er5kffrx.js";
35
35
  import"./cli-f14fvzag.js";
36
- import"./cli-3cfvx630.js";
37
- import"./cli-cya9knf9.js";
38
- import"./cli-vhpa5j27.js";
36
+ import"./cli-b7ytq2z8.js";
37
+ import"./cli-hc7hxy2m.js";
38
+ import"./cli-5m4da9px.js";
39
39
  import"./cli-q25ejbyn.js";
40
- import"./cli-nmt32dxp.js";
41
- import"./cli-kppbkzj4.js";
40
+ import"./cli-9zz27c9g.js";
41
+ import"./cli-gg6zxrx1.js";
42
42
  import"./cli-rkndnn90.js";
43
43
  import"./cli-b0p7rfnd.js";
44
- import"./cli-mmaz2btq.js";
44
+ import"./cli-fxk0as0h.js";
45
45
  import"./cli-6xd0bvf8.js";
46
46
  import"./cli-wspgs9bt.js";
47
- import"./cli-a0w6tqyc.js";
47
+ import"./cli-3p0e74xv.js";
48
48
  import"./cli-dh55d5fg.js";
49
- import"./cli-9wv4e7qs.js";
49
+ import"./cli-6jf8xfnt.js";
50
50
  import"./cli-z2zcxes1.js";
51
51
  import"./cli-q21d49ac.js";
52
52
  import"./cli-anrj584m.js";
53
- import"./cli-apdrt1ch.js";
53
+ import"./cli-tc19ec02.js";
54
54
  import"./cli-4j2mrzag.js";
55
55
  import"./cli-e15jhafb.js";
56
56
  import"./cli-9aryf9tv.js";
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  reembedAllScripts
3
- } from "./cli-cya9knf9.js";
4
- import"./cli-vhpa5j27.js";
3
+ } from "./cli-hc7hxy2m.js";
4
+ import"./cli-5m4da9px.js";
5
5
  import"./cli-q25ejbyn.js";
6
- import"./cli-9wv4e7qs.js";
6
+ import"./cli-6jf8xfnt.js";
7
7
  import"./cli-z2zcxes1.js";
8
8
  import"./cli-q21d49ac.js";
9
9
  import"./cli-anrj584m.js";
10
- import"./cli-apdrt1ch.js";
10
+ import"./cli-tc19ec02.js";
11
11
  import"./cli-4j2mrzag.js";
12
12
  import"./cli-p9swy5t3.js";
13
13
 
@@ -3,7 +3,7 @@ import {
3
3
  createDefaultMcpJson,
4
4
  createDefaultSettingsLocal,
5
5
  createHooksConfig
6
- } from "./cli-mkn2ryba.js";
6
+ } from "./cli-8gtrpqcw.js";
7
7
  import {
8
8
  require_jsx_dev_runtime
9
9
  } from "./cli-wx82tw1k.js";
@@ -13,7 +13,7 @@ import {
13
13
  import {
14
14
  init_package,
15
15
  package_default
16
- } from "./cli-apdrt1ch.js";
16
+ } from "./cli-tc19ec02.js";
17
17
  import {
18
18
  Box_default,
19
19
  Select,
@@ -6,7 +6,7 @@ import {
6
6
  import {
7
7
  init_package,
8
8
  package_default
9
- } from "./cli-apdrt1ch.js";
9
+ } from "./cli-tc19ec02.js";
10
10
  import {
11
11
  init_secret_scrubber,
12
12
  scrubSecrets
@@ -2,13 +2,13 @@ import {
2
2
  PRICING_REFRESH_INTERVAL_MS,
3
3
  refreshPricingFromModelsDev,
4
4
  startPricingRefreshLoop
5
- } from "./cli-75b0wmer.js";
6
- import"./cli-entbgsd5.js";
7
- import"./cli-9wv4e7qs.js";
5
+ } from "./cli-nt60dmfm.js";
6
+ import"./cli-b4hwyzev.js";
7
+ import"./cli-6jf8xfnt.js";
8
8
  import"./cli-z2zcxes1.js";
9
9
  import"./cli-q21d49ac.js";
10
10
  import"./cli-anrj584m.js";
11
- import"./cli-apdrt1ch.js";
11
+ import"./cli-tc19ec02.js";
12
12
  import"./cli-4j2mrzag.js";
13
13
  import"./cli-p9swy5t3.js";
14
14
  export {
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  buildModelsDevSeedRows,
3
3
  seedPricingFromModelsDev
4
- } from "./cli-entbgsd5.js";
5
- import"./cli-9wv4e7qs.js";
4
+ } from "./cli-b4hwyzev.js";
5
+ import"./cli-6jf8xfnt.js";
6
6
  import"./cli-z2zcxes1.js";
7
7
  import"./cli-q21d49ac.js";
8
8
  import"./cli-anrj584m.js";
9
- import"./cli-apdrt1ch.js";
9
+ import"./cli-tc19ec02.js";
10
10
  import"./cli-4j2mrzag.js";
11
11
  import"./cli-p9swy5t3.js";
12
12
  export {
@@ -4,14 +4,14 @@ import {
4
4
  createDefaultMcpJson,
5
5
  createDefaultSettingsLocal,
6
6
  createHooksConfig
7
- } from "./cli-mkn2ryba.js";
7
+ } from "./cli-8gtrpqcw.js";
8
8
  import {
9
9
  require_jsx_dev_runtime
10
10
  } from "./cli-wx82tw1k.js";
11
11
  import {
12
12
  getApiKey
13
13
  } from "./cli-f14fvzag.js";
14
- import"./cli-apdrt1ch.js";
14
+ import"./cli-tc19ec02.js";
15
15
  import {
16
16
  Box_default,
17
17
  Spinner,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  runAgent
3
- } from "./cli-a8gkr14c.js";
3
+ } from "./cli-bz6qha8p.js";
4
4
  import"./cli-w6zp4qcx.js";
5
5
  import"./cli-m3enfr8q.js";
6
6
  import"./cli-amncg1dq.js";
@@ -15,7 +15,7 @@ import"./cli-d6n0thxw.js";
15
15
  import"./cli-hx1dcevq.js";
16
16
  import {
17
17
  getEnabledCapabilities
18
- } from "./cli-gx7gnvha.js";
18
+ } from "./cli-xekgq1r8.js";
19
19
  import"./cli-1z7048dy.js";
20
20
  import"./cli-dcfyyh3t.js";
21
21
  import"./cli-gz9sbyk3.js";
@@ -25,32 +25,32 @@ import"./cli-tng3131n.js";
25
25
  import"./cli-zm7njwzd.js";
26
26
  import"./cli-tghre7g6.js";
27
27
  import"./cli-tkmm5nf9.js";
28
- import"./cli-tehpf2fm.js";
29
- import"./cli-75b0wmer.js";
30
- import"./cli-entbgsd5.js";
31
- import"./cli-h2qhdwhr.js";
28
+ import"./cli-cr2wze6h.js";
29
+ import"./cli-nt60dmfm.js";
30
+ import"./cli-b4hwyzev.js";
31
+ import"./cli-27pxayz8.js";
32
32
  import"./cli-fad8m16k.js";
33
- import"./cli-1zs2dm5w.js";
34
- import"./cli-psz2xq0n.js";
33
+ import"./cli-6z0y1339.js";
34
+ import"./cli-er5kffrx.js";
35
35
  import"./cli-f14fvzag.js";
36
- import"./cli-3cfvx630.js";
37
- import"./cli-cya9knf9.js";
38
- import"./cli-vhpa5j27.js";
36
+ import"./cli-b7ytq2z8.js";
37
+ import"./cli-hc7hxy2m.js";
38
+ import"./cli-5m4da9px.js";
39
39
  import"./cli-q25ejbyn.js";
40
- import"./cli-nmt32dxp.js";
41
- import"./cli-kppbkzj4.js";
40
+ import"./cli-9zz27c9g.js";
41
+ import"./cli-gg6zxrx1.js";
42
42
  import"./cli-rkndnn90.js";
43
43
  import"./cli-b0p7rfnd.js";
44
- import"./cli-mmaz2btq.js";
44
+ import"./cli-fxk0as0h.js";
45
45
  import"./cli-6xd0bvf8.js";
46
46
  import"./cli-wspgs9bt.js";
47
- import"./cli-a0w6tqyc.js";
47
+ import"./cli-3p0e74xv.js";
48
48
  import"./cli-dh55d5fg.js";
49
- import"./cli-9wv4e7qs.js";
49
+ import"./cli-6jf8xfnt.js";
50
50
  import"./cli-z2zcxes1.js";
51
51
  import"./cli-q21d49ac.js";
52
52
  import"./cli-anrj584m.js";
53
- import"./cli-apdrt1ch.js";
53
+ import"./cli-tc19ec02.js";
54
54
  import"./cli-4j2mrzag.js";
55
55
  import"./cli-e15jhafb.js";
56
56
  import"./cli-9aryf9tv.js";
package/openapi.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Agent Swarm API",
5
- "version": "1.108.0",
5
+ "version": "1.108.2",
6
6
  "description": "Multi-agent orchestration API for Claude Code, Codex, and Gemini CLI. Enables task distribution, agent communication, and service discovery.\n\nMCP tools are documented separately in [MCP.md](./MCP.md)."
7
7
  },
8
8
  "servers": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@desplega.ai/agent-swarm",
3
- "version": "1.108.0",
3
+ "version": "1.108.2",
4
4
  "description": "Multi-agent orchestration for Claude Code, Codex, Gemini CLI, and other AI coding assistants",
5
5
  "license": "MIT",
6
6
  "author": "desplega.sh <contact@desplega.sh>",
@@ -0,0 +1,59 @@
1
+ ---
2
+ name: download-task-attachment
3
+ description: Fetch a file attached to your current task in ONE call. Use whenever a task carries an attachment (an image, PDF, or other file the requester uploaded) and you need its bytes on disk — the dispatch prompt lists attachments with a ready-to-run curl command, but if you're improvising (resumed session, follow-up task, or the recipe scrolled out of context) use this skill instead of reaching for the `agent-fs` CLI directly.
4
+ ---
5
+
6
+ # Download a Task Attachment
7
+
8
+ Task attachments (files uploaded via the UI or API onto a task) are served
9
+ through a single **provider-agnostic** REST route — regardless of whether the
10
+ backing storage is `local-fs` or `agent-fs`. You do not need to know or care
11
+ which provider is active, and you do not need `agent-fs org`/`agent-fs drive`
12
+ discovery.
13
+
14
+ ## The one-call recipe
15
+
16
+ ```bash
17
+ curl "$MCP_BASE_URL/api/fs/tasks/$AGENT_SWARM_TASK_ID/files/<attachmentId>/raw" \
18
+ -H "X-Agent-ID: $AGENT_ID" \
19
+ -H "Authorization: Bearer ${AGENT_SWARM_API_KEY:-$API_KEY}" \
20
+ -o /tmp/<name>
21
+ ```
22
+
23
+ - `$MCP_BASE_URL`, `$AGENT_ID`, `$AGENT_SWARM_TASK_ID` (or `$AGENT_SWARM_AGENT_ID`/`TASK_FILE`-derived taskId), and `$AGENT_SWARM_API_KEY`/`$API_KEY` are already present in every worker container's env — no new plumbing needed.
24
+ - `<attachmentId>` and `<name>`/`mimeType` come from the task's attachment list (see below).
25
+ - The route resolves the active file-storage provider (`local-fs` in dev, `agent-fs` in prod) server-side and streams the raw bytes back with the correct `Content-Type` — you get one call no matter which provider is behind it.
26
+
27
+ ## Finding the attachment ID
28
+
29
+ If the dispatch prompt already injected an `## Attachments` section with the
30
+ curl command pre-filled, just run it — that's the fast path (see
31
+ `buildAttachmentsSection()` in `src/commands/runner.ts`).
32
+
33
+ Otherwise, pull it from the task:
34
+
35
+ ```
36
+ get-task-details taskId=<your task id>
37
+ ```
38
+
39
+ The response includes an `attachments` array: `{id, name, mimeType, sizeBytes}`.
40
+ Use `id` as `<attachmentId>` in the curl above.
41
+
42
+ ## Why not the `agent-fs` CLI directly?
43
+
44
+ `agent-fs` is a general-purpose CLI for the swarm's own agent-fs filesystem —
45
+ it requires you to already know the org/drive a file lives in, and task
46
+ attachments don't always carry that (older rows, or attachments stored via
47
+ `local-fs` in dev have no org/drive at all). Reaching for `agent-fs cat` /
48
+ `agent-fs download` on a task attachment means guessing subcommand names,
49
+ discovering the right org via `agent-fs org list`, and re-trying — several
50
+ tool calls where one `curl` suffices. This was root-caused from a real
51
+ session that burned 7 tool calls (~64s) doing exactly that detour before
52
+ succeeding; see `runbooks/harness-providers.md` and PR that added
53
+ `buildAttachmentsSection()` for the full trace.
54
+
55
+ ## Gotchas
56
+
57
+ - The route requires `X-Agent-ID` + bearer auth like any other swarm API call — same headers you'd use for any MCP-adjacent REST call.
58
+ - `mimeType` on the attachment record reflects the real upload `Content-Type` (not a filename-extension guess) — trust it when deciding how to handle the downloaded bytes (image vs PDF vs text).
59
+ - If you get a 404, double-check you're using the **attachment ID** (from `attachments[].id`), not the display `name`.
@@ -32,6 +32,12 @@ type InviteTarget = {
32
32
  role: Role;
33
33
  };
34
34
 
35
+ type OrgMember = {
36
+ email?: string | null;
37
+ role?: string | null;
38
+ user?: { email?: string | null } | null;
39
+ };
40
+
35
41
  type FetchLike = typeof fetch;
36
42
 
37
43
  // The provision seeder runs synchronously before the HTTP server binds its port.
@@ -92,8 +98,15 @@ async function provisionAgentFs(item: AgentFsSeedItem): Promise<void> {
92
98
  apiUrl: item.apiUrl,
93
99
  registerEmail: item.registerEmail,
94
100
  });
101
+ const currentRoles = await getCurrentOrgMemberRoles(
102
+ item.apiUrl,
103
+ shared.authHeaders,
104
+ shared.orgId,
105
+ );
95
106
 
96
107
  for (const invite of item.invites) {
108
+ const currentRole = currentRoles.get(invite.email.toLowerCase());
109
+ if (currentRole && roleRank(currentRole) >= roleRank(invite.role)) continue;
97
110
  await inviteToSharedOrg(item.apiUrl, shared.authHeaders, shared.orgId, invite);
98
111
  }
99
112
 
@@ -212,7 +225,11 @@ export async function ensureAgentFsCredentialsForAgent(agentId: string): Promise
212
225
  const apiKey = registered.apiKey ?? "";
213
226
  if (!apiKey) throw new Error(`agent-fs registration did not return an apiKey for ${agentId}`);
214
227
 
215
- await inviteToSharedOrg(apiUrl, shared.authHeaders, shared.orgId, { email, role: "editor" });
228
+ const currentRoles = await getCurrentOrgMemberRoles(apiUrl, shared.authHeaders, shared.orgId);
229
+ const currentRole = currentRoles.get(email.toLowerCase());
230
+ if (!currentRole || roleRank(currentRole) < roleRank("editor")) {
231
+ await inviteToSharedOrg(apiUrl, shared.authHeaders, shared.orgId, { email, role: "editor" });
232
+ }
216
233
 
217
234
  upsertSwarmConfig({
218
235
  scope: "agent",
@@ -309,6 +326,36 @@ async function inviteToSharedOrg(
309
326
  }
310
327
  }
311
328
 
329
+ async function getCurrentOrgMemberRoles(
330
+ apiUrl: string,
331
+ headers: Record<string, string>,
332
+ orgId: string,
333
+ ): Promise<Map<string, Role>> {
334
+ const response = await agentFsRequest<{ members?: OrgMember[] }>(
335
+ apiUrl,
336
+ `/orgs/${encodeURIComponent(orgId)}/members`,
337
+ { headers },
338
+ );
339
+ const roles = new Map<string, Role>();
340
+
341
+ for (const member of response.members ?? []) {
342
+ const email = (member.email ?? member.user?.email ?? "").trim().toLowerCase();
343
+ const role = normalizeRole(member.role);
344
+ if (email && role) roles.set(email, role);
345
+ }
346
+
347
+ return roles;
348
+ }
349
+
350
+ function normalizeRole(role: string | null | undefined): Role | null {
351
+ if (role === "viewer" || role === "editor" || role === "admin") return role;
352
+ return null;
353
+ }
354
+
355
+ function roleRank(role: Role): number {
356
+ return { viewer: 0, editor: 1, admin: 2 }[role];
357
+ }
358
+
312
359
  async function agentFsRequest<T = unknown>(
313
360
  apiUrl: string,
314
361
  path: string,
@@ -363,15 +410,15 @@ function resolveInviteTargets(): InviteTarget[] {
363
410
 
364
411
  for (const user of getAllUsers()) {
365
412
  if (!user.email) continue;
366
- const role = isOperatorRole(user.role) ? "editor" : "viewer";
413
+ const role = isExplicitViewerRole(user.role) ? "viewer" : "editor";
367
414
  targets.set(user.email.toLowerCase(), { email: user.email, role });
368
415
  }
369
416
 
370
417
  return [...targets.values()].sort((a, b) => a.email.localeCompare(b.email));
371
418
  }
372
419
 
373
- function isOperatorRole(role: string | undefined): boolean {
374
- return /\b(admin|operator|owner|lead)\b/i.test(role ?? "");
420
+ function isExplicitViewerRole(role: string | undefined): boolean {
421
+ return /\b(viewer|read[-\s]?only|guest)\b/i.test(role ?? "");
375
422
  }
376
423
 
377
424
  function provisionHashFromCurrentState(): string {
@@ -2289,6 +2289,43 @@ async function pollForTriggerOnce(opts: PollOptions): Promise<Trigger | null> {
2289
2289
  return null; // Timeout reached, no trigger found
2290
2290
  }
2291
2291
 
2292
+ /**
2293
+ * Build a ready-to-run fetch recipe for each task attachment, so the agent
2294
+ * can download the bytes in one call via the provider-agnostic
2295
+ * `/api/fs/tasks/{taskId}/files/{attachmentId}/raw` route — instead of having
2296
+ * to discover the file's storage provider/org/drive itself (e.g. guessing at
2297
+ * the `agent-fs` CLI with no org context). MCP_BASE_URL/API_KEY/AGENT_ID are
2298
+ * already present in every worker container's env.
2299
+ */
2300
+ export function buildAttachmentsSection(
2301
+ taskId: string | undefined,
2302
+ attachmentsRaw: unknown,
2303
+ ): string {
2304
+ if (!taskId || !Array.isArray(attachmentsRaw) || attachmentsRaw.length === 0) return "";
2305
+
2306
+ const lines = attachmentsRaw
2307
+ .filter((a): a is Record<string, unknown> => !!a && typeof a === "object")
2308
+ .map((a) => {
2309
+ const id = typeof a.id === "string" ? a.id : undefined;
2310
+ const name = typeof a.name === "string" ? a.name : id;
2311
+ if (!id || !name) return null;
2312
+ const details = [
2313
+ typeof a.mimeType === "string" ? a.mimeType : null,
2314
+ typeof a.sizeBytes === "number" ? `${a.sizeBytes} bytes` : null,
2315
+ ]
2316
+ .filter(Boolean)
2317
+ .join(", ");
2318
+ const url = `$MCP_BASE_URL/api/fs/tasks/${taskId}/files/${id}/raw`;
2319
+ const cmd = `curl -s -H "Authorization: Bearer \${AGENT_SWARM_API_KEY:-$API_KEY}" -H "X-Agent-ID: $AGENT_ID" "${url}" -o /tmp/${name}`;
2320
+ return `- ${name}${details ? ` (${details})` : ""}: \`${cmd}\``;
2321
+ })
2322
+ .filter((line): line is string => line !== null);
2323
+
2324
+ if (lines.length === 0) return "";
2325
+
2326
+ return `\n\n📎 Attachment(s) — fetch directly, no need to discover the storage path yourself:\n${lines.join("\n")}`;
2327
+ }
2328
+
2292
2329
  /** Build prompt based on trigger type */
2293
2330
  async function buildPromptForTrigger(
2294
2331
  trigger: Trigger,
@@ -2325,10 +2362,13 @@ async function buildPromptForTrigger(
2325
2362
  ? `\n\nRequested by: ${requestedBy.name}${requestedBy.email ? ` (${requestedBy.email})` : ""}`
2326
2363
  : "";
2327
2364
 
2365
+ const attachmentsSection = buildAttachmentsSection(trigger.taskId, taskObj?.attachments);
2366
+
2328
2367
  const result = await resolveTemplateAsync("task.trigger.assigned", {
2329
2368
  work_on_task_cmd: hasMcp ? fmt("work-on-task") : "",
2330
2369
  task_id: hasMcp ? trigger.taskId : "",
2331
2370
  task_desc_section: taskDescSection + requestedBySection,
2371
+ attachments_section: attachmentsSection,
2332
2372
  output_instructions: outputInstructions,
2333
2373
  });
2334
2374
  return result.text;
@@ -17,11 +17,16 @@ import { registerTemplate } from "../prompts/registry";
17
17
  registerTemplate({
18
18
  eventType: "task.trigger.assigned",
19
19
  header: "",
20
- defaultBody: `{{work_on_task_cmd}} {{task_id}}{{task_desc_section}}{{output_instructions}}`,
20
+ defaultBody: `{{work_on_task_cmd}} {{task_id}}{{task_desc_section}}{{attachments_section}}{{output_instructions}}`,
21
21
  variables: [
22
22
  { name: "work_on_task_cmd", description: "Formatted /work-on-task command" },
23
23
  { name: "task_id", description: "Task ID" },
24
24
  { name: "task_desc_section", description: "Task description section or empty string" },
25
+ {
26
+ name: "attachments_section",
27
+ description:
28
+ "Ready-to-run fetch recipe per task attachment (or empty string if none) — lets the agent download an attachment in one call instead of discovering the path/provider itself",
29
+ },
25
30
  {
26
31
  name: "output_instructions",
27
32
  description:
@@ -37,6 +37,12 @@ export class LocalFsProvider implements FileStorageProvider {
37
37
  }
38
38
 
39
39
  await Bun.write(target, await new Response(body).arrayBuffer());
40
+ // Persist the real upload Content-Type alongside the blob — `Bun.file(target).type`
41
+ // only guesses from the storage path's extension, which is wrong whenever the
42
+ // stored key doesn't carry the original filename's extension (see head()).
43
+ if (options.contentType) {
44
+ await Bun.write(this.metaPath(target), JSON.stringify({ contentType: options.contentType }));
45
+ }
40
46
  return this.head(scope);
41
47
  }
42
48
 
@@ -52,12 +58,16 @@ export class LocalFsProvider implements FileStorageProvider {
52
58
  const target = this.localPath(scope);
53
59
  try {
54
60
  const info = await stat(target);
61
+ const storedContentType = await this.readStoredContentType(target);
55
62
  return {
56
63
  providerId: this.id,
57
64
  key: providerPath(scope),
58
65
  taskId: scope.taskId,
59
66
  name: scope.name,
60
- contentType: Bun.file(target).type || undefined,
67
+ // Prefer the Content-Type recorded at upload time; fall back to
68
+ // extension-based sniffing only for files that predate the sidecar
69
+ // metadata (or were written directly on disk without going through upload()).
70
+ contentType: storedContentType ?? Bun.file(target).type ?? undefined,
61
71
  sizeBytes: info.size,
62
72
  updatedAt: info.mtime.toISOString(),
63
73
  };
@@ -82,12 +92,17 @@ export class LocalFsProvider implements FileStorageProvider {
82
92
  }
83
93
 
84
94
  async delete(scope: FileScope): Promise<void> {
85
- await rm(this.localPath(scope), { force: true });
95
+ const target = this.localPath(scope);
96
+ await rm(target, { force: true });
97
+ await rm(this.metaPath(target), { force: true });
86
98
  }
87
99
 
88
100
  async copy(source: FileScope, destination: FileScope): Promise<FileObject> {
101
+ const sourceHead = await this.head(source);
89
102
  const response = await this.download(source);
90
- await this.upload(destination, await response.arrayBuffer());
103
+ await this.upload(destination, await response.arrayBuffer(), {
104
+ contentType: sourceHead.contentType,
105
+ });
91
106
  return this.head(destination);
92
107
  }
93
108
 
@@ -133,6 +148,23 @@ export class LocalFsProvider implements FileStorageProvider {
133
148
  return join(this.rootDir, providerPath(scope));
134
149
  }
135
150
 
151
+ private metaPath(target: string): string {
152
+ return `${target}.meta.json`;
153
+ }
154
+
155
+ private async readStoredContentType(target: string): Promise<string | undefined> {
156
+ try {
157
+ const metaFile = Bun.file(this.metaPath(target));
158
+ if (!(await metaFile.exists())) {
159
+ return undefined;
160
+ }
161
+ const meta = await metaFile.json();
162
+ return typeof meta?.contentType === "string" ? meta.contentType : undefined;
163
+ } catch {
164
+ return undefined;
165
+ }
166
+ }
167
+
136
168
  private async walk(dir: string, visit: (path: string) => Promise<void>): Promise<void> {
137
169
  let entries: import("node:fs").Dirent<string>[];
138
170
  try {
@@ -148,7 +180,7 @@ export class LocalFsProvider implements FileStorageProvider {
148
180
  const path = join(dir, entry.name);
149
181
  if (entry.isDirectory()) {
150
182
  await this.walk(path, visit);
151
- } else if (entry.isFile()) {
183
+ } else if (entry.isFile() && !entry.name.endsWith(".meta.json")) {
152
184
  await visit(path);
153
185
  }
154
186
  }