@desplega.ai/agent-swarm 1.172.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 (525) hide show
  1. package/README.md +1 -1
  2. package/dist/{acp-adapter-fpecx1g1.js → acp-adapter-sj2g0m84.js} +18 -17
  3. package/dist/{actions-pw87a1a8.js → actions-fegk44gr.js} +65 -35
  4. package/dist/{amp-adapter-8jem2bc0.js → amp-adapter-ad8ft89x.js} +8 -7
  5. package/dist/{anthropic-messages-8ee2f22q.js → anthropic-messages-r6czkjv1.js} +7 -7
  6. package/dist/{app-wmf0m843.js → app-kfvgbvzy.js} +19 -13
  7. package/dist/{assistant-emp1e108.js → assistant-z7zdy9ja.js} +65 -38
  8. package/dist/{boot-reembed-1de4f4aj.js → boot-reembed-5c2z0mbe.js} +11 -9
  9. package/dist/{boot-reembed-4ymn9atn.js → boot-reembed-j8xa181e.js} +11 -9
  10. package/dist/{boot-scrub-sweep-adhk1s8c.js → boot-scrub-sweep-hnkn3rgt.js} +12 -10
  11. package/dist/{boot-seeds-8q637aew.js → boot-seeds-kwjq4d2z.js} +15 -13
  12. package/dist/{claude-adapter-t26cv8a4.js → claude-adapter-9vmvpvgd.js} +20 -19
  13. package/dist/{claude-managed-adapter-xgxhqrt2.js → claude-managed-adapter-k9qae05j.js} +7 -5
  14. package/dist/{claude-managed-setup-bnmjarjc.js → claude-managed-setup-q54zx5q2.js} +6 -4
  15. package/dist/{claude-sdk-session-gsfsh662.js → claude-sdk-session-9jye6cxr.js} +12698 -11995
  16. package/dist/{cli-k46r9hmk.js → cli-03gedhng.js} +16 -14
  17. package/dist/{cli-kb4ehzx5.js → cli-1e4b6bzh.js} +7 -7
  18. package/dist/{cli-d603qf86.js → cli-29jc5q7w.js} +10 -8
  19. package/dist/cli-2m1ktfzz.js +17795 -0
  20. package/dist/{cli-8vs9trd4.js → cli-3dj8k4vy.js} +1 -1
  21. package/dist/{cli-zxm9p3x4.js → cli-3fbmmawm.js} +5 -5
  22. package/dist/{cli-gmdajfw5.js → cli-4jxm2qtz.js} +4987 -1708
  23. package/dist/{cli-tadfgd0t.js → cli-5hkm7whv.js} +33 -4
  24. package/dist/{cli-6jb2rf77.js → cli-5t4f29ez.js} +3 -3
  25. package/dist/{cli-7a0sde2x.js → cli-6cagzc96.js} +9 -0
  26. package/dist/cli-6cvfxs8r.js +47 -0
  27. package/dist/{cli-pj6x9qce.js → cli-6jyj59nb.js} +2 -2
  28. package/dist/{cli-388egyf6.js → cli-6razj6t4.js} +2 -2
  29. package/dist/{cli-qsztpfry.js → cli-74ahz61x.js} +2 -2
  30. package/dist/{cli-4nz8p1p5.js → cli-7x0g4sn1.js} +15474 -16082
  31. package/dist/{cli-yatxqynm.js → cli-9ha6hs2d.js} +1 -290
  32. package/dist/{cli-81r1wjyv.js → cli-9nfx44eh.js} +1 -1
  33. package/dist/cli-9traj3dn.js +638 -0
  34. package/dist/{cli-smcrrmhy.js → cli-agzebbd5.js} +9 -9
  35. package/dist/{cli-bf7485pm.js → cli-apx87jne.js} +2 -2
  36. package/dist/{cli-ztrv9y21.js → cli-bzszjf2p.js} +3689 -1081
  37. package/dist/{cli-c5km5znr.js → cli-c8r3kzzb.js} +1 -1
  38. package/dist/{cli-hsem16hb.js → cli-cgtksawx.js} +7 -4
  39. package/dist/{cli-db3m6zdt.js → cli-d2qr7aj4.js} +1 -1
  40. package/dist/{cli-1hgat6pa.js → cli-d5f9ed5c.js} +5 -5
  41. package/dist/cli-e3g5mbkz.js +776 -0
  42. package/dist/{cli-7jykv7ef.js → cli-e6mpjhed.js} +1 -1
  43. package/dist/{cli-j6kpj9az.js → cli-ee9gtz70.js} +1553 -68
  44. package/dist/{cli-e0a9sk00.js → cli-eejsgvm4.js} +4 -4
  45. package/dist/{cli-tgpfnm6r.js → cli-ef1hg1wv.js} +518 -59
  46. package/dist/cli-f527ce74.js +781 -0
  47. package/dist/{cli-ppy4nnnh.js → cli-f7kr7svz.js} +2 -2
  48. package/dist/cli-hjwmhcg1.js +10622 -0
  49. package/dist/{cli-2bw3w47j.js → cli-hnhtk6cj.js} +1 -1
  50. package/dist/{cli-ywm1azg9.js → cli-hnmhtzvh.js} +6 -4
  51. package/dist/{cli-4fnb6884.js → cli-hvqbtna4.js} +7 -1259
  52. package/dist/{cli-pstq9888.js → cli-j0pfptdp.js} +220 -103
  53. package/dist/cli-jta2eyzc.js +619 -0
  54. package/dist/{cli-esnpehf5.js → cli-k382tshb.js} +12 -13
  55. package/dist/cli-k3dp5tpf.js +263 -0
  56. package/dist/{cli-wd0hz6ah.js → cli-kbrt8gqa.js} +9 -91
  57. package/dist/cli-kceah1eh.js +92 -0
  58. package/dist/{cli-prrt9fa0.js → cli-ke896fcf.js} +10 -10
  59. package/dist/{cli-nv4zd0b9.js → cli-kt6zqg0e.js} +6 -6
  60. package/dist/{cli-g6t9texm.js → cli-m50t4309.js} +5 -18
  61. package/dist/cli-n2wq4j7m.js +6985 -0
  62. package/dist/{cli-c2pq2rjb.js → cli-n3hzg89b.js} +46 -15
  63. package/dist/{cli-tmhsxhb8.js → cli-n5vwg842.js} +23 -6
  64. package/dist/{cli-5rv1j9ga.js → cli-ne236afy.js} +53 -7
  65. package/dist/cli-p0sg24zp.js +16 -0
  66. package/dist/{cli-d161g69q.js → cli-p1x85d2t.js} +50 -2
  67. package/dist/{cli-7pz3b7x5.js → cli-przpmdgr.js} +25 -0
  68. package/dist/{cli-g4f5pj26.js → cli-q0ke1yww.js} +111 -25
  69. package/dist/{cli-e3pmyt39.js → cli-q50942rs.js} +1 -1
  70. package/dist/{cli-r44t9wqc.js → cli-r106725k.js} +2 -2
  71. package/dist/{cli-gj81e47t.js → cli-r2pnvj8g.js} +2 -2
  72. package/dist/{cli-r8smj8b8.js → cli-r62a35ea.js} +19 -3
  73. package/dist/{cli-cd0hv9nh.js → cli-ra0xzggj.js} +2 -2
  74. package/dist/cli-rhs9pz33.js +294 -0
  75. package/dist/{cli-nyhb1r6x.js → cli-s2sp957d.js} +52 -29
  76. package/dist/{cli-1x7da0de.js → cli-s4setq91.js} +8 -8
  77. package/dist/{cli-xczpdp9e.js → cli-se8w6wny.js} +6 -6
  78. package/dist/cli-skxvvpap.js +10140 -0
  79. package/dist/{cli-014vns2y.js → cli-stfs0k5s.js} +208 -525
  80. package/dist/{cli-0p3enk35.js → cli-tqaazemc.js} +9 -1
  81. package/dist/{cli-cf69s3ew.js → cli-tr4sxp68.js} +3 -3
  82. package/dist/{cli-8hzfb6xz.js → cli-tsde1hdr.js} +15 -15
  83. package/dist/{cli-cceskcvt.js → cli-txqb9s38.js} +8 -8
  84. package/dist/{cli-aspcaef3.js → cli-v1yb0rqb.js} +6 -6
  85. package/dist/{cli-j1pmc07z.js → cli-vatyhshy.js} +268 -40
  86. package/dist/{cli-04emfe2q.js → cli-vdach5a9.js} +5 -5
  87. package/dist/{cli-7avy4v8w.js → cli-w1q7h2r2.js} +5 -3
  88. package/dist/{cli-rcp5aqdw.js → cli-w7m81tb7.js} +1 -1
  89. package/dist/{cli-hwwvy36w.js → cli-wayw93nt.js} +3 -3
  90. package/dist/cli-wmgrkscj.js +23254 -0
  91. package/dist/{cli-gp1rtyvk.js → cli-z5jjpaxw.js} +29 -6
  92. package/dist/{cli-451ks6aw.js → cli-zkpks3r3.js} +1 -1
  93. package/dist/cli.js +32 -48
  94. package/dist/{codex-adapter-m4tgdysy.js → codex-adapter-y81sssk0.js} +16 -15
  95. package/dist/{codex-login-841kv5fk.js → codex-login-f2rcfpp9.js} +3 -2
  96. package/dist/{codex-session-runner-ymwzm6ea.js → codex-session-runner-8vx4j6a9.js} +16 -15
  97. package/dist/{commands-n0hshcvs.js → commands-kcpvpcrr.js} +21 -10
  98. package/dist/{controller-hq5ggpcp.js → controller-ywrmt6yd.js} +5 -4
  99. package/dist/{controllers-connect-y07cqcvy.js → controllers-connect-kwpvj3rf.js} +45 -38
  100. package/dist/{cursor-adapter-q8pgv5qa.js → cursor-adapter-cehv1qns.js} +7 -6
  101. package/dist/{db-hkqvrdcb.js → db-k8ankj20.js} +13 -11
  102. package/dist/{db-4w7tmt1g.js → db-ww5d7jq3.js} +29 -9
  103. package/dist/{devin-adapter-xgtj680j.js → devin-adapter-x1022qb1.js} +3 -2
  104. package/dist/{dispatcher-5sd69y14.js → dispatcher-e8gq5m68.js} +41 -34
  105. package/dist/{docker-f503k9p0.js → docker-ff6y1190.js} +3 -2
  106. package/dist/{dsh-adapter-2h8m4cab.js → dsh-adapter-3epjm3af.js} +7 -6
  107. package/dist/{e2b-66xra4az.js → e2b-8ygwhg0f.js} +4 -3
  108. package/dist/{e2b-xwhyj8cw.js → e2b-swhnpw3n.js} +4 -3
  109. package/dist/{event-bus-vjtv82gm.js → event-bus-6axnngdj.js} +3 -2
  110. package/dist/external-channel-h3359cwn.js +52 -0
  111. package/dist/{google-generative-ai-7pavrbv7.js → google-generative-ai-g8db7x0f.js} +5 -3
  112. package/dist/{google-vertex-3f1jk41f.js → google-vertex-r2692q0r.js} +5 -3
  113. package/dist/{grok-adapter-8m9yqzbq.js → grok-adapter-0d4x7gkx.js} +19 -18
  114. package/dist/{handlers-s20xr10r.js → handlers-an1gzv21.js} +44 -37
  115. package/dist/{heartbeat-3k31q4g2.js → heartbeat-6dakeb61.js} +46 -39
  116. package/dist/{hook-gdn6a2fn.js → hook-6v6cv6d5.js} +12 -24
  117. package/dist/{hook-ygwerrdh.js → hook-sa5xye4y.js} +13 -12
  118. package/dist/{http-jdteeg81.js → http-r7f3eky0.js} +1598 -279
  119. package/dist/index-52j1aja4.js +3465 -0
  120. package/dist/{index-e0snym4t.js → index-7ep4gzdk.js} +41 -34
  121. package/dist/index-9gw58n4k.js +193 -0
  122. package/dist/index-9j87mcm8.js +56305 -0
  123. package/dist/index-bsmcresk.js +79424 -0
  124. package/dist/index-mh85qhec.js +475 -0
  125. package/dist/{index-vm3bm9t7.js → index-rmpw9a8q.js} +41 -34
  126. package/dist/{index-6ksewd5y.js → index-s2wxgzr0.js} +1015 -732
  127. package/dist/{index-tsszt7g9.js → index-v1v6n7hg.js} +46 -39
  128. package/dist/index-we27kd31.js +12810 -0
  129. package/dist/{index-ffqzqgwb.js → index-zk96aq1k.js} +11 -9
  130. package/dist/{keepalive-jekc896t.js → keepalive-be9vwy5h.js} +13 -11
  131. package/dist/{lead-4k9s9szx.js → lead-0861cwrf.js} +64 -57
  132. package/dist/{maintenance-shazch4n.js → maintenance-mzzgc8bn.js} +11 -9
  133. package/dist/node-c9we95xf.js +1 -1
  134. package/dist/{node-fkajaq96.js → node-mn1zs7an.js} +2 -1
  135. package/dist/{oauth-refresh-sweep-5gb36kn8.js → oauth-refresh-sweep-cpdnfwdj.js} +12 -10
  136. package/dist/{onboard-ydrbnvww.js → onboard-fh8qs4r3.js} +11 -3
  137. package/dist/{opencode-adapter-5zh0he20.js → opencode-adapter-nm42wwj8.js} +19 -14
  138. package/dist/{otel-impl-6pf5cv86.js → otel-impl-x6c1c9g4.js} +248 -869
  139. package/dist/{pi-mono-adapter-ef9wwx9m.js → pi-mono-adapter-9n2ya240.js} +245 -107
  140. package/dist/{pricing-refresh-s52nsa5q.js → pricing-refresh-qb0rv0ap.js} +13 -11
  141. package/dist/{rbac-roles-aq3msh4e.js → rbac-roles-1gzdwbh1.js} +12 -10
  142. package/dist/{rbac-roles-q5gb5e8v.js → rbac-roles-3mraey52.js} +13 -11
  143. package/dist/{render-v2-pd1rg54q.js → render-v2-agpa45zz.js} +19 -13
  144. package/dist/{resume-gw4a2c6v.js → resume-x0ydy9s9.js} +27 -21
  145. package/dist/{runtime-qfry88v2.js → runtime-sa2vr16g.js} +2 -2
  146. package/dist/{setup-jda1ksdh.js → setup-as9ga0d6.js} +2 -2
  147. package/dist/{virtual-modules-b3ga2m65.js → virtual-modules-9gkxqrap.js} +7 -6
  148. package/dist/{work-objects-tb7ycye8.js → work-objects-y5g1y7ev.js} +49 -37
  149. package/dist/{worker-njjccz5d.js → worker-860c3e38.js} +64 -57
  150. package/dist/{x-qfmygqqv.js → x-m477fr0h.js} +3 -2
  151. package/openapi.json +951 -356
  152. package/package.json +12 -13
  153. package/src/apps/catalog.generated.json +1309 -53
  154. package/src/apps/page-validator.ts +93 -31
  155. package/src/azure-devops/api.ts +94 -18
  156. package/src/azure-devops/auth.ts +32 -0
  157. package/src/azure-devops/boards-sync.ts +708 -0
  158. package/src/azure-devops/handlers.ts +52 -8
  159. package/src/azure-devops/html.ts +26 -0
  160. package/src/azure-devops/index.ts +15 -0
  161. package/src/azure-devops/templates.ts +86 -0
  162. package/src/azure-devops/types.ts +65 -1
  163. package/src/be/config-refs.ts +60 -0
  164. package/src/be/db/page-versions.ts +15 -0
  165. package/src/be/db/runtime.ts +35 -8
  166. package/src/be/db/skill-name-resolution.ts +36 -0
  167. package/src/be/db/tasks/read.ts +230 -0
  168. package/src/be/db/tasks/write.ts +7 -0
  169. package/src/be/db.ts +74 -13
  170. package/src/be/migrations/213_script_connections_sql.sql +79 -0
  171. package/src/be/migrations/215_swarm_config_export_to_env.sql +34 -0
  172. package/src/be/migrations/216_task_resolved_effort.sql +5 -0
  173. package/src/be/migrations/217_chat_sdk_state.sql +104 -0
  174. package/src/be/model-catalog-store.ts +5 -1
  175. package/src/be/model-tier-resolution.ts +12 -1
  176. package/src/be/model-validation.ts +5 -2
  177. package/src/be/models-catalog.ts +26 -1
  178. package/src/be/modelsdev-cache.json +198 -63
  179. package/src/be/modelsdev-cache.ts +9 -1
  180. package/src/be/script-connections.ts +47 -8
  181. package/src/be/script-credential-broker.ts +39 -2
  182. package/src/be/scripts/run-saved.ts +1 -0
  183. package/src/be/scripts/typecheck.ts +10 -8
  184. package/src/be/seed/runner.ts +13 -1
  185. package/src/be/seed/state-db.ts +5 -0
  186. package/src/be/seed/types.ts +6 -0
  187. package/src/be/seed-pricing.ts +19 -2
  188. package/src/be/seed-scripts/catalog/ops-catalog-audit.inline.ts +1 -1
  189. package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +1 -1
  190. package/src/be/seed-skills/bundled-files.generated.json +10 -18
  191. package/src/be/seed-skills/index.ts +107 -3
  192. package/src/be/sql-connections.ts +118 -0
  193. package/src/be/swarm-config-guard.ts +53 -0
  194. package/src/cli.tsx +0 -16
  195. package/src/commands/onboard/non-interactive.ts +4 -2
  196. package/src/commands/profile-sync.ts +5 -3
  197. package/src/commands/provider-credentials.ts +52 -6
  198. package/src/commands/runner.ts +196 -56
  199. package/src/e2b/env.ts +5 -0
  200. package/src/extensions/contract-types.generated.ts +1 -1
  201. package/src/extensions/contract.ts +45 -0
  202. package/src/extensions/dispatcher.ts +7 -0
  203. package/src/extensions/post-bridge.ts +40 -0
  204. package/src/github/handlers.ts +11 -4
  205. package/src/heartbeat/templates.ts +8 -5
  206. package/src/hooks/hook.ts +0 -26
  207. package/src/http/all-routes.ts +0 -1
  208. package/src/http/config.ts +36 -17
  209. package/src/http/core.ts +4 -0
  210. package/src/http/index.ts +14 -2
  211. package/src/http/integrations.ts +88 -1
  212. package/src/http/mcp-oauth.ts +6 -13
  213. package/src/http/mcp.ts +15 -5
  214. package/src/http/page-proxy.ts +2 -4
  215. package/src/http/pages-public.ts +6 -5
  216. package/src/http/pages.ts +5 -14
  217. package/src/http/poll.ts +28 -68
  218. package/src/http/requested-by.ts +54 -0
  219. package/src/http/route-def.ts +7 -6
  220. package/src/http/script-connection-proxy.ts +51 -4
  221. package/src/http/script-connections.ts +5 -2
  222. package/src/http/scripts.ts +1 -0
  223. package/src/http/session-cost-recompute.ts +12 -2
  224. package/src/http/session-data.ts +4 -0
  225. package/src/http/stats.ts +1 -30
  226. package/src/http/tasks.ts +61 -5
  227. package/src/http/utils.ts +14 -0
  228. package/src/http/webhooks.ts +183 -2
  229. package/src/http/x.ts +1 -0
  230. package/src/integrations/chat-sdk/adapters.ts +129 -0
  231. package/src/integrations/chat-sdk/attachments.ts +232 -0
  232. package/src/integrations/chat-sdk/background.ts +36 -0
  233. package/src/integrations/chat-sdk/commands.ts +194 -0
  234. package/src/integrations/chat-sdk/config.ts +198 -0
  235. package/src/integrations/chat-sdk/discord-api.ts +63 -0
  236. package/src/integrations/chat-sdk/discord-gateway.ts +185 -0
  237. package/src/integrations/chat-sdk/handlers.ts +28 -0
  238. package/src/integrations/chat-sdk/inbound.ts +426 -0
  239. package/src/integrations/chat-sdk/limits.ts +48 -0
  240. package/src/integrations/chat-sdk/logger.ts +81 -0
  241. package/src/integrations/chat-sdk/outbound.ts +123 -0
  242. package/src/integrations/chat-sdk/relay.ts +172 -0
  243. package/src/integrations/chat-sdk/router.ts +169 -0
  244. package/src/integrations/chat-sdk/runtime.ts +351 -0
  245. package/src/integrations/chat-sdk/schema.ts +245 -0
  246. package/src/integrations/chat-sdk/state.ts +243 -0
  247. package/src/integrations/chat-sdk/status.ts +244 -0
  248. package/src/integrations/chat-sdk/steering.ts +58 -0
  249. package/src/integrations/chat-sdk/teams-origin.ts +77 -0
  250. package/src/integrations/chat-sdk/telegram-api.ts +56 -0
  251. package/src/integrations/chat-sdk/templates.ts +16 -0
  252. package/src/integrations/chat-sdk/test-hooks.ts +37 -0
  253. package/src/integrations/chat-sdk/thread-buffer.ts +100 -0
  254. package/src/integrations/chat-sdk/typing.ts +65 -0
  255. package/src/jira/adf.ts +3 -1
  256. package/src/kv-reserved-namespaces.ts +2 -0
  257. package/src/oauth/mcp-wrapper.ts +10 -6
  258. package/src/otel-impl.ts +2 -1
  259. package/src/pages/version.ts +4 -5
  260. package/src/prompts/base-prompt.ts +111 -45
  261. package/src/providers/acp-target-catalog.ts +6 -0
  262. package/src/providers/acp-targets.ts +56 -3
  263. package/src/providers/amp-adapter.ts +1 -1
  264. package/src/providers/claude-adapter.ts +27 -2
  265. package/src/providers/claude-flag-settings.ts +133 -0
  266. package/src/providers/claude-managed-adapter.ts +1 -1
  267. package/src/providers/claude-sdk-session.ts +7 -0
  268. package/src/providers/claude-session-events.ts +6 -1
  269. package/src/providers/dsh-adapter.ts +1 -1
  270. package/src/providers/opencode-adapter.ts +268 -37
  271. package/src/providers/opencode-billing.ts +86 -0
  272. package/src/providers/opencode-swarm-plugin.ts +51 -0
  273. package/src/providers/pi-mono-adapter.ts +398 -153
  274. package/src/providers/pricing-sources.md +37 -3
  275. package/src/providers/reasoning-effort.ts +27 -0
  276. package/src/providers/types.ts +11 -2
  277. package/src/rbac/legacy-policy.ts +8 -2
  278. package/src/rbac/permissions.ts +21 -0
  279. package/src/scheduler/scheduler.ts +4 -2
  280. package/src/scripts-runtime/api-client.ts +15 -1
  281. package/src/scripts-runtime/api-types.ts +9 -1
  282. package/src/scripts-runtime/ctx.ts +1 -1
  283. package/src/scripts-runtime/loader.ts +19 -7
  284. package/src/scripts-runtime/mcp-client.ts +30 -19
  285. package/src/scripts-runtime/sdk-allowlist.ts +8 -6
  286. package/src/scripts-runtime/swarm-sdk.ts +0 -11
  287. package/src/scripts-runtime/types/stdlib.d.ts +27 -15
  288. package/src/scripts-runtime/types/swarm-extension.d.ts +63 -1
  289. package/src/scripts-runtime/types/swarm-sdk.d.ts +27 -15
  290. package/src/server.ts +52 -18
  291. package/src/slack/actions.ts +29 -1
  292. package/src/slack/app.ts +14 -2
  293. package/src/slack/assistant.ts +36 -2
  294. package/src/slack/channel-join.ts +3 -16
  295. package/src/slack/commands.ts +9 -1
  296. package/src/slack/event-dedup.ts +1 -1
  297. package/src/slack/external-channel-egress.ts +113 -0
  298. package/src/slack/external-channel.ts +309 -0
  299. package/src/slack/handlers.ts +61 -0
  300. package/src/slack/render-v2.ts +40 -47
  301. package/src/slack/work-objects.ts +5 -0
  302. package/src/tasks/additive-buffer.ts +4 -2
  303. package/src/tasks/context-key.ts +94 -2
  304. package/src/tasks/model-pin.ts +67 -0
  305. package/src/tasks/worker-follow-up.ts +4 -31
  306. package/src/telemetry.ts +2 -1
  307. package/src/tests/acp-adapter.test.ts +42 -0
  308. package/src/tests/api-drain-worker-e2e.test.ts +1 -6
  309. package/src/tests/apps-catalog-feedback.test.ts +310 -0
  310. package/src/tests/apps-catalog-inputs.test.ts +70 -0
  311. package/src/tests/apps-catalog-table.test.ts +113 -0
  312. package/src/tests/azure-devops-boards-sync.test.ts +584 -0
  313. package/src/tests/azure-devops-html.test.ts +13 -0
  314. package/src/tests/azure-devops-webhook-malformed.test.ts +4 -0
  315. package/src/tests/base-prompt.test.ts +58 -54
  316. package/src/tests/chat-sdk-commands.test.ts +191 -0
  317. package/src/tests/chat-sdk-config.test.ts +334 -0
  318. package/src/tests/chat-sdk-discord-api.test.ts +68 -0
  319. package/src/tests/chat-sdk-discord-gateway.test.ts +252 -0
  320. package/src/tests/chat-sdk-discord-webhook.test.ts +126 -0
  321. package/src/tests/chat-sdk-fields-sync.test.ts +50 -0
  322. package/src/tests/chat-sdk-harness.ts +146 -0
  323. package/src/tests/chat-sdk-inbound.test.ts +363 -0
  324. package/src/tests/chat-sdk-relay.test.ts +470 -0
  325. package/src/tests/chat-sdk-reload.test.ts +128 -0
  326. package/src/tests/chat-sdk-shutdown.test.ts +220 -0
  327. package/src/tests/chat-sdk-slack.test.ts +268 -0
  328. package/src/tests/chat-sdk-state.test.ts +163 -0
  329. package/src/tests/chat-sdk-status-route.test.ts +382 -0
  330. package/src/tests/chat-sdk-teams.test.ts +277 -0
  331. package/src/tests/chat-sdk-tools.test.ts +520 -0
  332. package/src/tests/chat-sdk-typing.test.ts +120 -0
  333. package/src/tests/chat-sdk-webhook-telegram.test.ts +200 -0
  334. package/src/tests/claude-adapter-binary.test.ts +144 -0
  335. package/src/tests/claude-flag-settings.test.ts +114 -0
  336. package/src/tests/claude-managed-adapter.test.ts +1 -1
  337. package/src/tests/claude-sdk-lifecycle.test.ts +148 -1
  338. package/src/tests/claude-sdk-process.fixture.ts +7 -0
  339. package/src/tests/claude-thinking-tokens.test.ts +57 -0
  340. package/src/tests/claude-worker-parity.test.ts +2 -7
  341. package/src/tests/config-env-export-chat-sdk.test.ts +11 -0
  342. package/src/tests/config-refs.test.ts +66 -0
  343. package/src/tests/context-key.test.ts +51 -0
  344. package/src/tests/cors-allowlist.test.ts +25 -0
  345. package/src/tests/create-page-tool.test.ts +18 -0
  346. package/src/tests/credential-check.test.ts +139 -0
  347. package/src/tests/credentials.test.ts +21 -0
  348. package/src/tests/db-runtime.test.ts +61 -0
  349. package/src/tests/db-task-read-contract.test.ts +20 -0
  350. package/src/tests/db-task-write-contract.test.ts +10 -0
  351. package/src/tests/deferred-task-wake.test.ts +52 -4
  352. package/src/tests/docs-index-footprint.test.js +3 -3
  353. package/src/tests/dsh-adapter.test.ts +1 -1
  354. package/src/tests/enabled-capabilities.test.ts +20 -0
  355. package/src/tests/entrypoint-api-readiness.test.ts +14 -8
  356. package/src/tests/entrypoint-config-env-export.test.ts +5 -2
  357. package/src/tests/entrypoint-export-to-env.test.ts +78 -0
  358. package/src/tests/explicit-model-validation.test.ts +105 -1
  359. package/src/tests/extensions-example-route-chat-channel.test.ts +154 -0
  360. package/src/tests/extensions-post-bridge.test.ts +51 -0
  361. package/src/tests/fetch-resolved-env.test.ts +128 -0
  362. package/src/tests/fixtures/azure-boards/workitem.commented.json +89 -0
  363. package/src/tests/fixtures/azure-boards/workitem.created.json +81 -0
  364. package/src/tests/fixtures/azure-boards/workitem.deleted.json +70 -0
  365. package/src/tests/fixtures/azure-boards/workitem.restored.json +82 -0
  366. package/src/tests/fixtures/azure-boards/workitem.updated.json +130 -0
  367. package/src/tests/fixtures/catalog-feedback-definition.json.txt +860 -0
  368. package/src/tests/fixtures/catalog-inputs-definition.json.txt +910 -0
  369. package/src/tests/fixtures/catalog-table-definition.json.txt +468 -0
  370. package/src/tests/fixtures/chat-sdk/telegram-private-text.json +10 -0
  371. package/src/tests/fixtures/extensions/route-chat-channel-to-agent/hooks.ts +27 -0
  372. package/src/tests/fixtures/extensions/route-chat-channel-to-agent/manifest.json +9 -0
  373. package/src/tests/fixtures/openai-compatible-registry-extension.child.ts +146 -0
  374. package/src/tests/fixtures/runner-identity-refresh.ts +3 -0
  375. package/src/tests/fixtures/runner-legacy-resume-prompt.ts +122 -0
  376. package/src/tests/github-handlers.test.ts +35 -0
  377. package/src/tests/harness-provider-resolution.test.ts +12 -0
  378. package/src/tests/heartbeat-checklist.test.ts +3 -2
  379. package/src/tests/hook-runtime-close.test.ts +0 -2
  380. package/src/tests/http-api-integration.test.ts +0 -25
  381. package/src/tests/migration-046-budgets.test.ts +17 -16
  382. package/src/tests/migration-217-chat-sdk-state.test.ts +113 -0
  383. package/src/tests/model-tier-resolution.test.ts +93 -0
  384. package/src/tests/models-catalog.test.ts +42 -1
  385. package/src/tests/node-env-runtime.test.ts +98 -0
  386. package/src/tests/openai-compatible-providers.test.ts +134 -0
  387. package/src/tests/openai-compatible-registry-extension.test.ts +42 -0
  388. package/src/tests/opencode-adapter.test.ts +348 -3
  389. package/src/tests/opencode-billing.test.ts +28 -0
  390. package/src/tests/opencode-helmcode-sdk.test.ts +201 -0
  391. package/src/tests/opencode-shell-env.test.ts +145 -0
  392. package/src/tests/openrouter-base-url.test.ts +101 -151
  393. package/src/tests/page-proxy.test.ts +1 -1
  394. package/src/tests/pages-browser-sdk.test.ts +76 -0
  395. package/src/tests/pages-feedback.test.ts +1 -1
  396. package/src/tests/paused-tasks-requested-by.test.ts +102 -0
  397. package/src/tests/pi-custom-provider-fake-server.test.ts +442 -0
  398. package/src/tests/pi-mono-adapter.test.ts +385 -5
  399. package/src/tests/pi-shell-env.test.ts +170 -0
  400. package/src/tests/prompt-template-remaining.test.ts +35 -0
  401. package/src/tests/rbac-engine.test.ts +11 -3
  402. package/src/tests/runner-exit-after-tasks.test.ts +0 -4
  403. package/src/tests/runner-identity-refresh.test.ts +3 -0
  404. package/src/tests/runner-legacy-resume-prompt.test.ts +64 -0
  405. package/src/tests/script-connections-sql.test.ts +675 -0
  406. package/src/tests/script-connections.test.ts +7 -1
  407. package/src/tests/scripts-http.test.ts +201 -1
  408. package/src/tests/scripts-only-gating.test.ts +2 -3
  409. package/src/tests/scripts-runtime.test.ts +58 -40
  410. package/src/tests/secret-scrubber.test.ts +86 -0
  411. package/src/tests/seed-skills-bundled-files.test.ts +45 -53
  412. package/src/tests/seed-skills-retire.test.ts +182 -0
  413. package/src/tests/send-task-requester-gate.test.ts +58 -0
  414. package/src/tests/session-cost-recompute-unit.test.ts +94 -0
  415. package/src/tests/skill-get-name-resolution.test.ts +137 -0
  416. package/src/tests/slack-channel-join.test.ts +4 -1
  417. package/src/tests/slack-external-channel-config.test.ts +65 -0
  418. package/src/tests/slack-external-channel-egress.test.ts +237 -0
  419. package/src/tests/slack-external-channel-guard.test.ts +280 -0
  420. package/src/tests/slack-external-channel-tools.test.ts +173 -0
  421. package/src/tests/slack-external-channel.test.ts +370 -0
  422. package/src/tests/slack-render-v2.test.ts +86 -10
  423. package/src/tests/sonnet-5-5-registry.test.ts +9 -1
  424. package/src/tests/spec-base-url.test.ts +16 -1
  425. package/src/tests/swarm-config-export-to-env.test.ts +267 -0
  426. package/src/tests/swarm-config-reserved-keys.test.ts +16 -0
  427. package/src/tests/swarm-diff.test.ts +1 -1
  428. package/src/tests/task-model-effort-rerun.test.ts +164 -0
  429. package/src/tests/task-session-prompts.test.ts +132 -0
  430. package/src/tests/task-tool-manifest.test.ts +14 -0
  431. package/src/tests/task-tools-ctx.test.ts +2 -0
  432. package/src/tests/tool-annotations.test.ts +6 -6
  433. package/src/tests/ui-logs-parser.test.ts +43 -0
  434. package/src/tests/used-by-route.test.ts +6 -0
  435. package/src/tests/vendored-openapi.test.ts +16 -1
  436. package/src/tests/vercel-ai-gateway.test.ts +149 -0
  437. package/src/tests/workflow-swarm-script.test.ts +38 -0
  438. package/src/tests/workflow-wait-filter.test.ts +5 -3
  439. package/src/tools/chat-access.ts +41 -0
  440. package/src/tools/chat-open-dm.ts +59 -0
  441. package/src/tools/chat-post.ts +74 -0
  442. package/src/tools/chat-read-thread.ts +100 -0
  443. package/src/tools/chat-reply.ts +75 -0
  444. package/src/tools/create-page.ts +3 -13
  445. package/src/tools/defer-task.ts +17 -0
  446. package/src/tools/model-catalog/index.ts +2 -1
  447. package/src/tools/register-chat-channel.ts +139 -0
  448. package/src/tools/send-task.ts +24 -0
  449. package/src/tools/skills/skill-get.ts +17 -9
  450. package/src/tools/slack-post.ts +6 -0
  451. package/src/tools/slack-read.ts +8 -0
  452. package/src/tools/slack-reply.ts +6 -0
  453. package/src/tools/slack-start-thread.ts +6 -0
  454. package/src/tools/slack-update.ts +6 -0
  455. package/src/tools/slack-upload-file.ts +6 -0
  456. package/src/tools/swarm-config/get-config.ts +2 -1
  457. package/src/tools/swarm-config/list-config.ts +2 -1
  458. package/src/tools/swarm-config/set-config.ts +16 -3
  459. package/src/tools/tool-config.ts +8 -6
  460. package/src/types.ts +12 -2
  461. package/src/utils/config-env-export.ts +74 -0
  462. package/src/utils/credentials.test.ts +92 -0
  463. package/src/utils/credentials.ts +52 -4
  464. package/src/utils/harness-provider.ts +15 -6
  465. package/src/utils/node-env.ts +24 -0
  466. package/src/utils/runtime-model-catalog.ts +11 -1
  467. package/src/utils/secret-scrubber.ts +71 -2
  468. package/src/utils/task-tool-manifest.ts +19 -2
  469. package/src/workflows/executors/swarm-script.ts +1 -0
  470. package/templates/skills/agent-browser/SKILL.md +1 -1
  471. package/templates/skills/agent-browser/content.md +1 -1
  472. package/templates/skills/apps/SKILL.md +749 -0
  473. package/templates/skills/apps/content.md +60 -22
  474. package/templates/skills/chat-sdk/config.json +14 -0
  475. package/templates/skills/chat-sdk/content.md +84 -0
  476. package/templates/skills/composio-gmail/content.md +7 -0
  477. package/templates/skills/composio-google-docs/content.md +1 -1
  478. package/templates/skills/email-domain-health/config.json +14 -0
  479. package/templates/skills/email-domain-health/content.md +131 -0
  480. package/templates/skills/kv-storage/content.md +2 -2
  481. package/templates/skills/pages/content.md +8 -10
  482. package/templates/skills/qa-loom/config.json +14 -0
  483. package/templates/skills/qa-loom/content.md +187 -0
  484. package/templates/skills/qa-loom/files/build-loom.mjs +221 -0
  485. package/templates/skills/qa-loom/files/run-qa.sh +57 -0
  486. package/templates/skills/slack-interaction/content.md +4 -0
  487. package/templates/skills/swarm-extensions/content.md +2 -0
  488. package/templates/skills/swarm-scripts/SKILL.md +4 -2
  489. package/templates/skills/swarm-scripts/config.json +1 -1
  490. package/templates/skills/swarm-scripts/content.md +4 -2
  491. package/templates/skills/workflow-iterate/content.md +38 -0
  492. package/dist/artifact-eczsknc3.js +0 -3843
  493. package/dist/cli-9xsmdpdt.js +0 -56377
  494. package/dist/cli-bd957dgb.js +0 -153
  495. package/dist/cli-q51yprfq.js +0 -1104
  496. package/dist/cli-t1ze64h3.js +0 -811
  497. package/dist/index-9ch1n761.js +0 -1102
  498. package/src/artifact-sdk/index.ts +0 -2
  499. package/src/artifact-sdk/localtunnel.d.ts +0 -20
  500. package/src/artifact-sdk/port.ts +0 -12
  501. package/src/artifact-sdk/server.ts +0 -183
  502. package/src/artifact-sdk/tunnel.ts +0 -19
  503. package/src/commands/artifact.ts +0 -249
  504. package/src/http/ecosystem.ts +0 -77
  505. package/src/tests/artifact-commands.test.ts +0 -92
  506. package/src/tests/artifact-sdk.test.ts +0 -793
  507. package/src/tests/register-service-security.test.ts +0 -194
  508. package/src/tools/list-services.ts +0 -107
  509. package/src/tools/register-service.ts +0 -177
  510. package/src/tools/unregister-service.ts +0 -73
  511. package/src/tools/update-service-status.ts +0 -96
  512. package/templates/skills/artifacts/config.json +0 -14
  513. package/templates/skills/artifacts/content.md +0 -344
  514. package/templates/skills/artifacts/files/examples/approval-flow.ts +0 -34
  515. package/templates/skills/artifacts/files/examples/hono-dashboard.ts +0 -31
  516. package/templates/skills/artifacts/files/examples/multi-artifact.ts +0 -20
  517. package/templates/skills/artifacts/files/examples/static-report.sh +0 -17
  518. package/dist/{azure-openai-responses-ap1gwxgr.js → azure-openai-responses-ygfreq4t.js} +5 -5
  519. package/dist/{cli-5rqrj7q0.js → cli-pw7ebjdt.js} +0 -0
  520. package/dist/{openai-codex-responses-33qhkkdy.js → openai-codex-responses-3bk0grnr.js} +4 -4
  521. package/dist/{openai-completions-s88xm3ns.js → openai-completions-5d4jpakc.js} +9 -9
  522. package/dist/{openai-responses-7xd2f2e2.js → openai-responses-krb51f59.js} +5 -5
  523. package/dist/{pi-messages-xszk613n.js → pi-messages-x9gzynm1.js} +3 -3
  524. /package/src/{artifact-sdk → pages}/browser-sdk.ts +0 -0
  525. /package/src/{artifact-sdk → pages}/feedback-overlay.ts +0 -0
