@desplega.ai/agent-swarm 1.150.0 → 1.152.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (311) hide show
  1. package/README.md +11 -0
  2. package/dist/{acp-adapter-d5mzggyk.js → acp-adapter-d05sg0q4.js} +5 -5
  3. package/dist/{actions-wn6ew0p7.js → actions-4sc0v342.js} +38 -11
  4. package/dist/{anthropic-messages-ec35ktvx.js → anthropic-messages-2y8nktar.js} +16 -16
  5. package/dist/{app-dty7p9hg.js → app-2d0v6kt6.js} +7 -7
  6. package/dist/{assistant-q4vhgjtx.js → assistant-zs6wdnrf.js} +41 -16
  7. package/dist/{azure-openai-responses-g85jv7m8.js → azure-openai-responses-f38x2avq.js} +13 -13
  8. package/dist/{boot-reembed-n15q6s9x.js → boot-reembed-3dwdnnsr.js} +13 -15
  9. package/dist/{boot-reembed-92292pgf.js → boot-reembed-vhh33520.js} +8 -8
  10. package/dist/{boot-scrub-logs-44pc955m.js → boot-scrub-logs-e1fnabcg.js} +5 -5
  11. package/dist/{claude-adapter-cjegjbje.js → claude-adapter-h58xqzzd.js} +12 -13
  12. package/dist/{claude-managed-adapter-p3ts5m9m.js → claude-managed-adapter-dsmkcrjy.js} +3 -3
  13. package/dist/{claude-sdk-session-7ywbn12v.js → claude-sdk-session-pbaczbgt.js} +12 -13
  14. package/dist/{cli-9gqddmxs.js → cli-4fqn2vpg.js} +8 -8
  15. package/dist/{cli-mzb0n642.js → cli-4wppfkcm.js} +350 -20
  16. package/dist/{cli-h505yhex.js → cli-5bqt4gwa.js} +2 -2
  17. package/dist/{cli-hbagregr.js → cli-5p007m2q.js} +88 -13
  18. package/dist/{cli-3ckzxdt1.js → cli-6k2athpf.js} +94 -81
  19. package/dist/{cli-3mh119qv.js → cli-72vksk1n.js} +9855 -693
  20. package/dist/{cli-taym3dmg.js → cli-87hdfyde.js} +13 -13
  21. package/dist/{cli-zthatyrx.js → cli-8fy9dmpv.js} +5 -5
  22. package/dist/{cli-asvrh7kj.js → cli-9xvdm3bx.js} +1 -1
  23. package/dist/{cli-72xdszyx.js → cli-awb59f08.js} +2 -2
  24. package/dist/{cli-vr6qj104.js → cli-cs9z1hw3.js} +1 -1
  25. package/dist/{cli-kt7wevqh.js → cli-cwvey7n1.js} +164 -56
  26. package/dist/{cli-5my3bjsd.js → cli-d3p82erb.js} +7 -7
  27. package/dist/{cli-7kwmt3r5.js → cli-evgy5w3y.js} +9 -3
  28. package/dist/{cli-111ey3ra.js → cli-f1f9n77e.js} +5 -5
  29. package/dist/{cli-ttje5a4z.js → cli-fhdssm2v.js} +105 -56
  30. package/dist/{cli-a45d5v9b.js → cli-fm4ytsdk.js} +2 -2
  31. package/dist/{cli-qp69dd5d.js → cli-gacq5hg8.js} +116 -14
  32. package/dist/{cli-hmyynq9b.js → cli-ht5rs63n.js} +1 -1
  33. package/dist/{cli-dyx0237s.js → cli-j4nv2wqy.js} +2 -4
  34. package/dist/{cli-gm0fhz13.js → cli-jh9txj4n.js} +2 -1
  35. package/dist/{cli-8chp8xab.js → cli-nd5s3m4y.js} +2 -2
  36. package/dist/{cli-zd21jrrc.js → cli-p2hx1pxm.js} +9872 -424
  37. package/dist/cli-pf9q3696.js +172 -0
  38. package/dist/{cli-7fesa1ky.js → cli-pgj4a5fb.js} +1 -1
  39. package/dist/{cli-07jd68a1.js → cli-re8z5p8p.js} +1 -1
  40. package/dist/{cli-ryz8q8p1.js → cli-t1mykkng.js} +40 -40
  41. package/dist/{cli-ck1vqzdj.js → cli-t6axtdcg.js} +9 -4
  42. package/dist/{cli-1mx65de9.js → cli-wbyk465z.js} +1 -1
  43. package/dist/{cli-kyysm7m3.js → cli-wp7cejrg.js} +1 -1
  44. package/dist/{cli-n75jt793.js → cli-z61ykvam.js} +6 -5
  45. package/dist/{cli-ceb5y160.js → cli-zdcdh02m.js} +1 -1
  46. package/dist/{cli-cmp8g3sw.js → cli-zfzznn7c.js} +45 -9
  47. package/dist/cli.js +22 -21
  48. package/dist/{codex-adapter-rn3aq7jm.js → codex-adapter-v3cz462q.js} +8 -9
  49. package/dist/{codex-hook-gthcmm0g.js → codex-hook-8jexc34x.js} +2 -2
  50. package/dist/{codex-session-runner-bge0cbsw.js → codex-session-runner-de1a5g5z.js} +8 -9
  51. package/dist/{commands-26phw0pc.js → commands-wfqqpn95.js} +5 -5
  52. package/dist/{db-jxjx6s1b.js → db-9zzv5y05.js} +5 -5
  53. package/dist/dispatcher-hfm1jbgx.js +74 -0
  54. package/dist/{google-generative-ai-tsskx7te.js → google-generative-ai-ndjmj5fa.js} +7 -7
  55. package/dist/{google-vertex-pkhpkbq9.js → google-vertex-fr665tqs.js} +10 -10
  56. package/dist/handlers-wrrhsack.js +65 -0
  57. package/dist/{hook-jw0ztn96.js → hook-9skye8zd.js} +9 -10
  58. package/dist/{hook-t6fqsggw.js → hook-fgjv62k5.js} +9 -10
  59. package/dist/{http-hyc7s57s.js → http-7grttjfq.js} +1434 -579
  60. package/dist/{index-bcyx5djw.js → index-29gxx9ar.js} +25 -17
  61. package/dist/{index-wtwpthe1.js → index-30pz2h9w.js} +25 -19
  62. package/dist/{index-1d9r6a74.js → index-4ek1df87.js} +26 -20
  63. package/dist/{index-sfhaq59d.js → index-myfwqqyq.js} +120 -102
  64. package/dist/index-pfhzrh1d.js +25 -0
  65. package/dist/{keepalive-fbsak4ya.js → keepalive-j75m2m7k.js} +7 -7
  66. package/dist/{lead-rdzdm627.js → lead-a1hpjt0a.js} +41 -49
  67. package/dist/{maintenance-25z1c1vf.js → maintenance-tngjzqdq.js} +8 -8
  68. package/dist/{node-209sxmg3.js → node-x1g4v0pq.js} +9 -9
  69. package/dist/{oauth-refresh-sweep-j3d7gtse.js → oauth-refresh-sweep-em1fn044.js} +6 -6
  70. package/dist/{onboard-dkcwrc75.js → onboard-fv7ezvwx.js} +13 -4
  71. package/dist/{openai-codex-responses-pm5gnyfa.js → openai-codex-responses-wy6a43wd.js} +11 -11
  72. package/dist/{openai-responses-vwvk1taj.js → openai-responses-6xwf7bzg.js} +13 -13
  73. package/dist/{opencode-adapter-dce4wskd.js → opencode-adapter-28axqa09.js} +4 -4
  74. package/dist/{otel-impl-8rgyy3j8.js → otel-impl-6vbb568y.js} +1 -1
  75. package/dist/{pi-mono-adapter-4s2bbkgq.js → pi-mono-adapter-jcqt71cg.js} +14 -16
  76. package/dist/{pricing-refresh-9cek71nq.js → pricing-refresh-nmm949an.js} +7 -7
  77. package/dist/{rbac-roles-vk8mqp9v.js → rbac-roles-0fz5qryx.js} +7 -7
  78. package/dist/{rbac-roles-z98s2ypy.js → rbac-roles-xtp05syf.js} +13 -9
  79. package/dist/{render-v2-3j26g0qh.js → render-v2-fpb8whme.js} +7 -7
  80. package/dist/{seed-pricing-7rxhcnzm.js → seed-pricing-zwa8bs3t.js} +6 -6
  81. package/dist/{setup-e2t7vz0c.js → setup-3ybwbdpd.js} +2 -2
  82. package/dist/{work-objects-8d3nd3x2.js → work-objects-rmayxcx7.js} +41 -16
  83. package/dist/{worker-0x9fn7nq.js → worker-rng4jhnh.js} +41 -49
  84. package/openapi.json +1351 -265
  85. package/package.json +2 -1
  86. package/src/agentmail/handlers.ts +95 -71
  87. package/src/be/db-client.ts +5 -0
  88. package/src/be/db.ts +12 -4
  89. package/src/be/extensions/db.ts +461 -0
  90. package/src/be/extensions/validate.ts +107 -0
  91. package/src/be/memory/boot-reembed.ts +17 -12
  92. package/src/be/memory/index.ts +12 -0
  93. package/src/be/memory/providers/openai-embedding.ts +4 -0
  94. package/src/be/memory/types.ts +2 -0
  95. package/src/be/migrations/155_extensions.sql +66 -0
  96. package/src/be/migrations/156_inbox_item_state_read_at.sql +7 -0
  97. package/src/be/rbac-roles.ts +3 -2
  98. package/src/be/scripts/typecheck.ts +56 -38
  99. package/src/be/seed-skills/index.ts +7 -0
  100. package/src/be/swarm-config-guard.ts +10 -2
  101. package/src/commands/onboard/compose-generator.ts +14 -2
  102. package/src/commands/onboard/env-generator.ts +7 -0
  103. package/src/commands/runner.ts +1 -1
  104. package/src/extensions/apply-task-create.ts +135 -0
  105. package/src/extensions/bundle-path.ts +10 -0
  106. package/src/extensions/contract-runtime.ts +7 -0
  107. package/src/extensions/contract-types.generated.ts +4 -0
  108. package/src/extensions/contract.ts +270 -0
  109. package/src/extensions/ctx.ts +186 -0
  110. package/src/extensions/dispatcher.ts +479 -0
  111. package/src/extensions/identity.ts +44 -0
  112. package/src/extensions/lifecycle.ts +303 -0
  113. package/src/extensions/loader.ts +207 -0
  114. package/src/extensions/post-bridge.ts +161 -0
  115. package/src/github/handlers.ts +185 -149
  116. package/src/gitlab/handlers.ts +97 -81
  117. package/src/heartbeat/heartbeat.ts +152 -83
  118. package/src/http/agents.ts +1 -1
  119. package/src/http/all-routes.ts +1 -0
  120. package/src/http/apps.ts +14 -10
  121. package/src/http/core.ts +34 -0
  122. package/src/http/extensions.ts +552 -0
  123. package/src/http/inbox-state.ts +2 -0
  124. package/src/http/index.ts +25 -0
  125. package/src/http/mcp-bridge.ts +96 -62
  126. package/src/http/mcp.ts +1 -1
  127. package/src/http/status.ts +6 -4
  128. package/src/http/tasks.ts +51 -35
  129. package/src/integrations/kapso/inbound.ts +15 -11
  130. package/src/jira/sync.ts +26 -18
  131. package/src/linear/sync.ts +26 -18
  132. package/src/prompts/session-templates.ts +5 -4
  133. package/src/rbac/admission.ts +1 -1
  134. package/src/rbac/elevated-agents.ts +24 -0
  135. package/src/rbac/legacy-policy.ts +47 -6
  136. package/src/rbac/permissions.ts +8 -0
  137. package/src/scheduler/deferred-task-waits.ts +133 -51
  138. package/src/scheduler/schedule-task.ts +50 -24
  139. package/src/scheduler/scheduler.ts +4 -2
  140. package/src/scripts-runtime/executors/native.ts +5 -1
  141. package/src/scripts-runtime/executors/types.ts +5 -0
  142. package/src/scripts-runtime/import-allowlist.ts +79 -19
  143. package/src/scripts-runtime/sdk-allowlist.ts +8 -0
  144. package/src/scripts-runtime/swarm-config.ts +8 -0
  145. package/src/scripts-runtime/swarm-sdk.ts +3 -0
  146. package/src/scripts-runtime/types/stdlib.d.ts +13 -0
  147. package/src/scripts-runtime/types/swarm-extension.d.ts +562 -0
  148. package/src/scripts-runtime/types/swarm-sdk.d.ts +13 -0
  149. package/src/server.ts +18 -0
  150. package/src/slack/HEURISTICS.md +1 -1
  151. package/src/slack/actions.ts +21 -17
  152. package/src/slack/blocks.ts +1 -1
  153. package/src/slack/handlers.ts +98 -5
  154. package/src/slack/inbound-files.ts +6 -2
  155. package/src/slack/render-v2.ts +181 -39
  156. package/src/slack/responses.ts +7 -4
  157. package/src/slack/router.ts +13 -3
  158. package/src/slack/steering.ts +3 -3
  159. package/src/slack/task-output.ts +21 -0
  160. package/src/slack/watcher.ts +3 -2
  161. package/src/slack/work-objects.ts +2 -1
  162. package/src/tasks/errors.ts +9 -0
  163. package/src/tasks/sibling-awareness.ts +35 -1
  164. package/src/tasks/worker-follow-up.ts +64 -12
  165. package/src/tests/base-prompt.test.ts +1 -0
  166. package/src/tests/deferred-task-wake.test.ts +161 -9
  167. package/src/tests/docs-index-footprint.test.js +3 -3
  168. package/src/tests/env-flag.test.ts +1 -0
  169. package/src/tests/extensions-db.test.ts +216 -0
  170. package/src/tests/extensions-dispatcher.test.ts +275 -0
  171. package/src/tests/extensions-example-heartbeat-remediate.test.ts +69 -0
  172. package/src/tests/extensions-example-mcp-install.test.ts +63 -0
  173. package/src/tests/extensions-example-no-exclamation-marks.test.ts +72 -0
  174. package/src/tests/extensions-example-rewrite-and-suppress.test.ts +70 -0
  175. package/src/tests/extensions-example-route-slack-channel.test.ts +217 -0
  176. package/src/tests/extensions-heartbeat.test.ts +291 -0
  177. package/src/tests/extensions-http.test.ts +512 -0
  178. package/src/tests/extensions-lead-equivalence.test.ts +70 -0
  179. package/src/tests/extensions-lifecycle.test.ts +209 -0
  180. package/src/tests/extensions-loader.test.ts +95 -0
  181. package/src/tests/extensions-mcp-tools.test.ts +353 -0
  182. package/src/tests/extensions-pre-task.test.ts +487 -0
  183. package/src/tests/extensions-run-context.test.ts +41 -0
  184. package/src/tests/extensions-tool-call.test.ts +288 -0
  185. package/src/tests/extensions-validate.test.ts +196 -0
  186. package/src/tests/fixtures/extensions/bad-import/hooks.ts +8 -0
  187. package/src/tests/fixtures/extensions/bad-import/manifest.json +9 -0
  188. package/src/tests/fixtures/extensions/bad-return-shape/hooks.ts +10 -0
  189. package/src/tests/fixtures/extensions/bad-return-shape/manifest.json +9 -0
  190. package/src/tests/fixtures/extensions/block-tasks-from-source/hooks.ts +22 -0
  191. package/src/tests/fixtures/extensions/block-tasks-from-source/manifest.json +9 -0
  192. package/src/tests/fixtures/extensions/ignore-channel/hooks.ts +22 -0
  193. package/src/tests/fixtures/extensions/ignore-channel/manifest.json +9 -0
  194. package/src/tests/fixtures/extensions/load.ts +10 -0
  195. package/src/tests/fixtures/extensions/minimal/hooks.ts +7 -0
  196. package/src/tests/fixtures/extensions/minimal/manifest.json +9 -0
  197. package/src/tests/fixtures/extensions/never-fail-long-tasks/hooks.ts +11 -0
  198. package/src/tests/fixtures/extensions/never-fail-long-tasks/manifest.json +9 -0
  199. package/src/tests/fixtures/extensions/no-exclamation-marks/hooks.ts +23 -0
  200. package/src/tests/fixtures/extensions/no-exclamation-marks/manifest.json +9 -0
  201. package/src/tests/fixtures/extensions/post-logger/hooks.ts +9 -0
  202. package/src/tests/fixtures/extensions/post-logger/manifest.json +9 -0
  203. package/src/tests/fixtures/extensions/priority-a/hooks.ts +7 -0
  204. package/src/tests/fixtures/extensions/priority-a/manifest.json +9 -0
  205. package/src/tests/fixtures/extensions/priority-b/hooks.ts +7 -0
  206. package/src/tests/fixtures/extensions/priority-b/manifest.json +9 -0
  207. package/src/tests/fixtures/extensions/record-only-on-tag/hooks.ts +16 -0
  208. package/src/tests/fixtures/extensions/record-only-on-tag/manifest.json +9 -0
  209. package/src/tests/fixtures/extensions/reserved-assets/hooks.ts +5 -0
  210. package/src/tests/fixtures/extensions/reserved-assets/manifest.json +10 -0
  211. package/src/tests/fixtures/extensions/rewrite-progress-prefix/hooks.ts +22 -0
  212. package/src/tests/fixtures/extensions/rewrite-progress-prefix/manifest.json +9 -0
  213. package/src/tests/fixtures/extensions/rewrite-task-priority/hooks.ts +7 -0
  214. package/src/tests/fixtures/extensions/rewrite-task-priority/manifest.json +9 -0
  215. package/src/tests/fixtures/extensions/route-channel-to-agent/hooks.ts +26 -0
  216. package/src/tests/fixtures/extensions/route-channel-to-agent/manifest.json +9 -0
  217. package/src/tests/fixtures/extensions/slow/hooks.ts +10 -0
  218. package/src/tests/fixtures/extensions/slow/manifest.json +9 -0
  219. package/src/tests/fixtures/extensions/suppress-lead-follow-up/hooks.ts +12 -0
  220. package/src/tests/fixtures/extensions/suppress-lead-follow-up/manifest.json +9 -0
  221. package/src/tests/fixtures/extensions/throws/hooks.ts +12 -0
  222. package/src/tests/fixtures/extensions/throws/manifest.json +9 -0
  223. package/src/tests/fixtures/extensions/worker-runtime/hooks.ts +5 -0
  224. package/src/tests/fixtures/extensions/worker-runtime/manifest.json +9 -0
  225. package/src/tests/fixtures/store-progress-malformed-output.json +4 -0
  226. package/src/tests/memory-edit-authorization.test.ts +32 -0
  227. package/src/tests/multi-runtime-enabled-stats.test.ts +2 -2
  228. package/src/tests/multi-runtime-registration.test.ts +14 -14
  229. package/src/tests/onboard-compose.test.ts +44 -0
  230. package/src/tests/preload.ts +5 -0
  231. package/src/tests/rbac-admission-e2e.test.ts +35 -4
  232. package/src/tests/rbac-admission.test.ts +18 -3
  233. package/src/tests/rbac-e2e-helpers.ts +18 -0
  234. package/src/tests/rbac-engine.test.ts +39 -0
  235. package/src/tests/rbac-mcp-admission-e2e.test.ts +22 -4
  236. package/src/tests/realtime-room-auth.test.ts +1 -1
  237. package/src/tests/reload-config.test.ts +61 -0
  238. package/src/tests/routing-affinity-inventory.test.ts +1 -1
  239. package/src/tests/script-connections.test.ts +2 -0
  240. package/src/tests/scripts-boot-reembed.test.ts +4 -0
  241. package/src/tests/scripts-embeddings.test.ts +4 -0
  242. package/src/tests/scripts-import-allowlist.test.ts +44 -1
  243. package/src/tests/scripts-mcp-e2e.test.ts +80 -0
  244. package/src/tests/scripts-only-gating.test.ts +6 -0
  245. package/src/tests/scripts-runtime-identity.test.ts +1 -1
  246. package/src/tests/slack-blocks.test.ts +12 -1
  247. package/src/tests/slack-render-v2.test.ts +100 -5
  248. package/src/tests/slack-router.test.ts +17 -2
  249. package/src/tests/slack-steering.test.ts +19 -0
  250. package/src/tests/slack-task-output.test.ts +36 -0
  251. package/src/tests/status.test.ts +12 -0
  252. package/src/tests/steering-enabled.test.ts +6 -8
  253. package/src/tests/store-progress-attachments-handler.test.ts +100 -1
  254. package/src/tests/swarm-config-reserved-keys.test.ts +6 -1
  255. package/src/tests/task-tool-preload-mcp.test.ts +4 -1
  256. package/src/tests/tool-annotations.test.ts +1 -1
  257. package/src/tests/tool-registrar-no-input.test.ts +42 -1
  258. package/src/tools/defer-task.ts +1 -1
  259. package/src/tools/extension-activate-version.ts +38 -0
  260. package/src/tools/extension-common.ts +85 -0
  261. package/src/tools/extension-delete.ts +29 -0
  262. package/src/tools/extension-disable.ts +36 -0
  263. package/src/tools/extension-enable.ts +36 -0
  264. package/src/tools/extension-install.ts +65 -0
  265. package/src/tools/extension-list.ts +66 -0
  266. package/src/tools/script-common.ts +43 -18
  267. package/src/tools/send-task.ts +69 -107
  268. package/src/tools/store-progress.ts +30 -0
  269. package/src/tools/task-action.ts +43 -22
  270. package/src/tools/task-tool-ctx.ts +2 -0
  271. package/src/tools/tool-config.ts +7 -1
  272. package/src/tools/utils.ts +140 -16
  273. package/src/types.ts +133 -0
  274. package/src/utils/multi-runtime.ts +2 -2
  275. package/src/utils/steering-enabled.ts +4 -4
  276. package/src/workflows/executors/agent-task.ts +13 -3
  277. package/templates/extensions/README.md +17 -0
  278. package/templates/extensions/notify-on-complete/README.md +11 -0
  279. package/templates/extensions/notify-on-complete/hooks.ts +61 -0
  280. package/templates/extensions/notify-on-complete/manifest.json +7 -0
  281. package/templates/extensions/require-ticket-ref/README.md +11 -0
  282. package/templates/extensions/require-ticket-ref/hooks.ts +30 -0
  283. package/templates/extensions/require-ticket-ref/manifest.json +7 -0
  284. package/templates/extensions/require-verification-note/README.md +11 -0
  285. package/templates/extensions/require-verification-note/hooks.ts +32 -0
  286. package/templates/extensions/require-verification-note/manifest.json +7 -0
  287. package/templates/skills/agent-browser/SKILL.md +84 -0
  288. package/templates/skills/agent-browser/content.md +21 -0
  289. package/templates/skills/scheduling/content.md +2 -0
  290. package/templates/skills/swarm-extensions/config.json +14 -0
  291. package/templates/skills/swarm-extensions/content.md +120 -0
  292. package/dist/cli-b8x19vbh.js +0 -823
  293. package/dist/cli-n1yj5fd5.js +0 -164
  294. package/dist/cli-q04q68be.js +0 -3066
  295. package/dist/cli-qgm2pfpq.js +0 -6377
  296. package/dist/cli-rnsr8asm.js +0 -4323
  297. package/dist/cli-rwp3ksm7.js +0 -545
  298. package/dist/cli-xjb9wghj.js +0 -668
  299. package/dist/cli-xvkmedgn.js +0 -332
  300. package/dist/cli-ydk0wjj8.js +0 -338
  301. package/dist/handlers-vr1djg4n.js +0 -41
  302. package/dist/{claude-managed-setup-f5pmsyrp.js → claude-managed-setup-48dwg9bv.js} +3 -3
  303. package/dist/{cli-81r683bv.js → cli-ch38q5z4.js} +0 -0
  304. package/dist/{cli-wk0gjrn3.js → cli-kj76p1ez.js} +6 -6
  305. package/dist/{cli-c71mg51n.js → cli-r2fpe7cx.js} +5 -5
  306. package/dist/{cli-8qx7jnrk.js → cli-y6x0gqtg.js} +16 -16
  307. package/dist/{index-aa3sa6s8.js → index-2wsdsnmm.js} +1 -1
  308. package/dist/{mistral-conversations-4ryk7h83.js → mistral-conversations-qhr84ph3.js} +9 -9
  309. package/dist/{openai-completions-qdcmkfnr.js → openai-completions-32rdwmze.js} +17 -17
  310. package/dist/{openrouter-images-qx18fyp8.js → openrouter-images-9f9ek4rw.js} +3 -3
  311. package/dist/{pi-messages-79jvzz1d.js → pi-messages-b1mbr6gp.js} +4 -4
