@desplega.ai/agent-swarm 1.173.0 → 2.0.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 (432) hide show
  1. package/README.md +1 -1
  2. package/dist/{acp-adapter-ccm9xrep.js → acp-adapter-sj2g0m84.js} +17 -16
  3. package/dist/{actions-sgeyv08k.js → actions-fegk44gr.js} +39 -34
  4. package/dist/{amp-adapter-gpxp3kc9.js → amp-adapter-ad8ft89x.js} +8 -7
  5. package/dist/{app-km9qnv2f.js → app-kfvgbvzy.js} +18 -14
  6. package/dist/{assistant-5r8tcvvd.js → assistant-z7zdy9ja.js} +40 -35
  7. package/dist/{boot-reembed-swwzdgd7.js → boot-reembed-5c2z0mbe.js} +10 -9
  8. package/dist/{boot-reembed-16rv9843.js → boot-reembed-j8xa181e.js} +10 -9
  9. package/dist/{boot-scrub-sweep-zkr6vkqb.js → boot-scrub-sweep-hnkn3rgt.js} +11 -10
  10. package/dist/{boot-seeds-d8sp49t2.js → boot-seeds-kwjq4d2z.js} +14 -13
  11. package/dist/{claude-adapter-yzzzh8jp.js → claude-adapter-9vmvpvgd.js} +19 -18
  12. package/dist/{claude-managed-adapter-xgxhqrt2.js → claude-managed-adapter-k9qae05j.js} +7 -5
  13. package/dist/{claude-managed-setup-bnmjarjc.js → claude-managed-setup-q54zx5q2.js} +6 -4
  14. package/dist/{claude-sdk-session-4whxere4.js → claude-sdk-session-9jye6cxr.js} +11881 -11472
  15. package/dist/{cli-94b48jhk.js → cli-03gedhng.js} +16 -14
  16. package/dist/{cli-1w0vj3gw.js → cli-1e4b6bzh.js} +7 -7
  17. package/dist/{cli-d603qf86.js → cli-29jc5q7w.js} +10 -8
  18. package/dist/cli-2m1ktfzz.js +17795 -0
  19. package/dist/{cli-x119kdba.js → cli-3dj8k4vy.js} +1 -1
  20. package/dist/{cli-srqq3gc5.js → cli-3fbmmawm.js} +5 -5
  21. package/dist/{cli-gmdajfw5.js → cli-4jxm2qtz.js} +4987 -1708
  22. package/dist/{cli-tadfgd0t.js → cli-5hkm7whv.js} +33 -4
  23. package/dist/{cli-42qhyvwx.js → cli-5t4f29ez.js} +3 -3
  24. package/dist/{cli-7a0sde2x.js → cli-6cagzc96.js} +9 -0
  25. package/dist/cli-6cvfxs8r.js +47 -0
  26. package/dist/{cli-0kxf81jj.js → cli-6jyj59nb.js} +2 -2
  27. package/dist/{cli-w1apjecb.js → cli-6razj6t4.js} +2 -2
  28. package/dist/{cli-anzdk0z4.js → cli-74ahz61x.js} +2 -2
  29. package/dist/{cli-m89t9jdf.js → cli-7x0g4sn1.js} +15416 -16024
  30. package/dist/{cli-yatxqynm.js → cli-9ha6hs2d.js} +1 -290
  31. package/dist/{cli-81r1wjyv.js → cli-9nfx44eh.js} +1 -1
  32. package/dist/cli-9traj3dn.js +638 -0
  33. package/dist/{cli-smcrrmhy.js → cli-agzebbd5.js} +9 -9
  34. package/dist/{cli-na2pxe4f.js → cli-apx87jne.js} +2 -2
  35. package/dist/{cli-rfz2ve55.js → cli-bzszjf2p.js} +2855 -862
  36. package/dist/{cli-c5km5znr.js → cli-c8r3kzzb.js} +1 -1
  37. package/dist/{cli-hsem16hb.js → cli-cgtksawx.js} +7 -4
  38. package/dist/{cli-db3m6zdt.js → cli-d2qr7aj4.js} +1 -1
  39. package/dist/{cli-rfs4vddv.js → cli-d5f9ed5c.js} +5 -5
  40. package/dist/cli-e3g5mbkz.js +776 -0
  41. package/dist/{cli-7jykv7ef.js → cli-e6mpjhed.js} +1 -1
  42. package/dist/{cli-7a93cv8c.js → cli-ee9gtz70.js} +98 -23
  43. package/dist/{cli-3nxdmjrc.js → cli-eejsgvm4.js} +4 -4
  44. package/dist/{cli-fmhj1ehj.js → cli-ef1hg1wv.js} +470 -51
  45. package/dist/cli-f527ce74.js +781 -0
  46. package/dist/{cli-ppy4nnnh.js → cli-f7kr7svz.js} +2 -2
  47. package/dist/cli-hjwmhcg1.js +10622 -0
  48. package/dist/{cli-2bw3w47j.js → cli-hnhtk6cj.js} +1 -1
  49. package/dist/{cli-ywm1azg9.js → cli-hnmhtzvh.js} +6 -4
  50. package/dist/{cli-h7k0xtry.js → cli-hvqbtna4.js} +4 -1256
  51. package/dist/{cli-s8z6x4b4.js → cli-j0pfptdp.js} +218 -102
  52. package/dist/cli-jta2eyzc.js +619 -0
  53. package/dist/{cli-5er5gjbm.js → cli-k382tshb.js} +10 -11
  54. package/dist/{cli-v3zhnxsb.js → cli-k3dp5tpf.js} +2 -2
  55. package/dist/{cli-wd0hz6ah.js → cli-kbrt8gqa.js} +9 -91
  56. package/dist/cli-kceah1eh.js +92 -0
  57. package/dist/{cli-ne7smj1b.js → cli-ke896fcf.js} +10 -10
  58. package/dist/{cli-xc7p7s3g.js → cli-kt6zqg0e.js} +6 -6
  59. package/dist/{cli-wrh08zjd.js → cli-m50t4309.js} +5 -18
  60. package/dist/cli-n2wq4j7m.js +6985 -0
  61. package/dist/{cli-bv2etw2y.js → cli-n3hzg89b.js} +46 -15
  62. package/dist/{cli-tmhsxhb8.js → cli-n5vwg842.js} +23 -6
  63. package/dist/{cli-qm1448f9.js → cli-ne236afy.js} +8 -8
  64. package/dist/{cli-d161g69q.js → cli-p1x85d2t.js} +50 -2
  65. package/dist/{cli-7pz3b7x5.js → cli-przpmdgr.js} +25 -0
  66. package/dist/{cli-0a7mm3zp.js → cli-q0ke1yww.js} +25 -21
  67. package/dist/{cli-e3pmyt39.js → cli-q50942rs.js} +1 -1
  68. package/dist/{cli-r44t9wqc.js → cli-r106725k.js} +2 -2
  69. package/dist/{cli-phcqp1b9.js → cli-r2pnvj8g.js} +2 -2
  70. package/dist/{cli-jpc4r224.js → cli-r62a35ea.js} +19 -3
  71. package/dist/{cli-cd0hv9nh.js → cli-ra0xzggj.js} +2 -2
  72. package/dist/cli-rhs9pz33.js +294 -0
  73. package/dist/{cli-r4fqfpwf.js → cli-s2sp957d.js} +50 -27
  74. package/dist/{cli-1x7da0de.js → cli-s4setq91.js} +8 -8
  75. package/dist/{cli-6thk4jz3.js → cli-se8w6wny.js} +6 -6
  76. package/dist/cli-skxvvpap.js +10140 -0
  77. package/dist/{cli-014vns2y.js → cli-stfs0k5s.js} +208 -525
  78. package/dist/{cli-0p3enk35.js → cli-tqaazemc.js} +9 -1
  79. package/dist/{cli-nwrdx840.js → cli-tr4sxp68.js} +3 -3
  80. package/dist/{cli-r0byd61h.js → cli-tsde1hdr.js} +15 -15
  81. package/dist/{cli-b07hr3bt.js → cli-txqb9s38.js} +2 -2
  82. package/dist/{cli-6r2f7edk.js → cli-v1yb0rqb.js} +6 -6
  83. package/dist/{cli-saqyage0.js → cli-vatyhshy.js} +179 -27
  84. package/dist/{cli-5wpw4rab.js → cli-vdach5a9.js} +5 -5
  85. package/dist/{cli-7avy4v8w.js → cli-w1q7h2r2.js} +5 -3
  86. package/dist/{cli-rcp5aqdw.js → cli-w7m81tb7.js} +1 -1
  87. package/dist/{cli-c3rk1v0x.js → cli-wayw93nt.js} +3 -3
  88. package/dist/cli-wmgrkscj.js +23254 -0
  89. package/dist/{cli-9xf7fdtp.js → cli-z5jjpaxw.js} +29 -6
  90. package/dist/{cli-451ks6aw.js → cli-zkpks3r3.js} +1 -1
  91. package/dist/cli.js +29 -45
  92. package/dist/{codex-adapter-r3dn4fp5.js → codex-adapter-y81sssk0.js} +15 -14
  93. package/dist/{codex-login-841kv5fk.js → codex-login-f2rcfpp9.js} +3 -2
  94. package/dist/{codex-session-runner-fwt793h4.js → codex-session-runner-8vx4j6a9.js} +15 -14
  95. package/dist/{commands-z8fq9qna.js → commands-kcpvpcrr.js} +11 -10
  96. package/dist/{controller-hq5ggpcp.js → controller-ywrmt6yd.js} +5 -4
  97. package/dist/{controllers-connect-3mf2btyj.js → controllers-connect-kwpvj3rf.js} +42 -37
  98. package/dist/{cursor-adapter-q8pgv5qa.js → cursor-adapter-cehv1qns.js} +7 -6
  99. package/dist/{db-yyhqqrbf.js → db-k8ankj20.js} +12 -11
  100. package/dist/{db-qfpq4bt7.js → db-ww5d7jq3.js} +28 -9
  101. package/dist/{devin-adapter-xgtj680j.js → devin-adapter-x1022qb1.js} +3 -2
  102. package/dist/{dispatcher-x0x7qfcq.js → dispatcher-e8gq5m68.js} +38 -33
  103. package/dist/{docker-f503k9p0.js → docker-ff6y1190.js} +3 -2
  104. package/dist/{dsh-adapter-34etvb5m.js → dsh-adapter-3epjm3af.js} +7 -6
  105. package/dist/{e2b-66xra4az.js → e2b-8ygwhg0f.js} +4 -3
  106. package/dist/{e2b-xwhyj8cw.js → e2b-swhnpw3n.js} +4 -3
  107. package/dist/{event-bus-vjtv82gm.js → event-bus-6axnngdj.js} +3 -2
  108. package/dist/{external-channel-h1fjecwe.js → external-channel-h3359cwn.js} +11 -10
  109. package/dist/{google-generative-ai-szdxbpqg.js → google-generative-ai-g8db7x0f.js} +4 -2
  110. package/dist/{google-vertex-qzw3p36d.js → google-vertex-r2692q0r.js} +4 -2
  111. package/dist/{grok-adapter-e7mr287e.js → grok-adapter-0d4x7gkx.js} +18 -17
  112. package/dist/{handlers-znb9n7d4.js → handlers-an1gzv21.js} +41 -36
  113. package/dist/{heartbeat-bm4qkn8d.js → heartbeat-6dakeb61.js} +43 -38
  114. package/dist/{hook-254r4mza.js → hook-6v6cv6d5.js} +11 -23
  115. package/dist/{hook-xaa3q80s.js → hook-sa5xye4y.js} +12 -11
  116. package/dist/{http-3dgrgp8y.js → http-r7f3eky0.js} +1499 -251
  117. package/dist/index-52j1aja4.js +3465 -0
  118. package/dist/{index-fx3jqq1e.js → index-7ep4gzdk.js} +38 -33
  119. package/dist/index-9gw58n4k.js +193 -0
  120. package/dist/index-9j87mcm8.js +56305 -0
  121. package/dist/index-bsmcresk.js +79424 -0
  122. package/dist/index-mh85qhec.js +475 -0
  123. package/dist/{index-g2byveez.js → index-rmpw9a8q.js} +38 -33
  124. package/dist/{index-2ybk7khy.js → index-s2wxgzr0.js} +468 -683
  125. package/dist/{index-bdsqa799.js → index-v1v6n7hg.js} +43 -38
  126. package/dist/index-we27kd31.js +12810 -0
  127. package/dist/{index-jd7sejm2.js → index-zk96aq1k.js} +10 -9
  128. package/dist/{keepalive-6ywhpja1.js → keepalive-be9vwy5h.js} +12 -11
  129. package/dist/{lead-cw45n21s.js → lead-0861cwrf.js} +61 -56
  130. package/dist/{maintenance-ss1a1ge9.js → maintenance-mzzgc8bn.js} +10 -9
  131. package/dist/{node-fkajaq96.js → node-mn1zs7an.js} +2 -1
  132. package/dist/{oauth-refresh-sweep-3c6f8vyy.js → oauth-refresh-sweep-cpdnfwdj.js} +11 -10
  133. package/dist/{onboard-8hzdhc1k.js → onboard-fh8qs4r3.js} +11 -3
  134. package/dist/{opencode-adapter-qbchbcfz.js → opencode-adapter-nm42wwj8.js} +18 -13
  135. package/dist/{otel-impl-e6mnvc4v.js → otel-impl-x6c1c9g4.js} +242 -868
  136. package/dist/{pi-mono-adapter-3c9g9snh.js → pi-mono-adapter-9n2ya240.js} +244 -106
  137. package/dist/{pricing-refresh-gedkjdw0.js → pricing-refresh-qb0rv0ap.js} +12 -11
  138. package/dist/{rbac-roles-dt4vh1yy.js → rbac-roles-1gzdwbh1.js} +11 -10
  139. package/dist/{rbac-roles-eszdg6pm.js → rbac-roles-3mraey52.js} +12 -11
  140. package/dist/{render-v2-907cy6sa.js → render-v2-agpa45zz.js} +18 -14
  141. package/dist/{resume-42stcaz0.js → resume-x0ydy9s9.js} +25 -21
  142. package/dist/{runtime-qfry88v2.js → runtime-sa2vr16g.js} +2 -2
  143. package/dist/{setup-adbc1c10.js → setup-as9ga0d6.js} +2 -2
  144. package/dist/{virtual-modules-rtmt494q.js → virtual-modules-9gkxqrap.js} +6 -5
  145. package/dist/{work-objects-wpj8ck95.js → work-objects-y5g1y7ev.js} +41 -36
  146. package/dist/{worker-bzdra9rf.js → worker-860c3e38.js} +61 -56
  147. package/dist/{x-qfmygqqv.js → x-m477fr0h.js} +3 -2
  148. package/openapi.json +523 -344
  149. package/package.json +10 -11
  150. package/src/be/config-refs.ts +60 -0
  151. package/src/be/db/page-versions.ts +15 -0
  152. package/src/be/db/runtime.ts +35 -8
  153. package/src/be/db/skill-name-resolution.ts +36 -0
  154. package/src/be/db/tasks/read.ts +230 -0
  155. package/src/be/db/tasks/write.ts +7 -0
  156. package/src/be/db.ts +52 -7
  157. package/src/be/migrations/215_swarm_config_export_to_env.sql +34 -0
  158. package/src/be/migrations/216_task_resolved_effort.sql +5 -0
  159. package/src/be/migrations/217_chat_sdk_state.sql +104 -0
  160. package/src/be/model-catalog-store.ts +5 -1
  161. package/src/be/model-tier-resolution.ts +12 -1
  162. package/src/be/model-validation.ts +5 -2
  163. package/src/be/models-catalog.ts +26 -1
  164. package/src/be/modelsdev-cache.json +198 -63
  165. package/src/be/modelsdev-cache.ts +9 -1
  166. package/src/be/script-credential-broker.ts +39 -2
  167. package/src/be/scripts/run-saved.ts +1 -0
  168. package/src/be/scripts/typecheck.ts +10 -8
  169. package/src/be/seed/runner.ts +13 -1
  170. package/src/be/seed/state-db.ts +5 -0
  171. package/src/be/seed/types.ts +6 -0
  172. package/src/be/seed-pricing.ts +19 -2
  173. package/src/be/seed-scripts/catalog/ops-catalog-audit.inline.ts +1 -1
  174. package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +1 -1
  175. package/src/be/seed-skills/bundled-files.generated.json +0 -18
  176. package/src/be/seed-skills/index.ts +104 -3
  177. package/src/be/swarm-config-guard.ts +52 -0
  178. package/src/cli.tsx +0 -16
  179. package/src/commands/onboard/non-interactive.ts +4 -2
  180. package/src/commands/provider-credentials.ts +52 -6
  181. package/src/commands/runner.ts +190 -54
  182. package/src/e2b/env.ts +5 -0
  183. package/src/extensions/contract-types.generated.ts +1 -1
  184. package/src/extensions/contract.ts +45 -0
  185. package/src/extensions/dispatcher.ts +7 -0
  186. package/src/extensions/post-bridge.ts +40 -0
  187. package/src/github/handlers.ts +11 -4
  188. package/src/heartbeat/templates.ts +8 -5
  189. package/src/hooks/hook.ts +0 -26
  190. package/src/http/all-routes.ts +0 -1
  191. package/src/http/config.ts +36 -17
  192. package/src/http/core.ts +4 -0
  193. package/src/http/index.ts +14 -2
  194. package/src/http/integrations.ts +88 -1
  195. package/src/http/mcp.ts +15 -5
  196. package/src/http/page-proxy.ts +2 -4
  197. package/src/http/pages-public.ts +3 -3
  198. package/src/http/pages.ts +3 -13
  199. package/src/http/poll.ts +28 -68
  200. package/src/http/requested-by.ts +54 -0
  201. package/src/http/scripts.ts +1 -0
  202. package/src/http/stats.ts +1 -30
  203. package/src/http/tasks.ts +61 -5
  204. package/src/http/utils.ts +14 -0
  205. package/src/http/webhooks.ts +159 -1
  206. package/src/http/x.ts +1 -0
  207. package/src/integrations/chat-sdk/adapters.ts +129 -0
  208. package/src/integrations/chat-sdk/attachments.ts +232 -0
  209. package/src/integrations/chat-sdk/background.ts +36 -0
  210. package/src/integrations/chat-sdk/commands.ts +194 -0
  211. package/src/integrations/chat-sdk/config.ts +198 -0
  212. package/src/integrations/chat-sdk/discord-api.ts +63 -0
  213. package/src/integrations/chat-sdk/discord-gateway.ts +185 -0
  214. package/src/integrations/chat-sdk/handlers.ts +28 -0
  215. package/src/integrations/chat-sdk/inbound.ts +426 -0
  216. package/src/integrations/chat-sdk/limits.ts +48 -0
  217. package/src/integrations/chat-sdk/logger.ts +81 -0
  218. package/src/integrations/chat-sdk/outbound.ts +123 -0
  219. package/src/integrations/chat-sdk/relay.ts +172 -0
  220. package/src/integrations/chat-sdk/router.ts +169 -0
  221. package/src/integrations/chat-sdk/runtime.ts +351 -0
  222. package/src/integrations/chat-sdk/schema.ts +245 -0
  223. package/src/integrations/chat-sdk/state.ts +243 -0
  224. package/src/integrations/chat-sdk/status.ts +244 -0
  225. package/src/integrations/chat-sdk/steering.ts +58 -0
  226. package/src/integrations/chat-sdk/teams-origin.ts +77 -0
  227. package/src/integrations/chat-sdk/telegram-api.ts +56 -0
  228. package/src/integrations/chat-sdk/templates.ts +16 -0
  229. package/src/integrations/chat-sdk/test-hooks.ts +37 -0
  230. package/src/integrations/chat-sdk/thread-buffer.ts +100 -0
  231. package/src/integrations/chat-sdk/typing.ts +65 -0
  232. package/src/pages/version.ts +4 -5
  233. package/src/prompts/base-prompt.ts +111 -45
  234. package/src/providers/acp-target-catalog.ts +6 -0
  235. package/src/providers/acp-targets.ts +55 -2
  236. package/src/providers/amp-adapter.ts +1 -1
  237. package/src/providers/claude-adapter.ts +1 -1
  238. package/src/providers/claude-flag-settings.ts +17 -5
  239. package/src/providers/claude-managed-adapter.ts +1 -1
  240. package/src/providers/claude-session-events.ts +6 -1
  241. package/src/providers/dsh-adapter.ts +1 -1
  242. package/src/providers/opencode-adapter.ts +243 -26
  243. package/src/providers/opencode-swarm-plugin.ts +31 -0
  244. package/src/providers/pi-mono-adapter.ts +398 -153
  245. package/src/providers/pricing-sources.md +33 -2
  246. package/src/providers/reasoning-effort.ts +27 -0
  247. package/src/providers/types.ts +6 -2
  248. package/src/rbac/legacy-policy.ts +8 -2
  249. package/src/rbac/permissions.ts +21 -0
  250. package/src/scheduler/scheduler.ts +4 -2
  251. package/src/scripts-runtime/loader.ts +19 -7
  252. package/src/scripts-runtime/sdk-allowlist.ts +8 -6
  253. package/src/scripts-runtime/swarm-sdk.ts +0 -11
  254. package/src/scripts-runtime/types/stdlib.d.ts +27 -15
  255. package/src/scripts-runtime/types/swarm-extension.d.ts +63 -1
  256. package/src/scripts-runtime/types/swarm-sdk.d.ts +27 -15
  257. package/src/server.ts +52 -18
  258. package/src/slack/event-dedup.ts +1 -1
  259. package/src/tasks/additive-buffer.ts +4 -2
  260. package/src/tasks/context-key.ts +60 -1
  261. package/src/tasks/model-pin.ts +67 -0
  262. package/src/tasks/worker-follow-up.ts +4 -31
  263. package/src/tests/acp-adapter.test.ts +42 -0
  264. package/src/tests/api-drain-worker-e2e.test.ts +1 -6
  265. package/src/tests/base-prompt.test.ts +58 -54
  266. package/src/tests/chat-sdk-commands.test.ts +191 -0
  267. package/src/tests/chat-sdk-config.test.ts +334 -0
  268. package/src/tests/chat-sdk-discord-api.test.ts +68 -0
  269. package/src/tests/chat-sdk-discord-gateway.test.ts +252 -0
  270. package/src/tests/chat-sdk-discord-webhook.test.ts +126 -0
  271. package/src/tests/chat-sdk-fields-sync.test.ts +50 -0
  272. package/src/tests/chat-sdk-harness.ts +146 -0
  273. package/src/tests/chat-sdk-inbound.test.ts +363 -0
  274. package/src/tests/chat-sdk-relay.test.ts +470 -0
  275. package/src/tests/chat-sdk-reload.test.ts +128 -0
  276. package/src/tests/chat-sdk-shutdown.test.ts +220 -0
  277. package/src/tests/chat-sdk-slack.test.ts +268 -0
  278. package/src/tests/chat-sdk-state.test.ts +163 -0
  279. package/src/tests/chat-sdk-status-route.test.ts +382 -0
  280. package/src/tests/chat-sdk-teams.test.ts +277 -0
  281. package/src/tests/chat-sdk-tools.test.ts +520 -0
  282. package/src/tests/chat-sdk-typing.test.ts +120 -0
  283. package/src/tests/chat-sdk-webhook-telegram.test.ts +200 -0
  284. package/src/tests/claude-adapter-binary.test.ts +9 -4
  285. package/src/tests/claude-flag-settings.test.ts +11 -3
  286. package/src/tests/claude-managed-adapter.test.ts +1 -1
  287. package/src/tests/claude-sdk-lifecycle.test.ts +9 -3
  288. package/src/tests/claude-thinking-tokens.test.ts +57 -0
  289. package/src/tests/claude-worker-parity.test.ts +2 -7
  290. package/src/tests/config-env-export-chat-sdk.test.ts +11 -0
  291. package/src/tests/config-refs.test.ts +66 -0
  292. package/src/tests/context-key.test.ts +19 -0
  293. package/src/tests/cors-allowlist.test.ts +25 -0
  294. package/src/tests/create-page-tool.test.ts +18 -0
  295. package/src/tests/credential-check.test.ts +139 -0
  296. package/src/tests/credentials.test.ts +21 -0
  297. package/src/tests/db-runtime.test.ts +61 -0
  298. package/src/tests/db-task-read-contract.test.ts +20 -0
  299. package/src/tests/db-task-write-contract.test.ts +10 -0
  300. package/src/tests/docs-index-footprint.test.js +3 -3
  301. package/src/tests/dsh-adapter.test.ts +1 -1
  302. package/src/tests/enabled-capabilities.test.ts +20 -0
  303. package/src/tests/entrypoint-api-readiness.test.ts +14 -8
  304. package/src/tests/entrypoint-config-env-export.test.ts +5 -2
  305. package/src/tests/entrypoint-export-to-env.test.ts +78 -0
  306. package/src/tests/explicit-model-validation.test.ts +105 -1
  307. package/src/tests/extensions-example-route-chat-channel.test.ts +154 -0
  308. package/src/tests/extensions-post-bridge.test.ts +51 -0
  309. package/src/tests/fetch-resolved-env.test.ts +128 -0
  310. package/src/tests/fixtures/chat-sdk/telegram-private-text.json +10 -0
  311. package/src/tests/fixtures/extensions/route-chat-channel-to-agent/hooks.ts +27 -0
  312. package/src/tests/fixtures/extensions/route-chat-channel-to-agent/manifest.json +9 -0
  313. package/src/tests/fixtures/openai-compatible-registry-extension.child.ts +146 -0
  314. package/src/tests/fixtures/runner-legacy-resume-prompt.ts +122 -0
  315. package/src/tests/github-handlers.test.ts +35 -0
  316. package/src/tests/harness-provider-resolution.test.ts +12 -0
  317. package/src/tests/heartbeat-checklist.test.ts +3 -2
  318. package/src/tests/hook-runtime-close.test.ts +0 -2
  319. package/src/tests/http-api-integration.test.ts +0 -25
  320. package/src/tests/migration-046-budgets.test.ts +17 -16
  321. package/src/tests/migration-217-chat-sdk-state.test.ts +113 -0
  322. package/src/tests/model-tier-resolution.test.ts +93 -0
  323. package/src/tests/models-catalog.test.ts +42 -1
  324. package/src/tests/openai-compatible-providers.test.ts +134 -0
  325. package/src/tests/openai-compatible-registry-extension.test.ts +42 -0
  326. package/src/tests/opencode-adapter.test.ts +216 -2
  327. package/src/tests/opencode-helmcode-sdk.test.ts +201 -0
  328. package/src/tests/opencode-shell-env.test.ts +145 -0
  329. package/src/tests/openrouter-base-url.test.ts +101 -151
  330. package/src/tests/page-proxy.test.ts +1 -1
  331. package/src/tests/pages-browser-sdk.test.ts +76 -0
  332. package/src/tests/pages-feedback.test.ts +1 -1
  333. package/src/tests/paused-tasks-requested-by.test.ts +102 -0
  334. package/src/tests/pi-custom-provider-fake-server.test.ts +442 -0
  335. package/src/tests/pi-mono-adapter.test.ts +385 -5
  336. package/src/tests/pi-shell-env.test.ts +170 -0
  337. package/src/tests/prompt-template-remaining.test.ts +35 -0
  338. package/src/tests/rbac-engine.test.ts +11 -3
  339. package/src/tests/runner-exit-after-tasks.test.ts +0 -4
  340. package/src/tests/runner-legacy-resume-prompt.test.ts +64 -0
  341. package/src/tests/script-connections.test.ts +2 -0
  342. package/src/tests/scripts-http.test.ts +201 -1
  343. package/src/tests/scripts-only-gating.test.ts +2 -3
  344. package/src/tests/scripts-runtime.test.ts +58 -40
  345. package/src/tests/secret-scrubber.test.ts +86 -0
  346. package/src/tests/seed-skills-bundled-files.test.ts +45 -53
  347. package/src/tests/seed-skills-retire.test.ts +182 -0
  348. package/src/tests/send-task-requester-gate.test.ts +58 -0
  349. package/src/tests/session-cost-recompute-unit.test.ts +46 -0
  350. package/src/tests/skill-get-name-resolution.test.ts +137 -0
  351. package/src/tests/sonnet-5-5-registry.test.ts +9 -1
  352. package/src/tests/swarm-config-export-to-env.test.ts +267 -0
  353. package/src/tests/swarm-config-reserved-keys.test.ts +16 -0
  354. package/src/tests/swarm-diff.test.ts +1 -1
  355. package/src/tests/task-model-effort-rerun.test.ts +164 -0
  356. package/src/tests/task-session-prompts.test.ts +132 -0
  357. package/src/tests/task-tool-manifest.test.ts +14 -0
  358. package/src/tests/task-tools-ctx.test.ts +2 -0
  359. package/src/tests/tool-annotations.test.ts +6 -6
  360. package/src/tests/ui-logs-parser.test.ts +43 -0
  361. package/src/tests/used-by-route.test.ts +6 -0
  362. package/src/tests/vercel-ai-gateway.test.ts +149 -0
  363. package/src/tests/workflow-swarm-script.test.ts +38 -0
  364. package/src/tests/workflow-wait-filter.test.ts +5 -3
  365. package/src/tools/chat-access.ts +41 -0
  366. package/src/tools/chat-open-dm.ts +59 -0
  367. package/src/tools/chat-post.ts +74 -0
  368. package/src/tools/chat-read-thread.ts +100 -0
  369. package/src/tools/chat-reply.ts +75 -0
  370. package/src/tools/create-page.ts +3 -13
  371. package/src/tools/model-catalog/index.ts +2 -1
  372. package/src/tools/register-chat-channel.ts +139 -0
  373. package/src/tools/send-task.ts +24 -0
  374. package/src/tools/skills/skill-get.ts +17 -9
  375. package/src/tools/swarm-config/get-config.ts +2 -1
  376. package/src/tools/swarm-config/list-config.ts +2 -1
  377. package/src/tools/swarm-config/set-config.ts +16 -3
  378. package/src/tools/tool-config.ts +8 -6
  379. package/src/types.ts +12 -2
  380. package/src/utils/config-env-export.ts +74 -0
  381. package/src/utils/credentials.test.ts +92 -0
  382. package/src/utils/credentials.ts +52 -4
  383. package/src/utils/harness-provider.ts +15 -6
  384. package/src/utils/runtime-model-catalog.ts +11 -1
  385. package/src/utils/secret-scrubber.ts +71 -2
  386. package/src/utils/task-tool-manifest.ts +19 -2
  387. package/src/workflows/executors/swarm-script.ts +1 -0
  388. package/templates/skills/agent-browser/SKILL.md +1 -1
  389. package/templates/skills/agent-browser/content.md +1 -1
  390. package/templates/skills/apps/SKILL.md +8 -1
  391. package/templates/skills/apps/content.md +8 -1
  392. package/templates/skills/chat-sdk/config.json +14 -0
  393. package/templates/skills/chat-sdk/content.md +84 -0
  394. package/templates/skills/composio-gmail/content.md +7 -0
  395. package/templates/skills/composio-google-docs/content.md +1 -1
  396. package/templates/skills/email-domain-health/config.json +14 -0
  397. package/templates/skills/email-domain-health/content.md +131 -0
  398. package/templates/skills/kv-storage/content.md +2 -2
  399. package/templates/skills/pages/content.md +8 -10
  400. package/templates/skills/swarm-extensions/content.md +2 -0
  401. package/templates/skills/swarm-scripts/SKILL.md +4 -2
  402. package/templates/skills/swarm-scripts/config.json +1 -1
  403. package/templates/skills/swarm-scripts/content.md +4 -2
  404. package/templates/skills/workflow-iterate/content.md +38 -0
  405. package/dist/artifact-eczsknc3.js +0 -3843
  406. package/dist/cli-bd957dgb.js +0 -153
  407. package/dist/cli-q51yprfq.js +0 -1104
  408. package/dist/cli-t1ze64h3.js +0 -811
  409. package/dist/cli-tcc4wd5d.js +0 -56471
  410. package/dist/index-9ch1n761.js +0 -1102
  411. package/src/artifact-sdk/index.ts +0 -2
  412. package/src/artifact-sdk/localtunnel.d.ts +0 -20
  413. package/src/artifact-sdk/port.ts +0 -12
  414. package/src/artifact-sdk/server.ts +0 -183
  415. package/src/artifact-sdk/tunnel.ts +0 -19
  416. package/src/commands/artifact.ts +0 -249
  417. package/src/http/ecosystem.ts +0 -77
  418. package/src/tests/artifact-commands.test.ts +0 -92
  419. package/src/tests/artifact-sdk.test.ts +0 -793
  420. package/src/tests/register-service-security.test.ts +0 -194
  421. package/src/tools/list-services.ts +0 -107
  422. package/src/tools/register-service.ts +0 -177
  423. package/src/tools/unregister-service.ts +0 -73
  424. package/src/tools/update-service-status.ts +0 -96
  425. package/templates/skills/artifacts/config.json +0 -14
  426. package/templates/skills/artifacts/content.md +0 -344
  427. package/templates/skills/artifacts/files/examples/approval-flow.ts +0 -34
  428. package/templates/skills/artifacts/files/examples/hono-dashboard.ts +0 -31
  429. package/templates/skills/artifacts/files/examples/multi-artifact.ts +0 -20
  430. package/templates/skills/artifacts/files/examples/static-report.sh +0 -17
  431. /package/src/{artifact-sdk → pages}/browser-sdk.ts +0 -0
  432. /package/src/{artifact-sdk → pages}/feedback-overlay.ts +0 -0