@@ -0,0 +1,309 @@
1
+ import type { WebClient } from "@slack/web-api";
2
+ import { claimKv, deleteKv, sweepExpiredKv, upsertKv } from "../be/db";
3
+ import { isEnvFlagEnabled } from "../utils/env-flag";
4
+ import { scrubSecrets } from "../utils/secret-scrubber";
5
+ import { slackCode, slackMissingScopeMessage } from "./channel-join";
6
+
7
+ /**
8
+ * External (Slack Connect) channel detection and enforcement.
9
+ *
10
+ * A channel is external when `conversations.info` returns `is_ext_shared` or
11
+ * `is_pending_ext_shared`, or when the event envelope carries
12
+ * `is_ext_shared_channel: true`. `is_shared` / `is_org_shared` alone are
13
+ * internal (Enterprise Grid org-shared channels).
14
+ *
15
+ * `SLACK_BLOCK_EXTERNAL_CHANNELS` is read at call time so a dashboard change
16
+ * applies without a restart.
17
+ */
18
+
19
+ export type ChannelExternality = "external" | "internal" | "unknown";
20
+ export type ExternalChannelTrigger = "joined" | "shared" | "boot-scan";
21
+ /** Why a channel is blocked: Slack says it is external, or Slack could not confirm it is not. */
22
+ export type ChannelBlockReason = "external" | "unverified";
23
+
24
+ const EXTERNALITY_TTL_MS = 10 * 60 * 1000;
25
+ const EXTERNALITY_CACHE_MAX = 500;
26
+ const ALERT_DEDUPE_MS = 60 * 60 * 1000;
27
+ /** A reservation whose post never settles (crash mid-post) frees the key after this. */
28
+ const ALERT_SENDING_TTL_MS = 2 * 60 * 1000;
29
+ /** Durable alert dedupe in kv_entries, so a restart does not repeat alerts. */
30
+ export const EXTERNAL_ALERT_KV_NAMESPACE = "slack-external-alert";
31
+
32
+ const externalityCache = new Map<string, { value: "external" | "internal"; expiresAt: number }>();
33
+ // Bumped on every invalidation or positive external evidence. A lookup that
34
+ // started under an older generation must not overwrite the newer state.
35
+ const channelGeneration = new Map<string, number>();
36
+ let generationCounter = 0;
37
+
38
+ export const EXTERNAL_CHANNEL_BLOCKED_REPLY =
39
+ "This channel is shared with another organization. The swarm does not work here.";
40
+
41
+ export function externalChannelToolError(
42
+ channelId: string,
43
+ reason: ChannelBlockReason = "external",
44
+ ): string {
45
+ if (reason === "unverified") {
46
+ return `Could not verify that channel ${channelId} is internal to this organization. SLACK_BLOCK_EXTERNAL_CHANNELS blocks swarm messages there until Slack confirms it. Try again later.`;
47
+ }
48
+ return `Channel ${channelId} is shared with another organization. SLACK_BLOCK_EXTERNAL_CHANNELS blocks swarm messages there.`;
49
+ }
50
+
51
+ export function isBlockExternalEnabled(): boolean {
52
+ return isEnvFlagEnabled("SLACK_BLOCK_EXTERNAL_CHANNELS", false);
53
+ }
54
+
55
+ /** Reset in-memory caches between tests. Durable alert state lives in kv_entries. */
56
+ export function resetExternalChannelStateForTesting(): void {
57
+ externalityCache.clear();
58
+ channelGeneration.clear();
59
+ }
60
+
61
+ /** Insert into an insertion-ordered Map, evicting the oldest entry at the cap. */
62
+ function setBounded<V>(map: Map<string, V>, key: string, value: V): void {
63
+ map.delete(key);
64
+ if (map.size >= EXTERNALITY_CACHE_MAX) {
65
+ const oldest = map.keys().next().value;
66
+ if (oldest !== undefined) map.delete(oldest);
67
+ }
68
+ map.set(key, value);
69
+ }
70
+
71
+ function bumpGeneration(channelId: string): void {
72
+ generationCounter += 1;
73
+ setBounded(channelGeneration, channelId, generationCounter);
74
+ }
75
+
76
+ export function invalidateChannelExternality(channelId: string): void {
77
+ externalityCache.delete(channelId);
78
+ bumpGeneration(channelId);
79
+ }
80
+
81
+ /**
82
+ * Records positive evidence that a channel is external (for example an event
83
+ * envelope with `is_ext_shared_channel: true`), so later outbound checks block
84
+ * without waiting for the cached internal value to expire.
85
+ */
86
+ export function recordExternalChannel(channelId: string): void {
87
+ bumpGeneration(channelId);
88
+ setBounded(externalityCache, channelId, {
89
+ value: "external",
90
+ expiresAt: Date.now() + EXTERNALITY_TTL_MS,
91
+ });
92
+ }
93
+
94
+ /**
95
+ * Looks up whether a channel is external. Known results are cached for 10
96
+ * minutes; an API error returns "unknown" and is not cached. A result whose
97
+ * channel was invalidated or marked external while the lookup was in flight
98
+ * is not cached: the newer state wins, and an invalidated lookup returns
99
+ * "unknown".
100
+ */
101
+ export async function getChannelExternality(
102
+ client: WebClient,
103
+ channelId: string,
104
+ ): Promise<ChannelExternality> {
105
+ const cached = externalityCache.get(channelId);
106
+ if (cached && cached.expiresAt > Date.now()) return cached.value;
107
+ if (cached) externalityCache.delete(channelId);
108
+
109
+ const generation = channelGeneration.get(channelId);
110
+ let value: "external" | "internal";
111
+ try {
112
+ const resp = await client.conversations.info({ channel: channelId });
113
+ const ch = (resp.channel ?? {}) as {
114
+ is_ext_shared?: boolean;
115
+ is_pending_ext_shared?: boolean;
116
+ };
117
+ value =
118
+ ch.is_ext_shared === true || ch.is_pending_ext_shared === true ? "external" : "internal";
119
+ } catch (error) {
120
+ console.warn(
121
+ `[Slack] conversations.info failed for ${channelId}:`,
122
+ scrubSecrets(error instanceof Error ? error.message : String(error)),
123
+ );
124
+ return "unknown";
125
+ }
126
+
127
+ if (channelGeneration.get(channelId) !== generation) {
128
+ const fresher = externalityCache.get(channelId);
129
+ if (fresher && fresher.expiresAt > Date.now()) return fresher.value;
130
+ return value === "external" ? "external" : "unknown";
131
+ }
132
+ setBounded(externalityCache, channelId, { value, expiresAt: Date.now() + EXTERNALITY_TTL_MS });
133
+ return value;
134
+ }
135
+
136
+ /**
137
+ * Returns why the swarm must not touch a channel, or null when it may. With the
138
+ * flag on, a channel Slack cannot classify is blocked as "unverified" (fail
139
+ * closed): disclosing swarm output across an unverified boundary is worse than
140
+ * a temporary refusal.
141
+ */
142
+ export async function getChannelBlockReason(
143
+ client: WebClient,
144
+ channelId: string,
145
+ envelopeFlag?: boolean,
146
+ ): Promise<ChannelBlockReason | null> {
147
+ if (!isBlockExternalEnabled()) return null;
148
+ if (envelopeFlag === true) {
149
+ recordExternalChannel(channelId);
150
+ return "external";
151
+ }
152
+ const externality = await getChannelExternality(client, channelId);
153
+ if (externality === "external") return "external";
154
+ if (externality === "unknown") {
155
+ console.warn(
156
+ `[Slack] Could not tell if channel ${channelId} is external; blocking (fail closed)`,
157
+ );
158
+ return "unverified";
159
+ }
160
+ return null;
161
+ }
162
+
163
+ /** True when the flag is on and the channel is external or cannot be verified. */
164
+ export async function shouldBlockChannel(
165
+ client: WebClient,
166
+ channelId: string,
167
+ envelopeFlag?: boolean,
168
+ ): Promise<boolean> {
169
+ return (await getChannelBlockReason(client, channelId, envelopeFlag)) !== null;
170
+ }
171
+
172
+ async function postExternalChannelAlert(
173
+ client: WebClient,
174
+ channelId: string,
175
+ trigger: ExternalChannelTrigger,
176
+ leave: string,
177
+ scopeHint?: string,
178
+ ): Promise<void> {
179
+ const alertsChannel = process.env.SLACK_ALERTS_CHANNEL?.trim();
180
+ if (!alertsChannel) {
181
+ console.warn("[Slack] SLACK_ALERTS_CHANNEL not set; skipping external channel alert");
182
+ return;
183
+ }
184
+
185
+ // Durable dedupe: reserve the key briefly, extend it to an hour only after
186
+ // Slack accepts the post, and release it when the post fails.
187
+ const key = `${channelId}:${trigger}`;
188
+ if (!(await reserveAlert(key))) return;
189
+
190
+ const lines = [
191
+ `:no_entry: Swarm bot in external Slack channel <#${channelId}> (${channelId})`,
192
+ `Trigger: ${trigger}`,
193
+ `Leave: ${leave}`,
194
+ ...(scopeHint ? [scopeHint] : []),
195
+ "Inbound guard: active. The swarm creates no task from this channel.",
196
+ ];
197
+
198
+ try {
199
+ await client.chat.postMessage({
200
+ channel: alertsChannel,
201
+ text: scrubSecrets(lines.join("\n")),
202
+ unfurl_links: false,
203
+ unfurl_media: false,
204
+ });
205
+ } catch (error) {
206
+ console.error(
207
+ "[Slack] Failed to send external channel alert:",
208
+ scrubSecrets(error instanceof Error ? error.message : String(error)),
209
+ );
210
+ await settleAlert(key, false);
211
+ return;
212
+ }
213
+ await settleAlert(key, true);
214
+ }
215
+
216
+ /** True when this caller may send the alert. A DB failure sends rather than suppresses. */
217
+ async function reserveAlert(key: string): Promise<boolean> {
218
+ try {
219
+ return await claimKv({
220
+ namespace: EXTERNAL_ALERT_KV_NAMESPACE,
221
+ key,
222
+ value: "sending",
223
+ valueType: "string",
224
+ expiresAt: Date.now() + ALERT_SENDING_TTL_MS,
225
+ });
226
+ } catch (error) {
227
+ console.warn("[Slack] External channel alert dedupe unavailable:", error);
228
+ return true;
229
+ }
230
+ }
231
+
232
+ async function settleAlert(key: string, sent: boolean): Promise<void> {
233
+ try {
234
+ if (sent) {
235
+ await upsertKv({
236
+ namespace: EXTERNAL_ALERT_KV_NAMESPACE,
237
+ key,
238
+ value: "sent",
239
+ valueType: "string",
240
+ expiresAt: Date.now() + ALERT_DEDUPE_MS,
241
+ });
242
+ } else {
243
+ await deleteKv(EXTERNAL_ALERT_KV_NAMESPACE, key);
244
+ }
245
+ } catch (error) {
246
+ console.warn("[Slack] Failed to record external channel alert state:", error);
247
+ }
248
+ }
249
+
250
+ /** Leaves an external channel and posts one alert with the result. */
251
+ export async function leaveExternalChannel(
252
+ client: WebClient,
253
+ channelId: string,
254
+ reason: "joined" | "shared",
255
+ ): Promise<void> {
256
+ let leave: string;
257
+ let scopeHint: string | undefined;
258
+ try {
259
+ // A bot that is not a member gets {"ok":false,"not_in_channel":true}; treat it as left.
260
+ await client.conversations.leave({ channel: channelId });
261
+ leave = "left";
262
+ } catch (error) {
263
+ const code = slackCode(error);
264
+ const notInChannel =
265
+ code === "not_in_channel" ||
266
+ (error as { data?: { not_in_channel?: boolean } })?.data?.not_in_channel === true;
267
+ if (notInChannel) {
268
+ leave = "left";
269
+ } else {
270
+ leave = `failed (${code ?? "unknown_error"})`;
271
+ if (code === "missing_scope") scopeHint = slackMissingScopeMessage(error);
272
+ console.error(`[Slack] Failed to leave external channel ${channelId}: ${leave}`);
273
+ }
274
+ }
275
+ if (leave === "left") console.log(`[Slack] Left external channel ${channelId} (${reason})`);
276
+ await postExternalChannelAlert(client, channelId, reason, leave, scopeHint);
277
+ }
278
+
279
+ /**
280
+ * Boot scan, report only: posts one alert per external channel the bot is in.
281
+ * It never leaves a channel; a human decides.
282
+ */
283
+ export async function scanExternalChannelsOnBoot(client: WebClient): Promise<void> {
284
+ if (!isBlockExternalEnabled()) return;
285
+
286
+ // kv_entries has no background sweep; drop expired alert keys so the namespace stays bounded.
287
+ await sweepExpiredKv(EXTERNAL_ALERT_KV_NAMESPACE).catch((error) => {
288
+ console.warn("[Slack] Failed to sweep expired external channel alert keys:", error);
289
+ });
290
+
291
+ let cursor: string | undefined;
292
+ do {
293
+ const resp = await client.users.conversations({
294
+ types: "public_channel,private_channel",
295
+ exclude_archived: true,
296
+ limit: 200,
297
+ cursor,
298
+ });
299
+ for (const ch of (resp.channels ?? []) as Array<{
300
+ id?: string;
301
+ is_ext_shared?: boolean;
302
+ is_pending_ext_shared?: boolean;
303
+ }>) {
304
+ if (!ch.id || !(ch.is_ext_shared === true || ch.is_pending_ext_shared === true)) continue;
305
+ await postExternalChannelAlert(client, ch.id, "boot-scan", "not attempted (report only)");
306
+ }
307
+ cursor = resp.response_metadata?.next_cursor || undefined;
308
+ } while (cursor);
309
+ }
@@ -44,6 +44,13 @@ import type { AgentMatch } from "./types";
44
44
  // Side-effect import: registers all Slack event templates in the in-memory registry