@@ -0,0 +1,36 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { slackTaskOutput } from "../slack/task-output";
3
+
4
+ const task = {
5
+ status: "completed" as const,
6
+ tags: ["deferred"],
7
+ output:
8
+ "Deferred until tomorrow 17:30 UTC ([12345678](https://example.com/schedules/12345678)) -> check the build",
9
+ };
10
+
11
+ describe("Slack deferral output", () => {
12
+ test("keeps the pending work without the ETA or schedule link", () => {
13
+ expect(slackTaskOutput(task)).toBe("Pending: check the build");
14
+ expect(task.output).toContain("tomorrow 17:30 UTC");
15
+ });
16
+
17
+ test("removes an ETA-only line and preserves punctuation in the pending description", () => {
18
+ expect(slackTaskOutput({ ...task, output: task.output.replace("check the build", "") })).toBe(
19
+ "",
20
+ );
21
+ expect(slackTaskOutput({ ...task, output: `${task.output} (CI) -> deploy` })).toBe(
22
+ "Pending: check the build (CI) -> deploy",
23
+ );
24
+ });
25
+
26
+ test("preserves final continuation results even with inherited deferred tags", () => {
27
+ expect(slackTaskOutput({ ...task, output: "Build passed." })).toBe("Build passed.");
28
+ });
29
+
30
+ test("preserves schema output and non-deferral output verbatim", () => {
31
+ expect(slackTaskOutput({ ...task, outputSchema: { type: "string" } })).toBe(task.output);
32
+ expect(slackTaskOutput({ ...task, tags: [] })).toBe(task.output);
33
+ expect(slackTaskOutput({ ...task, status: "in_progress" })).toBe(task.output);
34
+ expect(slackTaskOutput({ ...task, output: undefined })).toBeUndefined();
35
+ });
36
+ });
@@ -31,6 +31,7 @@ import {
31
31
  updateAgentCredStatus,
32
32
  } from "../be/db";