@@ -0,0 +1,382 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { unlink } from "node:fs/promises";
3
+ import { createServer as createHttpServer, type Server } from "node:http";
4
+ import {
5
+ startTelegramBotApiMock,
6
+ type TelegramBotApiMock,
7
+ } from "../../scripts/dev/telegram-bot-api-mock";
8
+ import { closeDb, createAgent, initDb } from "../be/db";
9
+ import { createIntegrationsHandler } from "../http/integrations";
10
+ import { getPathSegments } from "../http/utils";
11
+ import { stopChatRelayWatcher } from "../integrations/chat-sdk/relay";
12
+ import { startChatSdk, stopChatSdk } from "../integrations/chat-sdk/runtime";
13
+ import { snapshotChatSdkEnv, TELEGRAM_SECRET } from "./chat-sdk-harness";
14
+ import { listenOnFreePort } from "./test-net";
15
+
16
+ /*
17
+ * GET /api/integrations/chat/status and
18
+ * POST /api/integrations/chat/telegram/register-webhook.
19
+ * Telegram runs against the fake Bot API. Slack runs in socket mode against a
20
+ * stub Web API that answers every method with `invalid_auth`, so the socket
21
+ * start in `initialize()` fails at once (auth errors are not retried) and the
22
+ * adapter health is `error`. (Webhook mode only warns on a failed auth.test.)
23
+ */
24
+ const TEST_DB_PATH = "./test-chat-sdk-status-route.sqlite";
25
+ const PUBLIC_URL = "https://swarm.example.test";
26
+ const BOT_TOKEN = "123456:status-route-test-token";
27
+ const BOOT_ADAPTERS = {
28
+ telegram: { botToken: BOT_TOKEN, secretToken: TELEGRAM_SECRET, mode: "webhook" },
29
+ slack: { botToken: "xoxb-status-route", appToken: "xapp-status-route", mode: "socket" },
30
+ };
31
+ /** Configured but never started: `startChatSdk` ran before it was added. */
32
+ const DISCORD_ENTRY = {
33
+ botToken: "discord-status-route-token",
34
+ publicKey: "0".repeat(64),
35
+ applicationId: "123456789012345678",
36
+ };
37
+
38
+ const restoreEnv = snapshotChatSdkEnv();
39
+ const savedPublicUrl = process.env.PUBLIC_MCP_BASE_URL;
40
+ let server: Server;
41
+ let baseUrl = "";
42
+ let telegram: TelegramBotApiMock;
43
+ let slackStub: ReturnType<typeof Bun.serve>;
44
+ let leadId = "";
45
+ let workerId = "";
46
+
47
+ async function removeDbFiles(): Promise<void> {
48
+ for (const suffix of ["", "-wal", "-shm"]) {
49
+ await unlink(`${TEST_DB_PATH}${suffix}`).catch(() => {});
50
+ }
51
+ }
52
+
53
+ beforeAll(async () => {
54
+ await removeDbFiles();
55
+ initDb(TEST_DB_PATH);
56
+ leadId = (
57
+ await createAgent({
58
+ id: crypto.randomUUID(),
59
+ name: "StatusRouteLead",
60
+ isLead: true,
61
+ status: "idle",
62
+ capabilities: [],
63
+ })
64
+ ).id;
65
+ workerId = (
66
+ await createAgent({
67
+ id: crypto.randomUUID(),
68
+ name: "StatusRouteWorker",
69
+ isLead: false,
70
+ status: "idle",
71
+ capabilities: [],
72
+ })
73
+ ).id;
74
+
75
+ telegram = startTelegramBotApiMock();
76
+ slackStub = Bun.serve({
77
+ port: 0,
78
+ hostname: "127.0.0.1",
79
+ fetch: () => Response.json({ ok: false, error: "invalid_auth" }),
80
+ });
81
+ process.env.PUBLIC_MCP_BASE_URL = PUBLIC_URL;
82
+ process.env.SLACK_DISABLE = "true";
83
+ process.env.CHAT_SDK_ENABLED = "true";
84
+ process.env.CHAT_SDK_ADAPTERS = JSON.stringify(BOOT_ADAPTERS);
85
+ process.env.CHAT_SDK_API_URL_OVERRIDES = JSON.stringify({
86
+ telegram: telegram.url,
87
+ slack: `http://127.0.0.1:${slackStub.port}/api/`,
88
+ });
89
+ await startChatSdk();
90
+ await stopChatRelayWatcher();
91
+
92
+ const handler = createIntegrationsHandler();
93
+ server = createHttpServer(async (req, res) => {
94
+ const handled = await handler(req, res, getPathSegments(req.url || ""));
95
+ if (!handled) {
96
+ res.writeHead(404);
97
+ res.end("not found");
98
+ }
99
+ });
100
+ baseUrl = `http://127.0.0.1:${await listenOnFreePort(server)}`;
101
+ });
102
+
103
+ afterAll(async () => {
104
+ server.close();
105
+ await stopChatSdk();
106
+ telegram.stop();
107
+ slackStub.stop(true);
108
+ closeDb();
109
+ await removeDbFiles();
110
+ restoreEnv();
111
+ if (savedPublicUrl === undefined) delete process.env.PUBLIC_MCP_BASE_URL;
112
+ else process.env.PUBLIC_MCP_BASE_URL = savedPublicUrl;
113
+ });
114
+
115
+ /** Run `fn` with temporary env values, then restore them (undefined deletes). */
116
+ async function withEnv(
117
+ values: Record<string, string | undefined>,
118
+ fn: () => Promise<void>,
119
+ ): Promise<void> {
120
+ const saved = Object.fromEntries(Object.keys(values).map((key) => [key, process.env[key]]));
121
+ for (const [key, value] of Object.entries(values)) {
122
+ if (value === undefined) delete process.env[key];
123
+ else process.env[key] = value;
124
+ }
125
+ try {
126
+ await fn();
127
+ } finally {
128
+ for (const [key, value] of Object.entries(saved)) {
129
+ if (value === undefined) delete process.env[key];
130
+ else process.env[key] = value;
131
+ }
132
+ }
133
+ }
134
+
135
+ async function getStatus(): Promise<StatusBody> {
136
+ const res = await fetch(`${baseUrl}/api/integrations/chat/status`);
137
+ expect(res.status).toBe(200);
138
+ return (await res.json()) as StatusBody;
139
+ }
140
+
141
+ async function registerAsLead(): Promise<Response> {
142
+ return fetch(`${baseUrl}/api/integrations/chat/telegram/register-webhook`, {
143
+ method: "POST",
144
+ headers: { "content-type": "application/json", "x-agent-id": leadId },
145
+ body: "{}",
146
+ });
147
+ }
148
+
149
+ interface StatusBody {
150
+ enabled: boolean;
151
+ slackClash: boolean;
152
+ error?: string;
153
+ adapters: Array<{
154
+ name: string;
155
+ configured: boolean;
156
+ enabled: boolean;
157
+ health: string;
158
+ error?: string;
159
+ webhookUrl: string;
160
+ mode?: string;
161
+ guilds?: { count: number; names: string[] };
162
+ }>;
163
+ }
164
+
165
+ describe("GET /api/integrations/chat/status", () => {
166
+ test("reports one ok and one error adapter, webhook URLs, and no secrets", async () => {
167
+ const res = await fetch(`${baseUrl}/api/integrations/chat/status`);
168
+ expect(res.status).toBe(200);
169
+ const text = await res.text();
170
+ for (const secret of [BOT_TOKEN, TELEGRAM_SECRET, "xoxb-status-route", "xapp-status-route"]) {
171
+ expect(text).not.toContain(secret);
172
+ }
173
+ const body = JSON.parse(text) as StatusBody;
174
+ expect(body.enabled).toBe(true);
175
+ expect(body.slackClash).toBe(false);
176
+ const byName = Object.fromEntries(body.adapters.map((a) => [a.name, a]));
177
+ expect(Object.keys(byName).sort()).toEqual(["discord", "slack", "teams", "telegram"]);
178
+
179
+ expect(byName.telegram).toMatchObject({
180
+ configured: true,
181
+ health: "ok",
182
+ mode: "webhook",
183
+ webhookUrl: `${PUBLIC_URL}/api/integrations/chat/telegram/webhook`,
184
+ });
185
+ expect(byName.telegram?.error).toBeUndefined();
186
+
187
+ expect(byName.slack).toMatchObject({ configured: true, health: "error", mode: "socket" });
188
+ expect(byName.slack?.error).toContain("invalid_auth");
189
+
190
+ expect(byName.discord).toMatchObject({
191
+ configured: false,
192
+ health: "disabled",
193
+ webhookUrl: `${PUBLIC_URL}/api/integrations/chat/discord/webhook`,
194
+ });
195
+ expect(byName.teams).toMatchObject({ configured: false, health: "disabled" });
196
+ });
197
+
198
+ test("flags the Slack clash when SLACK_DISABLE is not true", async () => {
199
+ process.env.SLACK_DISABLE = "false";
200
+ try {
201
+ const body = (await (
202
+ await fetch(`${baseUrl}/api/integrations/chat/status`)
203
+ ).json()) as StatusBody;
204
+ expect(body.slackClash).toBe(true);
205
+ } finally {
206
+ process.env.SLACK_DISABLE = "true";
207
+ }
208
+ });
209
+
210
+ test("a configured adapter that is not running says it is still starting", async () => {
211
+ await withEnv(
212
+ { CHAT_SDK_ADAPTERS: JSON.stringify({ ...BOOT_ADAPTERS, discord: DISCORD_ENTRY }) },
213
+ async () => {
214
+ const discord = (await getStatus()).adapters.find((a) => a.name === "discord");
215
+ expect(discord).toMatchObject({ configured: true, health: "disabled" });
216
+ expect(discord?.error).toStartWith("The adapter is starting");
217
+ },
218
+ );
219
+ });
220
+
221
+ test("a Slack clash blocks boot: the idle adapter says Not started with the cause", async () => {
222
+ await withEnv(
223
+ {
224
+ SLACK_DISABLE: "false",
225
+ CHAT_SDK_ADAPTERS: JSON.stringify({ ...BOOT_ADAPTERS, discord: DISCORD_ENTRY }),
226
+ },
227
+ async () => {
228
+ const body = await getStatus();
229
+ expect(body.slackClash).toBe(true);
230
+ const discord = body.adapters.find((a) => a.name === "discord");
231
+ expect(discord?.error).toStartWith("Not started: ");
232
+ expect(discord?.error).toContain("SLACK_DISABLE=true");
233
+ },
234
+ );
235
+ });
236
+
237
+ test("a missing $config row makes that adapter incomplete; the others keep running", async () => {
238
+ const discord = { ...DISCORD_ENTRY, botToken: { $config: "CHAT_SDK_DISCORD_STATUS_MISSING" } };
239
+ await withEnv(
240
+ { CHAT_SDK_ADAPTERS: JSON.stringify({ ...BOOT_ADAPTERS, discord }) },
241
+ async () => {
242
+ const body = await getStatus();
243
+ expect(body.error).toBeUndefined();
244
+ const entry = body.adapters.find((a) => a.name === "discord");
245
+ expect(entry).toMatchObject({ configured: true, health: "incomplete" });
246
+ expect(entry?.error).toContain("CHAT_SDK_DISCORD_STATUS_MISSING");
247
+ expect(entry?.error?.length ?? 0).toBeLessThanOrEqual(300);
248
+ expect(body.adapters.find((a) => a.name === "telegram")?.health).toBe("ok");
249
+ },
250
+ );
251
+ });
252
+
253
+ test("a partial save (required field missing) is incomplete and names the field", async () => {
254
+ await withEnv(
255
+ { CHAT_SDK_ADAPTERS: JSON.stringify({ ...BOOT_ADAPTERS, discord: { botToken: "x" } }) },
256
+ async () => {
257
+ const body = await getStatus();
258
+ expect(body.error).toBeUndefined();
259
+ const entry = body.adapters.find((a) => a.name === "discord");
260
+ expect(entry).toMatchObject({ configured: true, health: "incomplete" });
261
+ expect(entry?.error).toBe("CHAT_SDK_ADAPTERS.discord.publicKey: required");
262
+ expect(entry?.guilds).toBeUndefined();
263
+ },
264
+ );
265
+ });
266
+
267
+ test("an entry with enabled: false is configured, not enabled, and says the config is kept", async () => {
268
+ await withEnv(
269
+ {
270
+ CHAT_SDK_ADAPTERS: JSON.stringify({
271
+ ...BOOT_ADAPTERS,
272
+ discord: { ...DISCORD_ENTRY, enabled: false },
273
+ }),
274
+ },
275
+ async () => {
276
+ const body = await getStatus();
277
+ const entry = body.adapters.find((a) => a.name === "discord");
278
+ expect(entry).toMatchObject({ configured: true, enabled: false, health: "disabled" });
279
+ expect(entry?.error).toContain("configuration is kept");
280
+ expect(body.adapters.find((a) => a.name === "telegram")?.enabled).toBe(true);
281
+ },
282
+ );
283
+ });
284
+
285
+ test("a forbidden key blocks boot: Not started with the config error, bounded", async () => {
286
+ const discord = { ...DISCORD_ENTRY, apiUrl: "http://evil.example" };
287
+ await withEnv(
288
+ { CHAT_SDK_ADAPTERS: JSON.stringify({ ...BOOT_ADAPTERS, discord }) },
289
+ async () => {
290
+ const body = await getStatus();
291
+ expect(body.error).toContain("discord.apiUrl");
292
+ expect(body.error?.length ?? 0).toBeLessThanOrEqual(300);
293
+ const entry = body.adapters.find((a) => a.name === "discord");
294
+ expect(entry?.error).toStartWith("Not started: ");
295
+ expect(entry?.error).toContain("discord.apiUrl");
296
+ },
297
+ );
298
+ });
299
+
300
+ test("invalid JSON reports a fixed error without the parser message or the input", async () => {
301
+ const broken = `{"telegram":{"botToken":"${BOT_TOKEN}",`;
302
+ await withEnv({ CHAT_SDK_ADAPTERS: broken }, async () => {
303
+ const res = await fetch(`${baseUrl}/api/integrations/chat/status`);
304
+ const text = await res.text();
305
+ expect(text).not.toContain(BOT_TOKEN);
306
+ expect((JSON.parse(text) as StatusBody).error).toBe("CHAT_SDK_ADAPTERS is not valid JSON");
307
+ });
308
+ });
309
+ });
310
+
311
+ describe("POST /api/integrations/chat/telegram/register-webhook", () => {
312
+ test("refuses a non-lead agent with 403 and calls no Bot API method", async () => {
313
+ const before = telegram.calls.length;
314
+ const res = await fetch(`${baseUrl}/api/integrations/chat/telegram/register-webhook`, {
315
+ method: "POST",
316
+ headers: { "content-type": "application/json", "x-agent-id": workerId },
317
+ body: "{}",
318
+ });
319
+ expect(res.status).toBe(403);
320
+ expect(telegram.calls.slice(before).map((c) => c.method)).not.toContain("setWebhook");
321
+ });
322
+
323
+ test("lead registers the public URL and secret, gets getWebhookInfo back", async () => {
324
+ const before = telegram.calls.length;
325
+ const res = await fetch(`${baseUrl}/api/integrations/chat/telegram/register-webhook`, {
326
+ method: "POST",
327
+ headers: { "content-type": "application/json", "x-agent-id": leadId },
328
+ body: "{}",
329
+ });
330
+ expect(res.status).toBe(200);
331
+ const expectedUrl = `${PUBLIC_URL}/api/integrations/chat/telegram/webhook`;
332
+ const body = (await res.json()) as {
333
+ webhookUrl: string;
334
+ webhookInfo: { url: string; pending_update_count?: number };
335
+ };
336
+ expect(body.webhookUrl).toBe(expectedUrl);
337
+ expect(body.webhookInfo.url).toBe(expectedUrl);
338
+ expect(body.webhookInfo.pending_update_count).toBe(0);
339
+
340
+ const calls = telegram.calls.slice(before);
341
+ expect(calls.map((c) => c.method)).toEqual(["setWebhook", "getWebhookInfo"]);
342
+ expect(calls[0]?.body).toEqual({ url: expectedUrl, secret_token: TELEGRAM_SECRET });
343
+ });
344
+
345
+ test("400 in polling mode, with no Bot API call", async () => {
346
+ const before = telegram.calls.length;
347
+ const adapters = { ...BOOT_ADAPTERS, telegram: { botToken: BOT_TOKEN, mode: "polling" } };
348
+ await withEnv({ CHAT_SDK_ADAPTERS: JSON.stringify(adapters) }, async () => {
349
+ const res = await registerAsLead();
350
+ expect(res.status).toBe(400);
351
+ expect(((await res.json()) as { error: string }).error).toContain("polling mode");
352
+ });
353
+ expect(telegram.calls.length).toBe(before);
354
+ });
355
+
356
+ test("400 says the channel is disabled when CHAT_SDK_ENABLED is false", async () => {
357
+ const before = telegram.calls.length;
358
+ await withEnv({ CHAT_SDK_ENABLED: "false" }, async () => {
359
+ const res = await registerAsLead();
360
+ expect(res.status).toBe(400);
361
+ const { error } = (await res.json()) as { error: string };
362
+ expect(error).toContain("disabled");
363
+ expect(error).not.toContain("not configured");
364
+ });
365
+ expect(telegram.calls.length).toBe(before);
366
+ });
367
+
368
+ test("502 when the Bot API rejects setWebhook, with the bot token redacted", async () => {
369
+ telegram.failSetWebhook(true);
370
+ try {
371
+ const res = await registerAsLead();
372
+ expect(res.status).toBe(502);
373
+ const text = await res.text();
374
+ expect(text).not.toContain(BOT_TOKEN);
375
+ const { error } = JSON.parse(text) as { error: string };
376
+ expect(error).toContain("Telegram setWebhook failed");
377
+ expect(error).toContain("[REDACTED]");
378
+ } finally {
379
+ telegram.failSetWebhook(false);
380
+ }
381
+ });
382
+ });
@@ -0,0 +1,277 @@
1
+ import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test";
2
+ import { unlink } from "node:fs/promises";
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import { closeDb, createAgent, getDbClient, initDb } from "../be/db";
5
+ import { drainBackground } from "../integrations/chat-sdk/background";
6
+ import { stopChatRelayWatcher } from "../integrations/chat-sdk/relay";
7
+ import { getChatSdkHealth, startChatSdk, stopChatSdk } from "../integrations/chat-sdk/runtime";
8
+ import { isTrustedTeamsServiceUrl } from "../integrations/chat-sdk/teams-origin";
9
+ import { setChatSdkTestHooks } from "../integrations/chat-sdk/test-hooks";
10
+ import { registerChatPostTool } from "../tools/chat-post";
11
+ import { flushChatBuffers, postChatWebhook, snapshotChatSdkEnv } from "./chat-sdk-harness";
12
+ import { getFreePort } from "./test-net";
13
+
14
+ /*
15
+ * Network: Teams `initialize()` only registers routes (no token or JWKS
16
+ * fetch), and an inbound activity without `aadObjectId` in a new thread reads
17
+ * nothing from Microsoft (no member lookup, no thread history). The Teams SDK
18
+ * talks HTTP through axios and MSAL, not `fetch`, so a `fetch` stub would not
19
+ * catch a call anyway. `apiUrl` points at a port nothing listens on
20
+ * (from `getFreePort()`), so an unexpected connector call fails here. It
21
+ * redirects connector calls only, not the MSAL token call to
22
+ * login.microsoftonline.com. The tests send nothing outbound, so no token
23
+ * call happens.
24
+ *
25
+ * No health-isolation case here: Teams `initialize()` only registers routes and
26
+ * has no clean failure (the schema rejects blank ids), and a failing second
27
+ * adapter needs a slow Slack WebClient retry or a real network call.
28
+ */
29
+ const TEST_DB_PATH = "./test-chat-sdk-teams.sqlite";
30
+ const APP_ID = "00000000-0000-0000-0000-00000000a99";
31
+ let UNREACHABLE_API_URL = "";
32
+
33
+ const restoreEnv = snapshotChatSdkEnv();
34
+ let leadId: string;
35
+ let nextActivityId = 1_700_000_000_000;
36
+
37
+ async function removeDbFiles(): Promise<void> {
38
+ for (const suffix of ["", "-wal", "-shm"]) {
39
+ await unlink(`${TEST_DB_PATH}${suffix}`).catch(() => {});
40
+ }
41
+ }
42
+
43
+ async function startTeams(teams: Record<string, unknown>, extra: Record<string, unknown> = {}) {
44
+ process.env.CHAT_SDK_ENABLED = "true";
45
+ process.env.CHAT_SDK_ADAPTERS = JSON.stringify({ teams, ...extra });
46
+ await startChatSdk();
47
+ await stopChatRelayWatcher();
48
+ }
49
+
50
+ /** A minimal Bot Framework `message` Activity that mentions the bot in a group chat. */
51
+ function teamsMention(conversationId: string, text: string): Record<string, unknown> {
52
+ const bot = { id: `28:${APP_ID}`, name: "swarm" };
53
+ return {
54
+ type: "message",
55
+ id: String(++nextActivityId),
56
+ timestamp: new Date().toISOString(),
57
+ channelId: "msteams",
58
+ serviceUrl: "https://smba.trafficmanager.net/teams/",
59
+ from: { id: "29:user-1", name: "Tester" },
60
+ recipient: bot,
61
+ conversation: { id: conversationId, conversationType: "groupChat", isGroup: true },
62
+ text: `<at>swarm</at> ${text}`,
63
+ entities: [{ type: "mention", mentioned: bot, text: "<at>swarm</at>" }],
64
+ };
65
+ }
66
+
67
+ async function postActivity(activity: unknown) {
68
+ return postChatWebhook("teams", JSON.stringify(activity), {
69
+ "content-type": "application/json",
70
+ });
71
+ }
72
+
73
+ beforeAll(async () => {
74
+ await removeDbFiles();
75
+ initDb(TEST_DB_PATH);
76
+ const lead = await createAgent({
77
+ id: crypto.randomUUID(),
78
+ name: "TeamsChatLead",
79
+ isLead: true,
80
+ status: "idle",
81
+ capabilities: [],
82
+ });
83
+ leadId = lead.id;
84
+ UNREACHABLE_API_URL = `http://127.0.0.1:${await getFreePort()}/`;
85
+ process.env.CHAT_SDK_BUFFER_MS = "60000";
86
+ process.env.CHAT_SDK_API_URL_OVERRIDES = JSON.stringify({ teams: UNREACHABLE_API_URL });
87
+ });
88
+
89
+ afterEach(async () => {
90
+ await stopChatSdk();
91
+ setChatSdkTestHooks({});
92
+ });
93
+
94
+ afterAll(async () => {
95
+ restoreEnv();
96
+ closeDb();
97
+ await removeDbFiles();
98
+ });
99
+
100
+ describe("Chat SDK Teams adapter", () => {
101
+ test("with the test verifier, a mention Activity creates a teams chat task", async () => {
102
+ setChatSdkTestHooks({ teamsWebhookVerifier: () => true });
103
+ await startTeams({ appId: APP_ID, appPassword: "teams-test-password" });
104
+ expect(getChatSdkHealth().teams?.status).toBe("ok");
105
+
106
+ const response = await postActivity(teamsMention("19:group-one@thread.v2", "triage the inbox"));
107
+ expect(response.status).toBe(200);
108
+ expect(await drainBackground(5_000)).toBe(true);
109
+ await flushChatBuffers();
110
+
111
+ const tasks = await getDbClient().query<{
112
+ contextKey: string;
113
+ source: string;
114
+ agentId: string | null;
115
+ task: string;
116
+ }>("SELECT contextKey, source, agentId, task FROM agent_tasks WHERE contextKey GLOB ?", [
117
+ "task:chat-sdk:teams:*",
118
+ ]);
119
+ expect(tasks).toHaveLength(1);
120
+ expect(tasks[0]!.source).toBe("chat");
121
+ expect(tasks[0]!.agentId).toBe(leadId);
122
+ expect(tasks[0]!.task).toContain("triage the inbox");
123
+ });
124
+
125
+ test("without a verifier, an unsigned Activity is rejected with 401", async () => {
126
+ await startTeams({ appId: APP_ID, appPassword: "teams-test-password" });
127
+ expect(getChatSdkHealth().teams?.status).toBe("ok");
128
+ const response = await postActivity(teamsMention("19:group-two@thread.v2", "should not land"));
129
+ expect(response.status).toBe(401);
130
+ const row = await getDbClient().get<{ n: number }>(
131
+ "SELECT COUNT(*) AS n FROM agent_tasks WHERE task LIKE '%should not land%'",
132
+ );
133
+ expect(row?.n).toBe(0);
134
+ });
135
+ });
136
+
137
+ /** A Teams thread id in the adapter's encoding, pointing at `serviceUrl`. */
138
+ function teamsThreadId(conversationId: string, serviceUrl: string): string {
139
+ const encode = (value: string) => Buffer.from(value).toString("base64url");
140
+ return `teams:${encode(conversationId)}:${encode(serviceUrl)}`;
141
+ }
142
+
143
+ /** An unsigned JWT the Teams SDK accepts from a token factory (it only decodes it). */
144
+ function fakeBotToken(): string {
145
+ const encode = (value: Record<string, unknown>) =>
146
+ Buffer.from(JSON.stringify(value)).toString("base64url");
147
+ const exp = Math.floor(Date.now() / 1000) + 3600;
148
+ return `${encode({ alg: "none", typ: "JWT" })}.${encode({ exp, aud: APP_ID })}.sig`;
149
+ }
150
+
151
+ interface ConnectorListener {
152
+ url: string;
153
+ requests: { method: string; path: string; authorization: string | null }[];
154
+ stop: () => void;
155
+ }
156
+
157
+ /** A local stand-in for a connector host that records every request. */
158
+ function startConnectorListener(): ConnectorListener {
159
+ const requests: ConnectorListener["requests"] = [];
160
+ const server = Bun.serve({
161
+ port: 0,
162
+ hostname: "127.0.0.1",
163
+ fetch(req) {
164
+ requests.push({
165
+ method: req.method,
166
+ path: new URL(req.url).pathname,
167
+ authorization: req.headers.get("authorization"),
168
+ });
169
+ return Response.json({ id: "activity-1" });
170
+ },
171
+ });
172
+ return { url: `http://127.0.0.1:${server.port}/`, requests, stop: () => server.stop(true) };
173
+ }
174
+
175
+ type ToolResult = {
176
+ isError?: boolean;
177
+ structuredContent: { success: boolean; message: string; [key: string]: unknown };
178
+ };
179
+
180
+ async function callChatPost(args: Record<string, unknown>): Promise<ToolResult> {
181
+ const server = new McpServer({ name: "chat-sdk-teams-test", version: "1.0.0" });
182
+ registerChatPostTool(server);
183
+ // biome-ignore lint/complexity/noBannedTypes: accessing internal MCP SDK type for test
184
+ const tools = (server as unknown as { _registeredTools: Record<string, { handler: Function }> })
185
+ ._registeredTools;
186
+ return (await tools["chat-post"]!.handler(args, {
187
+ sessionId: "test-session",
188
+ requestInfo: { headers: { "x-agent-id": leadId } },
189
+ })) as ToolResult;
190
+ }
191
+
192
+ describe("Chat SDK Teams service URL allowlist", () => {
193
+ test("only https Bot Framework hosts are trusted by default", () => {
194
+ const saved = process.env.CHAT_SDK_API_URL_OVERRIDES;
195
+ delete process.env.CHAT_SDK_API_URL_OVERRIDES;
196
+ try {
197
+ expect(isTrustedTeamsServiceUrl("https://smba.trafficmanager.net/teams/")).toBe(true);
198
+ expect(isTrustedTeamsServiceUrl("https://smba.trafficmanager.net/emea/")).toBe(true);
199
+ expect(isTrustedTeamsServiceUrl("https://smba.infra.gcc.teams.microsoft.com/teams")).toBe(
200
+ true,
201
+ );
202
+ expect(isTrustedTeamsServiceUrl("http://smba.trafficmanager.net/teams/")).toBe(false);
203
+ expect(isTrustedTeamsServiceUrl("https://smba.trafficmanager.net.evil.test/")).toBe(false);
204
+ expect(isTrustedTeamsServiceUrl("https://smba.trafficmanager.net@evil.test/")).toBe(false);
205
+ expect(isTrustedTeamsServiceUrl("http://127.0.0.1:8080/")).toBe(false);
206
+ expect(isTrustedTeamsServiceUrl("not a url")).toBe(false);
207
+ } finally {
208
+ if (saved === undefined) delete process.env.CHAT_SDK_API_URL_OVERRIDES;
209
+ else process.env.CHAT_SDK_API_URL_OVERRIDES = saved;
210
+ }
211
+ });
212
+
213
+ test("chat-post to a forged Teams id sends no request to its service URL", async () => {
214
+ const listener = startConnectorListener();
215
+ const saved = process.env.CHAT_SDK_API_URL_OVERRIDES;
216
+ // Production shape: no apiUrl override, so the adapter would send to the id's service URL.
217
+ delete process.env.CHAT_SDK_API_URL_OVERRIDES;
218
+ try {
219
+ setChatSdkTestHooks({ teamsToken: fakeBotToken });
220
+ await startTeams({ appId: APP_ID, appPassword: "teams-test-password" });
221
+ const threadId = teamsThreadId("19:forged@thread.v2", listener.url);
222
+
223
+ const result = await callChatPost({ adapter: "teams", threadId, text: "leak test" });
224
+
225
+ expect(result.isError).toBe(true);
226
+ expect(result.structuredContent.message).toContain("not a Microsoft Bot Framework");
227
+ expect(listener.requests).toHaveLength(0);
228
+ } finally {
229
+ listener.stop();
230
+ if (saved === undefined) delete process.env.CHAT_SDK_API_URL_OVERRIDES;
231
+ else process.env.CHAT_SDK_API_URL_OVERRIDES = saved;
232
+ }
233
+ });
234
+
235
+ test("chat-post to a Teams id with a Bot Framework service URL still sends", async () => {
236
+ const listener = startConnectorListener();
237
+ const saved = process.env.CHAT_SDK_API_URL_OVERRIDES;
238
+ // The override pins every connector call to the listener, whatever the id says.
239
+ process.env.CHAT_SDK_API_URL_OVERRIDES = JSON.stringify({ teams: listener.url });
240
+ try {
241
+ setChatSdkTestHooks({ teamsToken: fakeBotToken });
242
+ await startTeams({ appId: APP_ID, appPassword: "teams-test-password" });
243
+ const threadId = teamsThreadId(
244
+ "19:allowed@thread.v2",
245
+ "https://smba.trafficmanager.net/teams/",
246
+ );
247
+
248
+ const result = await callChatPost({ adapter: "teams", threadId, text: "hello teams" });
249
+
250
+ expect(result.isError).toBeFalsy();
251
+ expect(listener.requests).toHaveLength(1);
252
+ expect(listener.requests[0]!.method).toBe("POST");
253
+ expect(listener.requests[0]!.path).toContain("/v3/conversations/");
254
+ expect(listener.requests[0]!.authorization).toStartWith("Bearer ");
255
+ } finally {
256
+ listener.stop();
257
+ if (saved === undefined) delete process.env.CHAT_SDK_API_URL_OVERRIDES;
258
+ else process.env.CHAT_SDK_API_URL_OVERRIDES = saved;
259
+ }
260
+ });
261
+
262
+ test("a Teams activity with an untrusted serviceUrl is refused before the adapter", async () => {
263
+ setChatSdkTestHooks({ teamsWebhookVerifier: () => true });
264
+ await startTeams({ appId: APP_ID, appPassword: "teams-test-password" });
265
+ const activity = {
266
+ ...teamsMention("19:group-three@thread.v2", "hostile tenant"),
267
+ serviceUrl: "https://attacker.example/teams/",
268
+ };
269
+ const response = await postActivity(activity);
270
+ expect(response.status).toBe(403);
271
+ await flushChatBuffers();
272
+ const row = await getDbClient().get<{ n: number }>(
273
+ "SELECT COUNT(*) AS n FROM agent_tasks WHERE task LIKE '%hostile tenant%'",
274
+ );
275
+ expect(row?.n).toBe(0);
276
+ });
277
+ });