45
45
  import "./templates";
46
46
  import { isEnvFlagEnabled } from "../utils/env-flag";
47
+ import {
48
+ getChannelBlockReason,
49
+ invalidateChannelExternality,
50
+ isBlockExternalEnabled,
51
+ leaveExternalChannel,
52
+ shouldBlockChannel,
53
+ } from "./external-channel";
47
54
  import { extractSlackMessageText } from "./message-text";
48
55
  import { ensureSlackThreadTree, isSlackRenderV2Enabled } from "./render-v2";
49
56
  import { formatSlackSteeringAck, requestSlackThreadSteering } from "./steering";
@@ -461,6 +468,18 @@ export function registerMessageHandler(app: App): void {
461
468
  return ignoreSlackInbound("duplicate_message");
462
469
  }
463
470
 
471
+ // Block external (Slack Connect) channels when SLACK_BLOCK_EXTERNAL_CHANNELS is on.
472
+ if (
473
+ await shouldBlockChannel(
474
+ client,
475
+ msg.channel,
476
+ (body as { is_ext_shared_channel?: boolean }).is_ext_shared_channel,
477
+ )
478
+ ) {
479
+ console.log(`[Slack] Ignoring message in external channel ${msg.channel}`);
480
+ return ignoreSlackInbound("external_channel");
481
+ }
482
+
464
483
  // Check user authorization