33
33
  import { storeOAuthTokens, upsertOAuthApp } from "../be/db-queries/oauth";
34
+ import { resetEmbeddingProvider } from "../be/memory";
34
35
  import { validateProviderCredentials } from "../commands/provider-credentials";
35
36
  import {
36
37
  _resetTestConnectionCache,
@@ -123,6 +124,11 @@ function clearEnv() {
123
124
  for (const k of ENV_KEYS_TO_RESET) {
124
125
  delete process.env[k];
125
126
  }
127
+ // The embedding provider memoizes its API key at construction (see
128
+ // OpenAIEmbeddingProvider); reset the singleton so embeddingsMilestone()
129
+ // (which now reads the live provider, not process.env directly) reflects
130
+ // whatever this test just set/cleared instead of a stale prior test's key.
131
+ resetEmbeddingProvider();
126
132
  }
127
133
 
128
134
  function restoreEnv() {
@@ -245,14 +251,20 @@ describe("setup milestones", () => {
245
251
  hint: "Memory search is off. Set OPENAI_API_KEY (or EMBEDDING_API_KEY) on the API server to enable it; it is cheap.",
246
252
  });
247
253
 
254
+ // The provider memoizes its API key at construction; in production a
255
+ // config reload resets it (see resetEmbeddingProvider in src/http/core.ts).
256
+ // Mirror that here after each direct env mutation.
248
257
  process.env.OPENAI_API_KEY = "example-openai-embedding-key";
258
+ resetEmbeddingProvider();
249
259
  expect(getMilestone(await buildStatusPayload(), "embeddings").state).toBe("configured");
250
260
 
251
261
  process.env.EMBEDDING_API_KEY = "";
262
+ resetEmbeddingProvider();
252
263
  expect(getMilestone(await buildStatusPayload(), "embeddings").state).toBe("unverified");
253
264
 
254
265
  process.env.EMBEDDING_API_KEY = "example-dedicated-embedding-key";
255
266
  delete process.env.OPENAI_API_KEY;
267
+ resetEmbeddingProvider();
256
268
  expect(getMilestone(await buildStatusPayload(), "embeddings").state).toBe("configured");
257
269
  });
