@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,209 @@
1
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
2
+ import { closeDb, createTaskExtended, getAllAgents, getDbClient, getKv, initDb } from "../be/db";
3
+ import {
4
+ getExtensionById,
5
+ installExtension,
6
+ listExtensionRuns,
7
+ setExtensionState,
8
+ } from "../be/extensions/db";
9
+ import { dispatchPre, listRegistered } from "../extensions/dispatcher";
10
+ import {
11
+ activateVersion,
12
+ disableExtension,
13
+ enableExtension,
14
+ loadEnabledExtensions,
15
+ stopExtensionRuntime,
16
+ } from "../extensions/lifecycle";
17
+ import type { ExtensionInstallBody } from "../types";
18
+ import { loadBundleFixture } from "./fixtures/extensions/load";
19
+
20
+ const TEST_DB_PATH = "./test-extensions-lifecycle.sqlite";
21
+
22
+ async function removeDbFiles(): Promise<void> {
23
+ for (const suffix of ["", "-wal", "-shm"]) {
24
+ await Bun.file(TEST_DB_PATH + suffix)
25
+ .delete()
26
+ .catch(() => {});
27
+ }
28
+ }
29
+
30
+ async function install(bundle: ExtensionInstallBody) {
31
+ return (await installExtension(bundle)).extension;
32
+ }
33
+
34
+ async function waitFor(predicate: () => Promise<boolean>, timeoutMs = 10_000): Promise<void> {
35
+ const deadline = Date.now() + timeoutMs;
36
+ while (!(await predicate())) {
37
+ if (Date.now() >= deadline) throw new Error("Timed out while waiting for extension event");
38
+ await Bun.sleep(20);
39
+ }
40
+ }
41
+
42
+ describe("extension lifecycle", () => {
43
+ beforeAll(async () => {
44
+ await removeDbFiles();
45
+ initDb(TEST_DB_PATH);
46
+ });
47
+
48
+ afterAll(async () => {
49
+ await stopExtensionRuntime();
50
+ closeDb();
51
+ await removeDbFiles();
52
+ });
53
+
54
+ beforeEach(async () => {
55
+ await stopExtensionRuntime();
56
+ const client = getDbClient();
57
+ await client.run("DELETE FROM agent_tasks");
58
+ await client.run("DELETE FROM extensions");
59
+ await client.run("DELETE FROM kv_entries");
60
+ });
61
+
62
+ test("enable creates an offline extension agent and disable unregisters", async () => {
63
+ const extension = await install(await loadBundleFixture("minimal"));
64
+ const enabled = await enableExtension(extension.id);
65
+ expect(enabled).toMatchObject({ enabled: true, status: "enabled" });
66
+ expect(listRegistered().map((loaded) => loaded.record.id)).toEqual([extension.id]);
67
+
68
+ const agent = (await getAllAgents()).find((candidate) => candidate.name === "ext:minimal");
69
+ expect(agent).toMatchObject({
70
+ id: enabled.agentId,
71
+ status: "offline",
72
+ role: "extension",
73
+ description: "System agent for extension minimal",
74
+ maxTasks: 0,
75
+ });
76
+
77
+ expect(await disableExtension(extension.id)).toMatchObject({
78
+ enabled: false,
79
+ status: "disabled",
80
+ });
81
+ expect(listRegistered()).toHaveLength(0);
82
+ });
83
+
84
+ test("activate-version swaps source and reloads an enabled extension", async () => {
85
+ const bundle = await loadBundleFixture("priority-a");
86
+ const extension = await install(bundle);
87
+ await enableExtension(extension.id);
88
+
89
+ const versionTwo: ExtensionInstallBody = {
90
+ manifest: bundle.manifest,
91
+ files: {
92
+ "hooks.ts": `
93
+ import { block, type SwarmExtension } from "swarm-extension";
94
+ const extension: SwarmExtension = (api) => {
95
+ api.on("pre.task.create", () => block("version two"));
96
+ };
97
+ export default extension;
98
+ `,
99
+ },
100
+ };
101
+ await installExtension(versionTwo);
102
+
103
+ expect(await activateVersion(extension.id, 2)).toMatchObject({
104
+ activeVersion: 2,
105
+ enabled: true,
106
+ });
107
+ expect(
108
+ await dispatchPre("pre.task.create", {
109
+ options: {},
110
+ description: "start",
111
+ origin: "rest",
112
+ }),
113
+ ).toMatchObject({ action: "block", reason: "version two" });
114
+
115
+ expect(await activateVersion(extension.id, 1)).toMatchObject({
116
+ activeVersion: 1,
117
+ enabled: true,
118
+ });
119
+ expect(
120
+ await dispatchPre("pre.task.create", {
121
+ options: {},
122
+ description: "start",
123
+ origin: "rest",
124
+ }),
125
+ ).toEqual({ action: "modify", data: { description: "starta" } });
126
+ });
127
+
128
+ test("boot loads enabled rows and continues after one load error", async () => {
129
+ const valid = await install(await loadBundleFixture("post-logger"));
130
+ const invalidBundle = await loadBundleFixture("minimal");
131
+ invalidBundle.manifest = { ...invalidBundle.manifest, name: "boot-invalid" };
132
+ invalidBundle.files["hooks.ts"] = "export const invalid = true;";
133
+ const invalid = await install(invalidBundle);
134
+ await setExtensionState(valid.id, { enabled: true, status: "enabled" });
135
+ await setExtensionState(invalid.id, { enabled: true, status: "enabled" });
136
+
137
+ await loadEnabledExtensions();
138
+
139
+ expect(listRegistered().map((loaded) => loaded.record.id)).toEqual([valid.id]);
140
+ expect(await getExtensionById(invalid.id)).toMatchObject({ enabled: true, status: "error" });
141
+ expect(await listExtensionRuns(invalid.id)).toMatchObject([{ action: "load-error" }]);
142
+
143
+ await createTaskExtended("boot-loaded extension test", { source: "api" });
144
+ await waitFor(async () =>
145
+ (await listExtensionRuns(valid.id)).some(
146
+ (run) => run.event === "post.task.created" && run.action === "continue",
147
+ ),
148
+ );
149
+ });
150
+
151
+ test("the post bridge dispatches post.task.created after commit", async () => {
152
+ const extension = await install(await loadBundleFixture("post-logger"));
153
+ const enabled = await enableExtension(extension.id);
154
+ await createTaskExtended("extension post bridge test", { source: "api" });
155
+
156
+ await waitFor(async () =>
157
+ (await listExtensionRuns(extension.id)).some(
158
+ (run) => run.event === "post.task.created" && run.action === "continue",
159
+ ),
160
+ );
161
+ expect(await getExtensionById(extension.id)).toMatchObject({
162
+ enabled: true,
163
+ consecutiveFailures: 0,
164
+ });
165
+ expect(await getKv(`task:agent:${enabled.agentId}`, "ext:post-logger:created")).toMatchObject({
166
+ value: 1,
167
+ });
168
+ });
169
+
170
+ test("five post handler failures auto-disable and re-enable resets the counter", async () => {
171
+ const extension = await install(await loadBundleFixture("throws"));
172
+ await enableExtension(extension.id);
173
+
174
+ for (let index = 0; index < 5; index += 1) {
175
+ await createTaskExtended(`throwing extension test ${index}`, { source: "api" });
176
+ await waitFor(
177
+ async () => (await getExtensionById(extension.id))!.consecutiveFailures > index,
178
+ );
179
+ }
180
+
181
+ expect(await getExtensionById(extension.id)).toMatchObject({
182
+ enabled: false,
183
+ status: "auto-disabled",
184
+ consecutiveFailures: 5,
185
+ });
186
+ expect(listRegistered()).toHaveLength(0);
187
+ expect(await enableExtension(extension.id)).toMatchObject({
188
+ enabled: true,
189
+ status: "enabled",
190
+ consecutiveFailures: 0,
191
+ });
192
+ });
193
+
194
+ test("the post bridge skips the extension that created a task", async () => {
195
+ const first = await install(await loadBundleFixture("post-logger"));
196
+ const secondBundle = await loadBundleFixture("post-logger");
197
+ secondBundle.manifest = { ...secondBundle.manifest, name: "other-logger" };
198
+ const second = await install(secondBundle);
199
+ const creator = await enableExtension(first.id);
200
+ await enableExtension(second.id);
201
+ await createTaskExtended("extension-created task", {
202
+ source: "api",
203
+ creatorAgentId: creator.agentId!,
204
+ });
205
+ await waitFor(async () => (await listExtensionRuns(second.id)).length > 0);
206
+ expect(await listExtensionRuns(first.id)).toEqual([]);
207
+ expect(await listExtensionRuns(second.id)).toMatchObject([{ action: "continue" }]);
208
+ });
209
+ });
@@ -0,0 +1,95 @@
1
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
2
+ import { closeDb, getDbClient, initDb } from "../be/db";
3
+ import { getExtensionById, installExtension, listExtensionRuns } from "../be/extensions/db";
4
+ import {
5
+ cleanExtensionTmpRoot,
6
+ EXTENSIONS_TMP_ROOT,
7
+ type LoadableExtension,
8
+ loadExtension,
9
+ } from "../extensions/loader";
10
+ import { loadBundleFixture } from "./fixtures/extensions/load";
11
+
12
+ const TEST_DB_PATH = "./test-extensions-loader.sqlite";
13
+
14
+ async function removeDbFiles(): Promise<void> {
15
+ for (const suffix of ["", "-wal", "-shm"]) {
16
+ await Bun.file(TEST_DB_PATH + suffix)
17
+ .delete()
18
+ .catch(() => {});
19
+ }
20
+ }
21
+
22
+ async function installLoadable(name: string, priority?: number): Promise<LoadableExtension> {
23
+ const bundle = await loadBundleFixture(name);
24
+ const installed = await installExtension({ ...bundle, priority });
25
+ return { record: installed.extension, ...bundle };
26
+ }
27
+
28
+ describe("extension loader", () => {
29
+ beforeAll(async () => {
30
+ await removeDbFiles();
31
+ initDb(TEST_DB_PATH);
32
+ });
33
+
34
+ afterAll(async () => {
35
+ await cleanExtensionTmpRoot();
36
+ closeDb();
37
+ await removeDbFiles();
38
+ });
39
+
40
+ beforeEach(async () => {
41
+ await cleanExtensionTmpRoot();
42
+ await getDbClient().run("DELETE FROM extensions");
43
+ });
44
+
45
+ test("imports hooks and exposes registered handlers", async () => {
46
+ const loaded = await loadExtension(await installLoadable("minimal", 17));
47
+ expect(EXTENSIONS_TMP_ROOT.endsWith(`/swarm-extensions/${process.pid}`)).toBe(true);
48
+ expect(loaded.handlers).toHaveLength(1);
49
+ expect(loaded.handlers[0]).toMatchObject({ event: "pre.task.create", priority: 17 });
50
+ expect(await Bun.file(loaded.sourcePath).exists()).toBe(true);
51
+ await loaded.dispose();
52
+ expect(await Bun.file(loaded.sourcePath).exists()).toBe(false);
53
+ });
54
+
55
+ test("defensively rejects file traversal before writing bundle files", async () => {
56
+ const input = await installLoadable("minimal");
57
+ input.files["../escape.ts"] = "export default () => {};";
58
+ await expect(loadExtension(input)).rejects.toThrow("Unsafe bundle file path");
59
+ expect((await getExtensionById(input.record.id))?.status).toBe("error");
60
+ });
61
+
62
+ test("records a load error when hooks have no default export", async () => {
63
+ const bundle = await loadBundleFixture("minimal");
64
+ bundle.manifest = { ...bundle.manifest, name: "missing-default" };
65
+ bundle.files["hooks.ts"] = "export const value = 1;";
66
+ const installed = await installExtension(bundle);
67
+
68
+ await expect(loadExtension({ record: installed.extension, ...bundle })).rejects.toThrow(
69
+ "default function",
70
+ );
71
+ expect((await getExtensionById(installed.extension.id))?.status).toBe("error");
72
+ expect(await listExtensionRuns(installed.extension.id)).toMatchObject([
73
+ { action: "load-error", event: "load" },
74
+ ]);
75
+ });
76
+
77
+ test("removes the previous content-hash directory after a successful import", async () => {
78
+ const bundle = await loadBundleFixture("minimal");
79
+ const first = await installExtension(bundle);
80
+ const firstLoaded = await loadExtension({ record: first.extension, ...bundle });
81
+ expect(await Bun.file(firstLoaded.sourcePath).exists()).toBe(true);
82
+
83
+ const files = { ...bundle.files, "hooks.ts": `${bundle.files["hooks.ts"]}\n// v2\n` };
84
+ const second = await installExtension({ ...bundle, files, activate: true });
85
+ const secondLoaded = await loadExtension({
86
+ record: second.extension,
87
+ manifest: bundle.manifest,
88
+ files,
89
+ });
90
+
91
+ expect(await Bun.file(firstLoaded.sourcePath).exists()).toBe(false);
92
+ expect(await Bun.file(secondLoaded.sourcePath).exists()).toBe(true);
93
+ await secondLoaded.dispose();
94
+ });
95
+ });
@@ -0,0 +1,353 @@
1
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
2
+ import { unlink } from "node:fs/promises";
3
+ import type { IncomingMessage, ServerResponse } from "node:http";
4
+ import { Readable } from "node:stream";
5
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
+ import { closeDb, createAgent, getDbClient, initDb } from "../be/db";
7
+ import { getExtensionById, setExtensionState } from "../be/extensions/db";
8
+ import { typecheckScript } from "../be/scripts/typecheck";
9
+ import { stopExtensionRuntime } from "../extensions/lifecycle";
10
+ import { handleCore } from "../http/core";
11
+ import { handleExtensions } from "../http/extensions";
12
+ import { getPathSegments, parseQueryParams } from "../http/utils";
13
+ import { registerExtensionActivateVersionTool } from "../tools/extension-activate-version";
14
+ import { registerExtensionDeleteTool } from "../tools/extension-delete";
15
+ import { registerExtensionDisableTool } from "../tools/extension-disable";
16
+ import { registerExtensionEnableTool } from "../tools/extension-enable";
17
+ import { registerExtensionInstallTool } from "../tools/extension-install";
18
+ import { registerExtensionListTool } from "../tools/extension-list";
19
+ import { refreshSecretScrubberCache } from "../utils/secret-scrubber";
20
+ import { loadBundleFixture } from "./fixtures/extensions/load";
21
+
22
+ const TEST_DB_PATH = "./test-extensions-mcp-tools.sqlite";
23
+ const API_KEY = "test-extensions-mcp-tools-key-1234567890";
24
+ const API_URL = "http://extensions-mcp-tools.test";
25
+
26
+ type RegisteredTool = {
27
+ handler: (args: unknown, extra: unknown) => Promise<unknown>;
28
+ };
29
+
30
+ type ToolResult = {
31
+ content: Array<{ type: string; text: string }>;
32
+ isError?: boolean;
33
+ structuredContent: Record<string, unknown>;
34
+ };
35
+
36
+ async function removeDbFiles(): Promise<void> {
37
+ for (const suffix of ["", "-wal", "-shm"]) {
38
+ try {
39
+ await unlink(TEST_DB_PATH + suffix);
40
+ } catch (error) {
41
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
42
+ }
43
+ }
44
+ }
45
+
46
+ function buildToolServer() {
47
+ const server = new McpServer({ name: "extensions-mcp-tools", version: "1" });
48
+ registerExtensionDeleteTool(server);
49
+ registerExtensionEnableTool(server);
50
+ registerExtensionDisableTool(server);
51
+ registerExtensionActivateVersionTool(server);
52
+ registerExtensionInstallTool(server);
53
+ registerExtensionListTool(server);
54
+ const tools = (server as unknown as { _registeredTools: Record<string, RegisteredTool> })
55
+ ._registeredTools;
56
+ return {
57
+ install: tools["extension-install"]!,
58
+ list: tools["extension-list"]!,
59
+ enable: tools["extension-enable"]!,
60
+ disable: tools["extension-disable"]!,
61
+ activate: tools["extension-activate-version"]!,
62
+ delete: tools["extension-delete"]!,
63
+ };
64
+ }
65
+
66
+ function meta(agentId: string) {
67
+ return { sessionId: "extensions-mcp-tools", requestInfo: { headers: { "x-agent-id": agentId } } };
68
+ }
69
+
70
+ function headersRecord(headers: HeadersInit | undefined): Record<string, string> {
71
+ if (!headers) return {};
72
+ if (headers instanceof Headers) return Object.fromEntries(headers.entries());
73
+ if (Array.isArray(headers)) return Object.fromEntries(headers);
74
+ return headers as Record<string, string>;
75
+ }
76
+
77
+ async function dispatchExtensionsApi(url: string, init: RequestInit = {}): Promise<Response> {
78
+ const parsedUrl = new URL(url);
79
+ const headers = Object.fromEntries(
80
+ Object.entries(headersRecord(init.headers)).map(([key, value]) => [
81
+ key.toLowerCase(),
82
+ String(value),
83
+ ]),
84
+ );
85
+ const body = init.body === undefined ? undefined : String(init.body);
86
+ const req = Readable.from(body ? [Buffer.from(body)] : []) as IncomingMessage;
87
+ req.method = init.method ?? "GET";
88
+ req.url = `${parsedUrl.pathname}${parsedUrl.search}`;
89
+ req.headers = headers;
90
+
91
+ let status = 200;
92
+ let text = "";
93
+ const res = {
94
+ headersSent: false,
95
+ writableEnded: false,
96
+ setHeader() {},
97
+ writeHead(code: number) {
98
+ status = code;
99
+ this.headersSent = true;
100
+ return this;
101
+ },
102
+ end(chunk?: unknown) {
103
+ if (chunk !== undefined) text += String(chunk);
104
+ this.writableEnded = true;
105
+ return this;
106
+ },
107
+ } as unknown as ServerResponse;
108
+
109
+ const agentId = headers["x-agent-id"];
110
+ if (!(await handleCore(req, res, agentId, API_KEY))) {
111
+ const pathSegments = getPathSegments(req.url || "");
112
+ const queryParams = parseQueryParams(req.url || "");
113
+ if (!(await handleExtensions(req, res, pathSegments, queryParams, agentId))) {
114
+ res.writeHead(404);
115
+ res.end("Not Found");
116
+ }
117
+ }
118
+
119
+ return new Response(text, { status, headers: { "Content-Type": "application/json" } });
120
+ }
121
+
122
+ let leadId: string;
123
+ let workerId: string;
124
+ let savedEnv: NodeJS.ProcessEnv;
125
+ let savedFetch: typeof globalThis.fetch;
126
+
127
+ beforeAll(async () => {
128
+ savedEnv = { ...process.env };
129
+ savedFetch = globalThis.fetch;
130
+ await removeDbFiles();
131
+ initDb(TEST_DB_PATH);
132
+ process.env.AGENT_SWARM_API_KEY = API_KEY;
133
+ delete process.env.API_KEY;
134
+ process.env.MCP_BASE_URL = API_URL;
135
+ refreshSecretScrubberCache();
136
+ leadId = (await createAgent({ name: "extensions-mcp-lead", isLead: true, status: "idle" })).id;
137
+ workerId = (await createAgent({ name: "extensions-mcp-worker", isLead: false, status: "idle" }))
138
+ .id;
139
+ globalThis.fetch = (async (input, init) => {
140
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
141
+ if (url.startsWith(`${API_URL}/api/extensions`)) return dispatchExtensionsApi(url, init);
142
+ return savedFetch(input, init);
143
+ }) as typeof globalThis.fetch;
144
+ });
145
+
146
+ afterAll(async () => {
147
+ await stopExtensionRuntime();
148
+ globalThis.fetch = savedFetch;
149
+ closeDb();
150
+ await removeDbFiles();
151
+ for (const key of Object.keys(process.env)) {
152
+ if (!(key in savedEnv)) delete process.env[key];
153
+ }
154
+ for (const [key, value] of Object.entries(savedEnv)) {
155
+ if (value === undefined) delete process.env[key];
156
+ else process.env[key] = value;
157
+ }
158
+ refreshSecretScrubberCache();
159
+ });
160
+
161
+ beforeEach(async () => {
162
+ await stopExtensionRuntime();
163
+ await getDbClient().run("DELETE FROM extensions");
164
+ });
165
+
166
+ describe("extension MCP HTTP proxy tools", () => {
167
+ test("script SDK typechecks the full extension lifecycle", async () => {
168
+ const result = await typecheckScript(`
169
+ import type { ScriptContext } from "swarm-sdk";
170
+ export default async (_args: unknown, ctx: ScriptContext) => {
171
+ await ctx.swarm.extension_install({
172
+ manifest: {},
173
+ files: { "hooks.ts": "export default () => {}" },
174
+ });
175
+ const id = "00000000-0000-4000-8000-000000000001";
176
+ await ctx.swarm.extension_enable({ id });
177
+ await ctx.swarm.extension_activate_version({ id, version: 1 });
178
+ await ctx.swarm.extension_disable({ id });
179
+ await ctx.swarm.extension_delete({ id });
180
+ return await ctx.swarm.extension_list({ enabledOnly: true });
181
+ };
182
+ `);
183
+ expect(result.ok).toBe(true);
184
+ });
185
+
186
+ test("lead installation stays disabled and returns the shared tool envelope", async () => {
187
+ const tools = buildToolServer();
188
+ const result = (await tools.install.handler(
189
+ await loadBundleFixture("minimal"),
190
+ meta(leadId),
191
+ )) as ToolResult;
192
+
193
+ expect(result.isError).toBe(false);
194
+ expect(result.content[0]?.text).toContain("POST /api/extensions/{id}/enable");
195
+ expect(result.structuredContent).toMatchObject({
196
+ success: true,
197
+ enabled: false,
198
+ status: "disabled",
199
+ });
200
+ expect(result.structuredContent.message).toBeTruthy();
201
+ });
202
+
203
+ test("worker installation returns the REST permission error in the shared tool envelope", async () => {
204
+ const tools = buildToolServer();
205
+ const result = (await tools.install.handler(
206
+ await loadBundleFixture("minimal"),
207
+ meta(workerId),
208
+ )) as ToolResult;
209
+
210
+ expect(result.isError).toBe(true);
211
+ expect(result.structuredContent).toMatchObject({ success: false });
212
+ expect(String(result.structuredContent.message)).toContain("Forbidden");
213
+ });
214
+
215
+ test("lead reinstall stores an inactive version of an enabled extension", async () => {
216
+ const tools = buildToolServer();
217
+ const first = (await tools.install.handler(
218
+ await loadBundleFixture("minimal"),
219
+ meta(leadId),
220
+ )) as ToolResult;
221
+ const id = String(first.structuredContent.id);
222
+ await setExtensionState(id, { enabled: true, status: "enabled" });
223
+
224
+ const changed = await loadBundleFixture("minimal");
225
+ changed.files["hooks.ts"] += "\n// updated by MCP\n";
226
+ const result = (await tools.install.handler(changed, meta(leadId))) as ToolResult;
227
+
228
+ expect(result.isError).toBe(false);
229
+ expect(result.structuredContent).toMatchObject({
230
+ version: 2,
231
+ activeVersion: 1,
232
+ enabled: true,
233
+ });
234
+ });
235
+
236
+ test("typecheck diagnostics reach the error details", async () => {
237
+ const tools = buildToolServer();
238
+ const broken = await loadBundleFixture("minimal");
239
+ broken.files["hooks.ts"] = "export default 42;";
240
+ const result = (await tools.install.handler(broken, meta(leadId))) as ToolResult;
241
+
242
+ expect(result.isError).toBe(true);
243
+ expect(result.structuredContent).toMatchObject({
244
+ success: false,
245
+ message: "Extension rejected by typecheck.",
246
+ });
247
+ expect(String(result.structuredContent.details)).toContain("TS");
248
+ });
249
+
250
+ test("extension-list renders a table and supports enabledOnly", async () => {
251
+ const tools = buildToolServer();
252
+ await tools.install.handler(await loadBundleFixture("minimal"), meta(leadId));
253
+
254
+ const all = (await tools.list.handler({}, meta(leadId))) as ToolResult;
255
+ expect(all.isError).toBe(false);
256
+ expect(all.content[0]?.text).toContain(
257
+ "| Name | Version | Active | Enabled | Status | Priority | Failures |",
258
+ );
259
+ expect(all.structuredContent).toMatchObject({ success: true });
260
+ expect((all.structuredContent.extensions as unknown[]).length).toBe(1);
261
+
262
+ const enabledOnly = (await tools.list.handler(
263
+ { enabledOnly: true },
264
+ meta(leadId),
265
+ )) as ToolResult;
266
+ expect(enabledOnly.structuredContent).toMatchObject({ success: true, extensions: [] });
267
+ });
268
+ });
269
+
270
+ describe("extension lifecycle MCP tools", () => {
271
+ test("lead enables, activates, disables, and deletes through the shared HTTP routes", async () => {
272
+ const tools = buildToolServer();
273
+ const installed = (await tools.install.handler(
274
+ await loadBundleFixture("minimal"),
275
+ meta(leadId),
276
+ )) as ToolResult;
277
+ const id = String(installed.structuredContent.id);
278
+ const enabled = (await tools.enable.handler({ id }, meta(leadId))) as ToolResult;
279
+ expect(enabled.structuredContent).toMatchObject({
280
+ success: true,
281
+ enabled: true,
282
+ activeVersion: 1,
283
+ });
284
+
285
+ const rejected = await dispatchExtensionsApi(`${API_URL}/api/extensions/${id}`, {
286
+ method: "DELETE",
287
+ headers: { "x-agent-id": leadId, authorization: `Bearer ${API_KEY}` },
288
+ });
289
+ expect(rejected.status).toBe(409);
290
+ const blocked = (await tools.delete.handler({ id }, meta(leadId))) as ToolResult;
291
+ expect(blocked.isError).toBe(true);
292
+ expect(blocked.structuredContent).toMatchObject({ success: false });
293
+ expect(blocked.content[0]?.text).toContain("Disable");
294
+ expect(await getExtensionById(id)).not.toBeNull();
295
+
296
+ const changed = await loadBundleFixture("minimal");
297
+ changed.files["hooks.ts"] += "\n// next lifecycle version\n";
298
+ await tools.install.handler(changed, meta(leadId));
299
+ const activated = (await tools.activate.handler(
300
+ { id, version: 2 },
301
+ meta(leadId),
302
+ )) as ToolResult;
303
+ expect(activated.structuredContent).toMatchObject({
304
+ success: true,
305
+ activeVersion: 2,
306
+ enabled: true,
307
+ });
308
+ const disabled = (await tools.disable.handler({ id }, meta(leadId))) as ToolResult;
309
+ expect(disabled.structuredContent).toMatchObject({ success: true, enabled: false });
310
+ const deleted = (await tools.delete.handler({ id }, meta(leadId))) as ToolResult;
311
+ expect(deleted.isError).toBe(false);
312
+ expect(deleted.structuredContent).toMatchObject({ success: true, id, deleted: true });
313
+ expect(await getExtensionById(id)).toBeNull();
314
+ });
315
+
316
+ test("workers cannot mutate the extension lifecycle", async () => {
317
+ const tools = buildToolServer();
318
+ const installed = (await tools.install.handler(
319
+ await loadBundleFixture("minimal"),
320
+ meta(leadId),
321
+ )) as ToolResult;
322
+ const id = String(installed.structuredContent.id);
323
+ for (const tool of [tools.enable, tools.disable, tools.activate, tools.delete]) {
324
+ const result = (await tool.handler({ id, version: 1 }, meta(workerId))) as ToolResult;
325
+ expect(result.isError).toBe(true);
326
+ expect(String(result.structuredContent.message)).toContain("Forbidden");
327
+ }
328
+ expect(await getExtensionById(id)).toMatchObject({ enabled: false, activeVersion: 1 });
329
+ });
330
+
331
+ test("missing extensions and versions return error envelopes", async () => {
332
+ const tools = buildToolServer();
333
+ const id = crypto.randomUUID();
334
+ for (const tool of [tools.enable, tools.disable, tools.activate, tools.delete]) {
335
+ const result = (await tool.handler({ id, version: 1 }, meta(leadId))) as ToolResult;
336
+ expect(result.isError).toBe(true);
337
+ expect(result.structuredContent.success).toBe(false);
338
+ }
339
+ const installed = (await tools.install.handler(
340
+ await loadBundleFixture("minimal"),
341
+ meta(leadId),
342
+ )) as ToolResult;
343
+ const result = (await tools.activate.handler(
344
+ { id: installed.structuredContent.id, version: 99 },
345
+ meta(leadId),
346
+ )) as ToolResult;
347
+ expect(result.isError).toBe(true);
348
+ expect(await getExtensionById(String(installed.structuredContent.id))).toMatchObject({
349
+ activeVersion: 1,
350
+ enabled: false,
351
+ });
352
+ });
353
+ });