465
484
  if (!(await isUserAllowed(client, msg.user))) {
466
485
  console.log(`[Slack] Ignoring message from unauthorized user ${msg.user}`);
@@ -1018,4 +1037,46 @@ export function registerMessageHandler(app: App): void {
1018
1037
  // but we can add specific behavior here if needed
1019
1038
  console.log(`[Slack] App mentioned in channel ${event.channel}`);
1020
1039
  });
1040
+
1041
+ // External (Slack Connect) channels: leave when the bot joins one, or when a
1042
+ // channel the bot is in becomes shared. All no-ops unless
1043
+ // SLACK_BLOCK_EXTERNAL_CHANNELS is on.
1044
+ app.event("member_joined_channel", async ({ event, client }) => {
1045
+ if (!isBlockExternalEnabled()) return ignoreSlackInbound("external_block_disabled");
1046
+
1047
+ if (!cachedBotUserId) {
1048
+ try {
1049
+ const authResult = await client.auth.test();
1050
+ cachedBotUserId = authResult.user_id as string;
1051
+ cachedBotId = (authResult.bot_id as string | undefined) ?? null;
1052
+ } catch (error) {
1053
+ console.error("[Slack] Failed to cache bot user ID:", error);
1054
+ }
1055
+ }
1056
+ if (!cachedBotUserId || event.user !== cachedBotUserId) {
1057
+ return ignoreSlackInbound("member_joined_not_bot");
1058
+ }
1059
+
1060
+ invalidateChannelExternality(event.channel);
1061
+ // Leave only on positive evidence; an unverified channel stays blocked by the guards.
1062
+ if ((await getChannelBlockReason(client, event.channel)) !== "external") {
1063
+ return ignoreSlackInbound("member_joined_internal");
1064
+ }
1065
+ await leaveExternalChannel(client, event.channel, "joined");
1066
+ });
1067
+
1068
+ app.event("channel_shared", async ({ event, client }) => {
1069
+ if (!isBlockExternalEnabled()) return ignoreSlackInbound("external_block_disabled");
1070
+
1071
+ invalidateChannelExternality(event.channel);
1072
+ if ((await getChannelBlockReason(client, event.channel)) !== "external") {
1073
+ return ignoreSlackInbound("channel_shared_internal");
1074
+ }
1075
+ await leaveExternalChannel(client, event.channel, "shared");
1076
+ });
1077
+
1078
+ app.event("channel_unshared", async ({ event }) => {
1079
+ if (!isBlockExternalEnabled()) return ignoreSlackInbound("external_block_disabled");
1080
+ invalidateChannelExternality(event.channel);
1081
+ });
1021
1082
  }