258
270
 
@@ -48,12 +48,11 @@ function restoreEnv(name: string, value: string | undefined): void {
48
48
  }
49
49
 
50
50
  /**
51
- * Steering is opt-in (disabled unless STEERING_ENABLED=true|1), so "disabled"
52
- * is simply the default: unset the flag for the duration of the callback.
51
+ * Exercise an explicit opt-out without depending on the deployment default.
53
52
  */
54
53
  async function withSteeringDisabled<T>(run: () => Promise<T> | T): Promise<T> {
55
54
  const previous = process.env.STEERING_ENABLED;
56
- delete process.env.STEERING_ENABLED;
55
+ process.env.STEERING_ENABLED = "false";
57
56
  try {
58
57
  return await run();
59
58
  } finally {
@@ -116,8 +115,7 @@ async function api(
116
115
  const originalSteeringEnabled = process.env.STEERING_ENABLED;
117
116
 
118
117
  beforeAll(async () => {
119
- // The "enabled" halves of these tests opt in explicitly (steering is off by
120
- // default); withSteeringDisabled() drops the flag to exercise the default.
118
+ // Pin both branches explicitly so inherited configuration cannot affect them.
121
119
  process.env.STEERING_ENABLED = "true";
122
120
  await removeTestDb();
123
121
  process.env.DATABASE_PATH = TEST_DB_PATH;
@@ -147,9 +145,9 @@ afterAll(async () => {
147
145
  await removeTestDb();
148
146
  });
149
147
 
150
- describe("STEERING_ENABLED opt-in", () => {
151
- test("steering is disabled by default (flag unset) and on falsy values", () => {
152
- expect(isSteeringEnabled({})).toBe(false);
148
+ describe("STEERING_ENABLED defaults and overrides", () => {
149
+ test("steering defaults on and accepts explicit opt-outs", () => {
150
+ expect(isSteeringEnabled({})).toBe(true);
153
151
  expect(isSteeringEnabled({ STEERING_ENABLED: "false" })).toBe(false);
154
152
  expect(isSteeringEnabled({ STEERING_ENABLED: "0" })).toBe(false);
155
153
  expect(isSteeringEnabled({ STEERING_ENABLED: "true" })).toBe(true);
@@ -17,7 +17,7 @@
17
17
  * org/drive and credentials
18
18
  * 4. missing files and wrong drives both block attachment + task writes
19
19
  */
20
- import { afterAll, beforeAll, describe, expect, test } from "bun:test";
20
+ import { afterAll, beforeAll, describe, expect, spyOn, test } from "bun:test";
21
21
  import crypto from "node:crypto";
22
22
  import { unlink } from "node:fs/promises";
23
23
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -34,6 +34,7 @@ import {
34
34
  upsertSwarmConfig,
35
35
  } from "../be/db";
36
36
  import { registerStoreProgressTool } from "../tools/store-progress";
37
+ import malformedOutput from "./fixtures/store-progress-malformed-output.json";
37
38
 
38
39
  const TEST_DB_PATH = "./test-store-progress-attachments-handler.sqlite";
39
40
  const ORIGINAL_FETCH = globalThis.fetch;
@@ -137,6 +138,104 @@ describe("store-progress handler — attachments insert path", () => {
137
138
  };
138
139
  }
139
140
 
141
+ test("salvages the literal 59811b57 output and registers both URL attachments", async () => {
142
+ const task = await createTaskExtended("salvage leaked tool parameters", { agentId });
143
+ await startTask(task.id);
144
+ const result = (await buildServer().handler(
145
+ { taskId: task.id, status: "completed", output: malformedOutput.output },
146
+ buildMeta(),
147
+ )) as StoreProgressResult;
148
+
149
+ expect(result.structuredContent.success).toBe(true);
150
+ const saved = await getTaskById(task.id);
151
+ expect(saved?.output).toBe(malformedOutput.output.split("</output>")[0]);
152
+ expect(saved?.output?.endsWith("reposted clean.")).toBe(true);
153
+ const attachments = await getTaskAttachments(task.id);
154
+ expect(attachments).toHaveLength(2);
155
+ expect(attachments.every((a) => a.kind === "url")).toBe(true);
156
+ expect(attachments.map((a) => a.url).sort()).toEqual([
157
+ "https://api.desplega.agent-swarm.dev/p/556b11781dbb40bf9b1eb1b3315adae6",
158
+ "https://x.com/desplegalabs/status/2100595056939827590",
159
+ ]);
160
+ expect(attachments.find((a) => a.isPrimary)?.name).toBe("X reply in the Jev thread");
161
+ });
162
+
163
+ test("preserves clean output byte-for-byte", async () => {
164
+ const output = "The output is ready: 2 < 3.\n Keep this whitespace. ";
165
+ const task = await createTaskExtended("clean output control", { agentId });
166
+ await startTask(task.id);
167
+ const result = (await buildServer().handler(
168
+ { taskId: task.id, status: "completed", output },
169
+ buildMeta(),
170
+ )) as StoreProgressResult;
171
+ expect(result.structuredContent.success).toBe(true);
172
+ expect((await getTaskById(task.id))?.output).toBe(output);
173
+ expect(await getTaskAttachments(task.id)).toHaveLength(0);
174
+ });
175
+
176
+ test.each([
177
+ ["invalid JSON", '</output>\n<parameter name="attachments">[broken'],
178
+ ["invalid attachment schema", '</output><parameter name="attachments">[{"kind":"url"}]'],
179
+ ["unknown parameter", '</output><parameter name="status">"failed"'],
180
+ ["alternate closing tag", '</parameter>\n<parameter name="progress">Not JSON'],
181
+ ["bare opener", '<parameter name="progress">Not JSON'],
182
+ ["non-array JSON", '</output><parameter name="attachments">null'],
183
+ ])("strips %s without losing the clean result", async (_name, tail) => {
184
+ const task = await createTaskExtended("unrecoverable tail", { agentId });
185
+ await startTask(task.id);
186
+ const warning = spyOn(console, "warn").mockImplementation(() => {});
187
+ try {
188
+ const result = (await buildServer().handler(
189
+ { taskId: task.id, status: "completed", output: `Clean result.${tail}` },
190
+ buildMeta(),
191
+ )) as StoreProgressResult;
192
+ expect(result.structuredContent.success).toBe(true);
193
+ expect((await getTaskById(task.id))?.output).toBe("Clean result.");
194
+ expect((await getTaskById(task.id))?.status).toBe("completed");
195
+ expect(await getTaskAttachments(task.id)).toHaveLength(0);
196
+ expect(warning).toHaveBeenCalledTimes(1);
197
+ expect(warning.mock.calls[0]?.[0]).not.toContain(tail);
198
+ } finally {
199
+ warning.mockRestore();
200
+ }
201
+ });
202
+
203
+ test("appends recovered pointers to explicit attachments through the normal dedupe path", async () => {
204
+ const task = await createTaskExtended("mixed explicit and leaked attachments", { agentId });
205
+ await startTask(task.id);
206
+ const attachment = { kind: "url", name: "link", url: "https://example.com/link" };
207
+ const result = (await buildServer().handler(
208
+ {
209
+ taskId: task.id,
210
+ status: "completed",
211
+ output: `Done.<parameter name="attachments">${JSON.stringify([attachment])}</parameter>`,
212
+ attachments: [attachment],
213
+ },
214
+ buildMeta(),
215
+ )) as StoreProgressResult;
216
+ expect(result.structuredContent.success).toBe(true);
217
+ expect((await getTaskById(task.id))?.output).toBe("Done.");
218
+ expect(await getTaskAttachments(task.id)).toHaveLength(1);
219
+ });
220
+
221
+ test("recovered agent-fs pointers still require ordinary scope verification", async () => {
222
+ const task = await createTaskExtended("verify recovered pointer", { agentId });
223
+ await startTask(task.id);
224
+ const result = (await buildServer().handler(
225
+ {
226
+ taskId: task.id,
227
+ status: "completed",
228
+ output:
229
+ 'Done.</output><parameter name="attachments">[{"kind":"agent-fs","name":"missing.md","path":"/missing.md"}]',
230
+ },
231
+ buildMeta(),
232
+ )) as StoreProgressResult;
233
+ expect(result.structuredContent.success).toBe(false);
234
+ expect(result.structuredContent.message).toContain("both orgId and driveId must resolve");
235
+ expect((await getTaskById(task.id))?.status).toBe("in_progress");
236
+ expect(await getTaskAttachments(task.id)).toHaveLength(0);
237
+ });
238
+
140
239
  test("inserts attachment row on an in-progress task (baseline)", async () => {
141
240
  const task = await createTaskExtended("handler in-progress baseline", {
142
241
  agentId,
@@ -134,7 +134,12 @@ describe("swarm-config reserved keys guard", () => {
134
134
  await unlink(`${TEST_DB_PATH}-shm`).catch(() => {});
135
135
  });
136
136
 
137
- for (const key of ["CORS_ALLOW_ANY_ORIGIN", "cors_allow_any_origin"]) {
137
+ for (const key of [
138
+ "CORS_ALLOW_ANY_ORIGIN",
139
+ "cors_allow_any_origin",
140
+ "EXTENSION_ALLOW_LEAD_ACTIVATION",
141
+ "extension_allow_lead_activation",
142
+ ]) {
138
143
  test(`rejects deployment-only ${key} through DB, HTTP, and MCP`, async () => {
139
144
  const config = { scope: "global" as const, key, value: "true" };
140
145
  await expect(upsertSwarmConfig(config)).rejects.toThrow(EXPECTED_MESSAGE(key));
@@ -73,11 +73,14 @@ async function pins(client: Client): Promise<string[]> {
73
73
  .sort();
74
74
  }
75
75
 
76
- test("wire response is default-off, per-task, additive, and fixed for one MCP session", async () => {
76
+ test("wire response is default-on, per-task, additive, and fixed for one MCP session", async () => {
77
77
  const task = await createTaskExtended("review fixture", { agentId, taskType: "review" });
78
78
  const other = await createTaskExtended("other fixture", { agentId, taskType: "other" });
79
79
  const foreign = await createTaskExtended("unowned", { taskType: "review" });
80
80
  await configure("TASK_TOOL_MANIFESTS", '{"taskTypes":{"review":["script-run","get-tasks"]}}');
81
+ const defaultOn = await connect(task.id);
82
+ expect(await pins(defaultOn)).toEqual(["get-tasks", "script-run"]);
83
+ await configure("TASK_TOOL_PRELOAD_ENABLED", "false");
81
84
  const off = await connect(task.id);
82
85
  const baseline = (await off.listTools()).tools;
83
86
  expect(baseline.some((tool) => tool.name === "script-run")).toBe(true);
@@ -378,7 +378,7 @@ describe("Tool Annotations & Classification", () => {
378
378
  // Includes 11 skill tools, 7 MCP server tools, reusable script tools, and the
379
379
  // native Kapso/WhatsApp tools (register/unregister number + send/reply message).
380
380
  expect(count).toBeGreaterThanOrEqual(45);
381
- expect(count).toBeLessThanOrEqual(141);
381
+ expect(count).toBeLessThanOrEqual(147);
382
382
  });
383
383
 
384
384
  test("core tools are fewer than deferred tools", () => {
@@ -1,7 +1,11 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import * as z from "zod";
4
- import { createToolRegistrar } from "../tools/utils";
4
+ import {
5
+ createToolRegistrar,
6
+ finalizeSwarmToolResult,
7
+ markExtensionRequestOrigin,
8
+ } from "../tools/utils";
5
9
 
6
10
  describe("createToolRegistrar with no inputSchema", () => {
7
11
  test("handler receives requestInfo from meta when no inputSchema is defined", async () => {
@@ -117,4 +121,41 @@ describe("createToolRegistrar with no inputSchema", () => {
117
121
  });
118
122
  expect(result).toBeDefined();
119
123
  });
124
+
125
+ test("server-marked extension calls carry extension origin", async () => {
126
+ const server = new McpServer({ name: "test-extension-origin", version: "1.0.0" });
127
+ let callOrigin: string | undefined;
128
+ createToolRegistrar(server)(
129
+ "test-extension-origin-tool",
130
+ {
131
+ inputSchema: z.object({}),
132
+ outputSchema: z.looseObject({}),
133
+ },
134
+ async (_args, requestInfo) => {
135
+ callOrigin = requestInfo.callOrigin;
136
+ return { ok: true, message: "ok" };
137
+ },
138
+ );
139
+ const registeredTools = (server as Record<string, unknown>)._registeredTools as Record<
140
+ string,
141
+ { handler: (args: unknown, extra: unknown) => Promise<unknown> }
142
+ >;
143
+ const extra = markExtensionRequestOrigin({
144
+ sessionId: "extension",
145
+ requestInfo: { headers: { "x-agent-id": "extension-agent" } },
146
+ });
147
+
148
+ await registeredTools["test-extension-origin-tool"]!.handler({}, extra);
149
+ expect(callOrigin).toBe("extension");
150
+ });
151
+
152
+ test("extension calls bypass the model context ceiling", async () => {
153
+ const result = await finalizeSwarmToolResult(
154
+ "large-extension-result",
155
+ { ok: true, message: "ok", data: { value: "x".repeat(1_100_000) } },
156
+ { agentId: "extension-agent", callOrigin: "extension" },
157
+ );
158
+ expect(result.structuredContent?.truncation).toBeUndefined();
159
+ expect((result.structuredContent?.value as string).length).toBe(1_100_000);
160
+ });
120
161
  });
@@ -169,7 +169,7 @@ export const registerDeferTaskTool = (server: McpServer) => {
169
169
  })
170
170
  .optional()
171
171
  .describe(
172
- "Wake early on a task event. Provide taskId or nonempty, unique taskIds. mode defaults to all (every member must match); any wakes on the first match. settled covers completed or failed. Deferred members follow their continuations. Any already-terminal member rejects the request; one delayMs/runAt ceiling is still required for the whole set.",
172
+ "Wake early on a task event. Provide taskId or nonempty, unique taskIds. mode defaults to all (every member must match); any wakes on the first match. settled covers completed, failed, or cancelled. Deferred and superseded members follow their continuations; a superseded member without a resume child holds to the ceiling. Any already-terminal member rejects the request; one delayMs/runAt ceiling is still required for the whole set.",
173
173
  ),
174
174
  summary: z
175
175
  .string()
@@ -0,0 +1,38 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as z from "zod";
3
+ import { createToolRegistrar, toolOk } from "@/tools/utils";
4
+ import {
5
+ coerceExtensionSummary,
6
+ extensionToolOutputSchema,
7
+ proxyExtensionsApi,
8
+ } from "./extension-common";
9
+
10
+ export const registerExtensionActivateVersionTool = (server: McpServer) => {
11
+ createToolRegistrar(server)(
12
+ "extension-activate-version",
13
+ {
14
+ title: "Extension Activate Version",
15
+ description:
16
+ "Activate a stored extension version, reloading it if enabled. Requires a lead, operator, or dashboard user.",
17
+ annotations: { openWorldHint: false },
18
+ inputSchema: z
19
+ .object({
20
+ id: z.string().uuid().describe("Installed extension ID from extension-list."),
21
+ version: z.number().int().min(1).describe("Stored version number to activate."),
22
+ })
23
+ .strict(),
24
+ outputSchema: extensionToolOutputSchema,
25
+ },
26
+ async (args, requestInfo) =>
27
+ proxyExtensionsApi({
28
+ method: "POST",
29
+ path: `/api/extensions/${args.id}/activate-version`,
30
+ body: { version: args.version },
31
+ requestInfo,
32
+ success: (data) =>
33
+ toolOk("Extension activated.", {
34
+ data: coerceExtensionSummary((data as { extension?: unknown }).extension),
35
+ }),
36
+ }),
37
+ );
38
+ };
@@ -0,0 +1,85 @@
1
+ import * as z from "zod";
2
+ import { proxySwarmApi } from "./script-common";
3
+ import { type RequestInfo, type SwarmToolResult, swarmToolOutputSchema, toolErr } from "./utils";
4
+
5
+ export const EXTENSION_TRANSPORT_ERROR =
6
+ "extension_* tools require HTTP MCP transport because agent identity is unavailable over stdio. Use MCP_BASE_URL=http://... or invoke the HTTP API directly.";
7
+
8
+ const extensionListItemSchema = z.looseObject({
9
+ id: z.string().optional(),
10
+ name: z.string().optional(),
11
+ version: z.number().optional(),
12
+ activeVersion: z.number().optional(),
13
+ enabled: z.boolean().optional(),
14
+ status: z.string().optional(),
15
+ priority: z.number().optional(),
16
+ consecutiveFailures: z.number().optional(),
17
+ });
18
+
19
+ export const extensionToolOutputSchema = swarmToolOutputSchema({
20
+ id: z.string().optional(),
21
+ name: z.string().optional(),
22
+ version: z.number().optional(),
23
+ activeVersion: z.number().optional(),
24
+ enabled: z.boolean().optional(),
25
+ status: z.string().optional(),
26
+ priority: z.number().optional(),
27
+ consecutiveFailures: z.number().optional(),
28
+ contentDeduped: z.boolean().optional(),
29
+ deleted: z.boolean().optional(),
30
+ extensions: z.array(extensionListItemSchema).optional(),
31
+ });
32
+
33
+ function isRecord(value: unknown): value is Record<string, unknown> {
34
+ return typeof value === "object" && value !== null && !Array.isArray(value);
35
+ }
36
+
37
+ export function coerceExtensionSummary(entry: unknown) {
38
+ const extension = isRecord(entry) ? entry : {};
39
+ return {
40
+ id: typeof extension.id === "string" ? extension.id : undefined,
41
+ name: typeof extension.name === "string" ? extension.name : undefined,
42
+ version: typeof extension.version === "number" ? extension.version : undefined,
43
+ activeVersion:
44
+ typeof extension.activeVersion === "number" ? extension.activeVersion : undefined,
45
+ enabled: typeof extension.enabled === "boolean" ? extension.enabled : undefined,
46
+ status: typeof extension.status === "string" ? extension.status : undefined,
47
+ priority: typeof extension.priority === "number" ? extension.priority : undefined,
48
+ consecutiveFailures:
49
+ typeof extension.consecutiveFailures === "number" ? extension.consecutiveFailures : undefined,
50
+ };
51
+ }
52
+
53
+ function errorMessage(data: unknown, status: number): string {
54
+ if (isRecord(data) && typeof data.error === "string" && data.error) return data.error;
55
+ return `Extensions API request failed with ${status}`;
56
+ }
57
+
58
+ function renderDiagnostics(data: unknown): string | undefined {
59
+ if (!isRecord(data) || !Array.isArray(data.diagnostics)) return undefined;
60
+ const diagnostics = data.diagnostics.map(String).filter(Boolean);
61
+ return diagnostics.length > 0 ? diagnostics.join("\n") : undefined;
62
+ }
63
+
64
+ export async function proxyExtensionsApi<T>(args: {
65
+ method: "GET" | "POST" | "DELETE";
66
+ path: string;
67
+ body?: unknown;
68
+ requestInfo: RequestInfo;
69
+ success: (data: unknown) => SwarmToolResult<T & Record<string, unknown>>;
70
+ }): Promise<SwarmToolResult<T & Record<string, unknown>>> {
71
+ return proxySwarmApi({
72
+ ...args,
73
+ transportError: EXTENSION_TRANSPORT_ERROR,
74
+ respond: (data, response) => {
75
+ if (!response.ok) {
76
+ const diagnostics = renderDiagnostics(data);
77
+ if (response.status === 400 && diagnostics) {
78
+ return toolErr("Extension rejected by typecheck.", { details: diagnostics });
79
+ }
80
+ return toolErr(errorMessage(data, response.status), { details: diagnostics });
81
+ }
82
+ return args.success(data);
83
+ },
84
+ });
85
+ }
@@ -0,0 +1,29 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as z from "zod";
3
+ import { createToolRegistrar, toolOk } from "@/tools/utils";
4
+ import { extensionToolOutputSchema, proxyExtensionsApi } from "./extension-common";
5
+
6
+ export const registerExtensionDeleteTool = (server: McpServer) => {
7
+ createToolRegistrar(server)(
8
+ "extension-delete",
9
+ {
10
+ title: "Extension Delete",
11
+ description:
12
+ "Uninstall a disabled extension and delete its stored history. Disable it first with extension-disable. Requires a lead, operator, or dashboard user.",
13
+ annotations: { openWorldHint: false, destructiveHint: true },
14
+ inputSchema: z
15
+ .object({
16
+ id: z.string().uuid().describe("Installed extension ID from extension-list."),
17
+ })
18
+ .strict(),
19
+ outputSchema: extensionToolOutputSchema,
20
+ },
21
+ async (args, requestInfo) =>
22
+ proxyExtensionsApi({
23
+ method: "DELETE",
24
+ path: `/api/extensions/${args.id}`,
25
+ requestInfo,
26
+ success: () => toolOk("Extension deleted.", { data: { id: args.id, deleted: true } }),
27
+ }),
28
+ );
29
+ };
@@ -0,0 +1,36 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as z from "zod";
3
+ import { createToolRegistrar, toolOk } from "@/tools/utils";
4
+ import {
5
+ coerceExtensionSummary,
6
+ extensionToolOutputSchema,
7
+ proxyExtensionsApi,
8
+ } from "./extension-common";
9
+
10
+ export const registerExtensionDisableTool = (server: McpServer) => {
11
+ createToolRegistrar(server)(
12
+ "extension-disable",
13
+ {
14
+ title: "Extension Disable",
15
+ description:
16
+ "Unload and disable an installed extension. Requires a lead, operator, or dashboard user.",
17
+ annotations: { openWorldHint: false },
18
+ inputSchema: z
19
+ .object({
20
+ id: z.string().uuid().describe("Installed extension ID from extension-list."),
21
+ })
22
+ .strict(),
23
+ outputSchema: extensionToolOutputSchema,
24
+ },
25
+ async (args, requestInfo) =>
26
+ proxyExtensionsApi({
27
+ method: "POST",
28
+ path: `/api/extensions/${args.id}/disable`,
29
+ requestInfo,
30
+ success: (data) =>
31
+ toolOk("Extension disabled.", {
32
+ data: coerceExtensionSummary((data as { extension?: unknown }).extension),
33
+ }),
34
+ }),
35
+ );
36
+ };
@@ -0,0 +1,36 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as z from "zod";
3
+ import { createToolRegistrar, toolOk } from "@/tools/utils";
4
+ import {
5
+ coerceExtensionSummary,
6
+ extensionToolOutputSchema,
7
+ proxyExtensionsApi,
8
+ } from "./extension-common";
9
+
10
+ export const registerExtensionEnableTool = (server: McpServer) => {
11
+ createToolRegistrar(server)(
12
+ "extension-enable",
13
+ {
14
+ title: "Extension Enable",
15
+ description:
16
+ "Load and enable an installed extension. Requires a lead, operator, or dashboard user.",
17
+ annotations: { openWorldHint: false },
18
+ inputSchema: z
19
+ .object({
20
+ id: z.string().uuid().describe("Installed extension ID from extension-list."),
21
+ })
22
+ .strict(),
23
+ outputSchema: extensionToolOutputSchema,
24
+ },
25
+ async (args, requestInfo) =>
26
+ proxyExtensionsApi({
27
+ method: "POST",
28
+ path: `/api/extensions/${args.id}/enable`,
29
+ requestInfo,
30
+ success: (data) =>
31
+ toolOk("Extension enabled.", {
32
+ data: coerceExtensionSummary((data as { extension?: unknown }).extension),
33
+ }),
34
+ }),
35
+ );
36
+ };
@@ -0,0 +1,65 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as z from "zod";
3
+ import { createToolRegistrar, toolOk } from "@/tools/utils";
4
+ import { ExtensionManifestSchema } from "@/types";
5
+ import {
6
+ coerceExtensionSummary,
7
+ extensionToolOutputSchema,
8
+ proxyExtensionsApi,
9
+ } from "./extension-common";
10
+
11
+ export const EXTENSION_INSTALL_DESCRIPTION =
12
+ "Validate and store a TypeScript extension bundle (manifest plus a files map with one hooks.ts). Fetch the hook contract first: GET /api/extensions/type-defs returns swarm-extension.d.ts with every event and modify shape. The extension remains disabled until a lead, operator, or dashboard user enables it with extension-enable.";
13
+
14
+ function extensionInstallResult(data: unknown, fallbackName: string) {
15
+ const body = data as {
16
+ extension?: unknown;
17
+ contentDeduped?: unknown;
18
+ };
19
+ const extension = coerceExtensionSummary(body.extension);
20
+ const name = extension.name ?? fallbackName;
21
+ const version = typeof extension.version === "number" ? ` v${extension.version}` : "";
22
+ const deduped = body.contentDeduped === true ? " Content was unchanged." : "";
23
+ const enableInstruction =
24
+ "A lead, operator, or dashboard user can enable it with `extension-enable` or `POST /api/extensions/{id}/enable`.";
25
+
26
+ return toolOk(`Extension \`${name}\`${version} installed.${deduped} ${enableInstruction}`, {
27
+ data: {
28
+ ...extension,
29
+ name,
30
+ contentDeduped: body.contentDeduped === true,
31
+ },
32
+ });
33
+ }
34
+
35
+ export const registerExtensionInstallTool = (server: McpServer) => {
36
+ createToolRegistrar(server)(
37
+ "extension-install",
38
+ {
39
+ title: "Extension Install",
40
+ description: EXTENSION_INSTALL_DESCRIPTION,
41
+ annotations: { openWorldHint: false },
42
+ inputSchema: z
43
+ .object({
44
+ manifest: ExtensionManifestSchema.describe("Extension bundle manifest."),
45
+ files: z.record(z.string(), z.string()).describe("Bundle files keyed by relative path."),
46
+ priority: z
47
+ .number()
48
+ .int()
49
+ .optional()
50
+ .describe("Handler priority. Lower values run first."),
51
+ config: z.record(z.string(), z.unknown()).optional().describe("Extension configuration."),
52
+ })
53
+ .strict(),
54
+ outputSchema: extensionToolOutputSchema,
55
+ },
56
+ async (args, requestInfo) =>
57
+ proxyExtensionsApi({
58
+ method: "POST",
59
+ path: "/api/extensions/install",
60
+ body: args,
61
+ requestInfo,
62
+ success: (data) => extensionInstallResult(data, args.manifest.name),
63
+ }),
64
+ );
65
+ };