@@ -961,14 +961,16 @@ export async function childOutcomeContent(
961
961
  );
962
962
  }
963
963
 
964
- // A deferred wake continues a human conversation even though the scheduler
965
- // assigns source="schedule". Its answer must not depend on delegation cards.
966
- function isSlackContinuation(task: AgentTask): boolean {
964
+ // A lead deferred wake continues the human conversation. Worker wakes report
965
+ // through their lead follow-up instead of posting their own outcome cards.
966
+ async function isSlackContinuation(task: AgentTask): Promise<boolean> {
967
967
  return (
968
968
  task.source === "schedule" &&
969
969
  task.taskType === "deferred" &&
970
970
  !!task.slackChannelId &&
971
- !!task.slackThreadTs
971
+ !!task.slackThreadTs &&
972
+ !!task.agentId &&
973
+ !!(await getAgentById(task.agentId))?.isLead
972
974
  );
973
975
  }
974
976
 
@@ -981,7 +983,7 @@ function isSlackContinuation(task: AgentTask): boolean {
981
983
  function isChildCardCandidate(task: AgentTask, delegationActivatedAt: string): boolean {
982
984
  return (
983
985
  task.source !== "slack" &&
984
- !isSlackContinuation(task) &&
986
+ !(task.source === "schedule" && task.taskType === "deferred") &&
985
987
  !!task.slackChannelId &&
986
988
  !!task.slackThreadTs &&
987
989
  (task.status === "completed" || task.status === "failed") &&
@@ -991,10 +993,10 @@ function isChildCardCandidate(task: AgentTask, delegationActivatedAt: string): b
991
993
  );
992
994
  }
993
995
 
994
- function taskNeedsDirectOutcome(task: AgentTask, activatedAt: string): boolean {
996
+ async function taskNeedsDirectOutcome(task: AgentTask, activatedAt: string): Promise<boolean> {
995
997
  return (
996
998
  ((task.source === "slack" && isAskOutcomeStatus(task.status)) ||
997
- (isSlackContinuation(task) && isOutcomeStatus(task.status))) &&
999
+ ((await isSlackContinuation(task)) && isOutcomeStatus(task.status))) &&
998
1000
  task.createdAt >= activatedAt
999
1001
  );
1000
1002
  }
@@ -1431,48 +1433,34 @@ export async function streamOutcomeCard(
1431
1433
  });
1432
1434
  }
1433
1435
 
1434
- /**
1435
- * The body a resolved deferral card is rewritten to. The ⏳ block promised
1436
- * the thread an answer; this is that promise being kept in place, instead of
1437
- * in a second card below it — `isAnsweredByDeferralCard` keeps the wake-up
1438
- * from posting one, so the answer lands in the thread exactly once. It is
1439
- * the card the wake-up would have posted, collapsed the same way when the
1440
- * wake-up already answered by hand with `slack-reply`.
1441
- *
1442
- * A wake-up that deferred again does post its own card — the next ⏳ in the
1443
- * chain, which its own wake-up resolves — so this card only points at it.
1444
- */
1436
+ /** Render the latest wait or final result into the chain's original card. */
1445
1437
  async function resolvedDeferralContent(
1446
1438
  wake: AgentTask,
1447
1439
  ): Promise<{ text: string; blocks?: unknown[] }> {
1448
- if (!isDeferredTask(wake)) {
1449
- const content = await outcomeContent(wake, wake.slackReplySent);
1450
- const presentation = await outcomePresentation(wake, content);
1451
- const captions = await outcomeCaptionBlocks(wake, content);
1452
- return { text: presentation, blocks: [{ type: "markdown", text: presentation }, ...captions] };
1453
- }
1454
- const card = await getSlackOutcomeMessage(wake.id);
1455
- const pointer = card?.permalink ? ` — <${card.permalink}|next update>` : ".";
1456
- return { text: `↪️ Resumed by ${await agentDisplayNameFor(wake)} and deferred again${pointer}` };
1440
+ const content = await outcomeContent(wake, wake.slackReplySent);
1441
+ const presentation = await outcomePresentation(wake, content);
1442
+ const captions = await outcomeCaptionBlocks(wake, content);
1443
+ return { text: presentation, blocks: [{ type: "markdown", text: presentation }, ...captions] };
1457
1444
  }
1458
1445
 
1459
- /**
1460
- * True when `task` is a wake-up whose answer goes into its deferral's ⏳ card
1461
- * (`refreshResolvedDeferralCards`), so it must not post an outcome card of
1462
- * its own. False — and the wake-up posts its own card as before — when it
1463
- * deferred again, when the deferral never got a card, and when the in-place
1464
- * rewrite was abandoned.
1465
- */
1446
+ /** A continuation uses the first retained, usable deferral card in its chain. */
1466
1447
  async function isAnsweredByDeferralCard(
1467
1448
  task: AgentTask,
1468
1449
  threadTasks: readonly AgentTask[],
1469
1450
  ): Promise<boolean> {
1470
- if (!isSlackContinuation(task) || !task.parentTaskId || isDeferredTask(task)) return false;
1471
- const deferred = threadTasks.find((candidate) => candidate.id === task.parentTaskId);
1472
- // Mirrors getResolvableDeferralOutcomes: only a `deferredAt` card is rewritten.
1473
- if (!deferred?.deferredAt) return false;
1474
- const card = await getSlackOutcomeMessage(deferred.id);
1475
- return !!card?.finalizedAt && !isPendingSlackMessage(card) && !card.deferralAbandonedAt;
1451
+ const seen = new Set<string>();
1452
+ let current = task;
1453
+ while ((await isSlackContinuation(current)) && current.parentTaskId && !seen.has(current.id)) {
1454
+ seen.add(current.id);
1455
+ const deferred = threadTasks.find((candidate) => candidate.id === current.parentTaskId);
1456
+ if (!deferred?.deferredAt) return false;
1457
+ const card = await getSlackOutcomeMessage(deferred.id);
1458
+ if (card) {
1459
+ return !!card.finalizedAt && !isPendingSlackMessage(card) && !card.deferralAbandonedAt;
1460
+ }
1461
+ current = deferred;
1462
+ }
1463
+ return false;
1476
1464
  }
1477
1465
 
1478
1466
  /**
@@ -1490,6 +1478,8 @@ const TRANSIENT_SLACK_ERROR_CODES = new Set([
1490
1478
  "service_unavailable",
1491
1479
  "fatal_error",
1492
1480
  "request_timeout",
1481
+ // The external-channel guard could not classify the channel; Slack may answer next time.
1482
+ "external_channel_unverified",
1493
1483
  ]);
1494
1484
 
1495
1485
  export function slackErrorCode(error: unknown): string | undefined {
@@ -1539,9 +1529,8 @@ const deferralRefreshAttempts = new Map<string, number>();
1539
1529
  *
1540
1530
  * Runs after the tree loop and independently of it: resolution needs only
1541
1531
  * the retained outcome `ts`, so it must not be skipped when the thread has
1542
- * no other pending render work. After, so a wake-up that deferred again has
1543
- * already posted the card this one points at. Each card is rewritten exactly
1544
- * once (`deferral_resolved_at`).
1532
+ * no other pending render work. Re-defers update the same card; the final
1533
+ * result marks it resolved (`deferral_resolved_at`).
1545
1534
  *
1546
1535
  * The marker is also burned when Slack refuses the rewrite for good, and when
1547
1536
  * an unclassifiable failure has used up its attempts — a card that can never
@@ -1562,10 +1551,14 @@ export async function refreshResolvedDeferralCards(): Promise<void> {
1562
1551
  ts: card.ts,
1563
1552
  ...content,
1564
1553
  });
1565
- await markSlackDeferralResolved(card.id);
1554
+ if (isDeferredTask(wake)) {
1555
+ await updateSlackMessageRecord(card.id, {});
1556
+ } else {
1557
+ await markSlackDeferralResolved(card.id);
1558
+ }
1566
1559
  deferralRefreshAttempts.delete(card.id);
1567
1560
  // The rewrite is the wake-up's outcome, so it settles the ask's reaction.
1568
- await finalizeTerminalSlackReactions([wake]);
1561
+ if (!isDeferredTask(wake)) await finalizeTerminalSlackReactions([wake]);
1569
1562
  } catch (error) {
1570
1563
  const attempts = (deferralRefreshAttempts.get(card.id) ?? 0) + 1;
1571
1564
  deferralRefreshAttempts.set(card.id, attempts);
@@ -1652,7 +1645,7 @@ export async function processSlackRenderV2(): Promise<void> {
1652
1645
  // not represent immediately active outcome work, so avoid waking an old
1653
1646
  // tree solely to verify its permalink on every render tick.
1654
1647
  const isDirectCandidate =
1655
- taskNeedsDirectOutcome(task, activatedAt) &&
1648
+ (await taskNeedsDirectOutcome(task, activatedAt)) &&
1656
1649
  isOutcomeStatus(task.status) &&
1657
1650
  !(await isAnsweredByDeferralCard(task, tasks));
1658
1651
  const isChildCandidate =
@@ -1751,7 +1744,7 @@ export async function processSlackRenderV2(): Promise<void> {
1751
1744
 
1752
1745
  for (const task of tasks) {
1753
1746
  if (!isSlackRenderV2Enabled()) return;
1754
- if (!taskNeedsDirectOutcome(task, activatedAt)) continue;
1747
+ if (!(await taskNeedsDirectOutcome(task, activatedAt))) continue;
1755
1748
  const card = await getSlackOutcomeMessage(task.id);
1756
1749
  if (card && isSettledSlackMessage(card)) continue;
1757
1750
  if (await isAnsweredByDeferralCard(task, tasks)) continue;
@@ -3,6 +3,7 @@ import type { EntityPresentDetailsArguments, WebClient } from "@slack/web-api";
3
3
  import { getAgentById, getTaskById } from "../be/db";
4
4
  import { isEnvFlagEnabled } from "../utils/env-flag";
5
5
  import { getTaskUrl } from "./blocks";
6
+ import { shouldBlockChannel } from "./external-channel";
6
7
  import { isUserAllowed } from "./handlers";
7
8
  import { slackTaskOutput } from "./task-output";
8
9
 
@@ -30,6 +31,10 @@ async function getDetails(event: DetailsEvent, client: WebClient): Promise<Detai
30
31
  return { error: { status: "restricted" } };
31
32
  }
32
33
 
34
+ if (await shouldBlockChannel(client, event.channel)) {
35
+ return { error: { status: "restricted" } };
36
+ }
37
+
33
38
  const task = await getTaskById(ref.id);
34
39
  if (!task) return { error: { status: "not_found" } };
35
40
 
@@ -22,8 +22,10 @@
22
22
  * the underlying context becomes irrelevant — e.g. user cancels).
23
23
  *
24
24
  * Concurrency: single-process, single-event-loop. No cross-instance locking.
25
- * Flush callbacks run sequentially per key (the buffer is cleared BEFORE the
26
- * callback fires, so re-enqueues during `onFlush` create a fresh buffer).
25
+ * The buffer is cleared BEFORE the callback fires, so re-enqueues during
26
+ * `onFlush` create a fresh buffer, and its flush can start while the earlier
27
+ * `onFlush` for the same key still runs. A caller that needs per-key order
28
+ * must serialize inside `onFlush`.
27
29
  */
28
30
 
29
31
  export type BufferFlushReason = "timer" | "manual";