@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
@@ -542,7 +542,12 @@ describe("harness compatibility at every create entry point", () => {
542
542
  });
543
543
 
544
544
  test("send-task: the parent auto-route target is judged", async () => {
545
- const parent = await createTaskExtended("codex parent", { agentId: codexId, source: "mcp" });
545
+ // The worker created the parent, so it may continue it (task.parent.attach).
546
+ const parent = await createTaskExtended("codex parent", {
547
+ agentId: codexId,
548
+ creatorAgentId: workerId,
549
+ source: "mcp",
550
+ });
546
551
  const refused = await sendTaskHandler(ownerCtx({ agentId: workerId }), {
547
552
  task: "child of a codex task",
548
553
  parentTaskId: parent.id,
@@ -662,3 +667,102 @@ describe("harness compatibility at every create entry point", () => {
662
667
  expect(String(res.body.error)).toContain("does not run on the codex harness");
663
668
  });
664
669
  });
670
+
671
+ describe("nan and helmcode catalog models", () => {
672
+ let piId = "";
673
+ let opencodeId = "";
674
+
675
+ beforeAll(async () => {
676
+ piId = (
677
+ await createAgent({
678
+ name: "validation-pi",
679
+ isLead: false,
680
+ status: "idle",
681
+ harnessProvider: "pi",
682
+ })
683
+ ).id;
684
+ opencodeId = (
685
+ await createAgent({
686
+ name: "validation-opencode",
687
+ isLead: false,
688
+ status: "idle",
689
+ harnessProvider: "opencode",
690
+ })
691
+ ).id;
692
+ });
693
+
694
+ const ids = [
695
+ "nan/glm5.3-flash",
696
+ "nan/glm5.3",
697
+ "nan/mimo-v2.6-flash",
698
+ "helmcode/glm5.3",
699
+ "helmcode/qwen3.6",
700
+ "helmcode/deepseek-v4-flash",
701
+ ];
702
+
703
+ test("pi and opencode accept the ids without allowCustomModel", async () => {
704
+ for (const harnessProvider of ["pi", "opencode"]) {
705
+ for (const model of ids) {
706
+ expect(await explicitModelError({ model, harnessProvider })).toBeNull();
707
+ }
708
+ }
709
+ });
710
+
711
+ test("claude and codex reject them with the harness mismatch, even with allowCustomModel", async () => {
712
+ for (const harnessProvider of ["claude", "codex"]) {
713
+ for (const allowCustomModel of [false, true]) {
714
+ for (const model of ["nan/glm5.3-flash", "helmcode/glm5.3"]) {
715
+ expect(await explicitModelError({ model, harnessProvider, allowCustomModel })).toContain(
716
+ `does not run on the ${harnessProvider} harness`,
717
+ );
718
+ }
719
+ }
720
+ }
721
+ });
722
+
723
+ test("a misspelt helmcode id is unknown", async () => {
724
+ expect(
725
+ await explicitModelError({ model: "helmcode/glm-5.3", harnessProvider: "pi" }),
726
+ ).toContain("Unknown model");
727
+ });
728
+
729
+ test("send-task: pi and opencode agents take the ids, a claude agent gets the mismatch", async () => {
730
+ const sender = ownerCtx({ agentId: leadId });
731
+ const args = {
732
+ task: "nan route",
733
+ offerMode: false,
734
+ allowDuplicate: false,
735
+ routingReason: "human_pinned" as const,
736
+ routingNote: "pinned to check the nan/helmcode catalog ids",
737
+ };
738
+ for (const agentId of [piId, opencodeId]) {
739
+ for (const model of ["nan/mimo-v2.6-flash", "helmcode/glm5.3"]) {
740
+ const accepted = await sendTaskHandler(sender, {
741
+ ...args,
742
+ task: `nan route ${model} to ${agentId}`,
743
+ agentId,
744
+ model,
745
+ });
746
+ expect({ ok: accepted.ok, message: accepted.message }).toEqual({
747
+ ok: true,
748
+ message: expect.any(String),
749
+ });
750
+ }
751
+ }
752
+ const refused = await sendTaskHandler(sender, {
753
+ ...args,
754
+ agentId: workerId,
755
+ model: "helmcode/glm5.3",
756
+ });
757
+ expect(refused.ok).toBe(false);
758
+ expect(refused.message).toContain("does not run on the claude harness");
759
+ });
760
+
761
+ test("PATCH /api/agents/:id/runtime stores a helmcode model on a pi agent", async () => {
762
+ const res = await api("PATCH", `/api/agents/${piId}/runtime`, {
763
+ harness_provider: "pi",
764
+ model: "helmcode/glm5.3",
765
+ });
766
+ expect(res.status).toBe(200);
767
+ });
768
+ });
@@ -0,0 +1,154 @@
1
+ import { afterAll, beforeAll, beforeEach, describe, expect, spyOn, test } from "bun:test";
2
+ import { unlink } from "node:fs/promises";
3
+ import { closeDb, createAgent, getDbClient, getKv, initDb } from "../be/db";
4
+ import { installExtension, listExtensionRuns } from "../be/extensions/db";
5
+ import { enableExtension, stopExtensionRuntime } from "../extensions/lifecycle";
6
+ import { chatSdkContextKey } from "../tasks/context-key";
7
+ import type { Agent } from "../types";
8
+ import {
9
+ flushChatBuffers,
10
+ postTelegramUpdate,
11
+ startTelegramHarness,
12
+ type TelegramHarness,
13
+ telegramUpdate,
14
+ } from "./chat-sdk-harness";
15
+ import { loadBundleFixture } from "./fixtures/extensions/load";
16
+
17
+ const TEST_DB_PATH = "./test-extensions-example-route-chat-channel.sqlite";
18
+
19
+ let harness: TelegramHarness;
20
+ let lead: Agent;
21
+ let worker: Agent;
22
+
23
+ async function removeDbFiles(): Promise<void> {
24
+ for (const suffix of ["", "-wal", "-shm"]) {
25
+ await unlink(`${TEST_DB_PATH}${suffix}`).catch(() => {});
26
+ }
27
+ }
28
+
29
+ async function chatTask(chatId: number) {
30
+ return getDbClient().get<{ agentId: string | null }>(
31
+ "SELECT agentId FROM agent_tasks WHERE source = 'chat' AND contextKey = ? ORDER BY createdAt DESC LIMIT 1",
32
+ [chatSdkContextKey(`telegram:${chatId}`)],
33
+ );
34
+ }
35
+
36
+ /** A private-chat message, flushed out of the thread buffer. */
37
+ async function sendDm(chatId: number, text: string): Promise<void> {
38
+ await postTelegramUpdate(telegramUpdate({ id: chatId, type: "private" }, text));
39
+ await flushChatBuffers();
40
+ }
41
+
42
+ async function installRouteFixture(channelId: string, agentId: string) {
43
+ const bundle = await loadBundleFixture("route-chat-channel-to-agent");
44
+ const { extension } = await installExtension({
45
+ ...bundle,
46
+ config: { adapter: "telegram", channelId, agentId },
47
+ });
48
+ return { extension, enabled: await enableExtension(extension.id) };
49
+ }
50
+
51
+ describe("extension Chat SDK channel routing", () => {
52
+ beforeAll(async () => {
53
+ await removeDbFiles();
54
+ initDb(TEST_DB_PATH);
55
+ lead = await createAgent({
56
+ id: crypto.randomUUID(),
57
+ name: "chat-route-lead",
58
+ isLead: true,
59
+ status: "idle",
60
+ });
61
+ worker = await createAgent({
62
+ id: crypto.randomUUID(),
63
+ name: "chat-route-worker",
64
+ isLead: false,
65
+ status: "idle",
66
+ });
67
+ harness = await startTelegramHarness();
68
+ });
69
+
70
+ afterAll(async () => {
71
+ await harness.stop();
72
+ await stopExtensionRuntime();
73
+ closeDb();
74
+ await removeDbFiles();
75
+ });
76
+
77
+ beforeEach(async () => {
78
+ await stopExtensionRuntime();
79
+ await getDbClient().run("DELETE FROM extensions");
80
+ });
81
+
82
+ test("routes the configured channel to its agent and leaves other channels with the lead", async () => {
83
+ const { extension } = await installRouteFixture("telegram:3001", worker.id);
84
+
85
+ await sendDm(3001, "route this to the worker");
86
+ expect(await chatTask(3001)).toEqual({ agentId: worker.id });
87
+ expect(
88
+ (await listExtensionRuns(extension.id)).some(
89
+ (run) => run.event === "pre.chat.route" && run.action === "modify",
90
+ ),
91
+ ).toBe(true);
92
+
93
+ await sendDm(3002, "use the normal route");
94
+ expect(await chatTask(3002)).toEqual({ agentId: lead.id });
95
+ });
96
+
97
+ test("a block creates no task", async () => {
98
+ const bundle = await loadBundleFixture("minimal");
99
+ const { extension } = await installExtension({
100
+ manifest: { ...bundle.manifest, name: "block-chat-route", description: "blocks chat" },
101
+ files: {
102
+ "hooks.ts": `import { block, type SwarmExtension } from "swarm-extension";
103
+ const extension: SwarmExtension = (api) => {
104
+ api.on("pre.chat.route", () => block("chat is muted"));
105
+ };
106
+ export default extension;
107
+ `,
108
+ },
109
+ });
110
+ await enableExtension(extension.id);
111
+
112
+ await sendDm(3003, "this must not become a task");
113
+ expect(await chatTask(3003)).toBeNull();
114
+ const runs = await listExtensionRuns(extension.id);
115
+ expect(runs.some((run) => run.event === "pre.chat.route" && run.action === "block")).toBe(true);
116
+ // Ids only in the run subject: never the message text.
117
+ expect(runs.every((run) => !run.subject?.includes("must not"))).toBe(true);
118
+ });
119
+
120
+ test("falls back to the built-in router when the extension names an unknown agent", async () => {
121
+ await installRouteFixture("telegram:3004", "missing-agent");
122
+ const warn = spyOn(console, "warn").mockImplementation(() => {});
123
+ try {
124
+ await sendDm(3004, "use the fallback");
125
+ expect(await chatTask(3004)).toEqual({ agentId: lead.id });
126
+ expect(warn).toHaveBeenCalledWith(
127
+ "[chat-sdk] Extension selected an unknown agent missing-agent. Using the router.",
128
+ );
129
+ } finally {
130
+ warn.mockRestore();
131
+ }
132
+ });
133
+
134
+ test("post.chat.message carries the inbound sender", async () => {
135
+ const { extension, enabled } = await installRouteFixture("telegram:3005", worker.id);
136
+ await sendDm(3005, "record this message");
137
+
138
+ const deadline = Date.now() + 5_000;
139
+ while (
140
+ !(await listExtensionRuns(extension.id)).some(
141
+ (run) => run.event === "post.chat.message" && run.action === "continue",
142
+ ) &&
143
+ Date.now() < deadline
144
+ ) {
145
+ await Bun.sleep(20);
146
+ }
147
+ expect(
148
+ await getKv(
149
+ `task:agent:${enabled.agentId}`,
150
+ "ext:route-chat-channel-to-agent:chat:telegram:3005",
151
+ ),
152
+ ).toMatchObject({ value: "777001" });
153
+ });
154
+ });
@@ -33,6 +33,7 @@ const NEW_EVENTS = [
33
33
  "post.task.budgetRefused",
34
34
  "post.email.received",
35
35
  "post.kapso.message",
36
+ "post.chat.message",
36
37
  "post.vcs.event",
37
38
  ] as const satisfies readonly (keyof SwarmEventMap)[];
38
39
 
@@ -358,6 +359,56 @@ describe("extension post bridge: new events", () => {
358
359
  });
359
360
  });
360
361
 
362
+ describe("post.chat.message", () => {
363
+ test("chat.message.received dispatches an inbound event", async () => {
364
+ await registerCapture("capture");
365
+ workflowEventBus.emit("chat.message.received", {
366
+ adapter: "telegram",
367
+ channelId: "telegram:555001",
368
+ threadId: "telegram:555001",
369
+ userId: "555001",
370
+ text: "hello swarm",
371
+ messageId: "555001:11",
372
+ });
373
+ await waitForEvents(1);
374
+ expect(received).toEqual([
375
+ {
376
+ extension: "capture",
377
+ event: "post.chat.message",
378
+ payload: {
379
+ adapter: "telegram",
380
+ channelId: "telegram:555001",
381
+ threadId: "telegram:555001",
382
+ userId: "555001",
383
+ text: "hello swarm",
384
+ direction: "inbound",
385
+ messageId: "555001:11",
386
+ },
387
+ },
388
+ ]);
389
+ });
390
+
391
+ test("chat.message.sent dispatches an outbound event", async () => {
392
+ await registerCapture("capture");
393
+ workflowEventBus.emit("chat.message.sent", {
394
+ adapter: "telegram",
395
+ channelId: "telegram:555001",
396
+ threadId: "telegram:555001",
397
+ text: "done",
398
+ taskId: "task-1",
399
+ });
400
+ await waitForEvents(1);
401
+ expect(received[0]?.payload).toEqual({
402
+ adapter: "telegram",
403
+ channelId: "telegram:555001",
404
+ threadId: "telegram:555001",
405
+ text: "done",
406
+ direction: "outbound",
407
+ taskId: "task-1",
408
+ });
409
+ });
410
+ });
411
+
361
412
  describe("post.kapso.message", () => {
362
413
  test("a bus emit dispatches the typed payload", async () => {
363
414
  await registerCapture("capture");
@@ -120,6 +120,95 @@ describe("fetchResolvedEnv", () => {
120
120
  warning.mockRestore();
121
121
  }
122
122
  });
123
+
124
+ test("exportToEnv=false rows stay out of the harness env, even over a boot copy", async () => {
125
+ const agentId = crypto.randomUUID();
126
+ mockResponsesByAgentId.set(agentId, {
127
+ status: 200,
128
+ body: {
129
+ configs: [
130
+ { key: "VERCEL_TOKEN", value: "row-vercel", isSecret: true, exportToEnv: false },
131
+ { key: "GITHUB_TOKEN", value: "row-github", isSecret: true, exportToEnv: true },
132
+ { key: "PUBLIC_SETTING", value: "visible", isSecret: false, exportToEnv: true },
133
+ { key: "HIDDEN_SETTING", value: "hidden", isSecret: false, exportToEnv: false },
134
+ // A pre-215 API sends no flag: the row keeps exporting.
135
+ { key: "LEGACY_SECRET", value: "row-legacy", isSecret: true },
136
+ // Restricted keys never export, whatever the row says.
137
+ { key: "SLACK_APP_TOKEN", value: "row-xapp", isSecret: true, exportToEnv: true },
138
+ ],
139
+ },
140
+ });
141
+ const log = spyOn(console, "log").mockImplementation(() => {});
142
+ try {
143
+ // The base env stands for the container env or an older entrypoint's
144
+ // boot export; a withheld row must remove those copies too.
145
+ const result = await fetchResolvedEnv(testUrl, "key", agentId, {
146
+ VERCEL_TOKEN: "boot-vercel",
147
+ SLACK_APP_TOKEN: "boot-xapp",
148
+ });
149
+ expect(result.env.VERCEL_TOKEN).toBeUndefined();
150
+ expect(result.env.HIDDEN_SETTING).toBeUndefined();
151
+ expect(result.env.SLACK_APP_TOKEN).toBeUndefined();
152
+ expect(result.env.GITHUB_TOKEN).toBe("row-github");
153
+ expect(result.env.PUBLIC_SETTING).toBe("visible");
154
+ expect(result.env.LEGACY_SECRET).toBe("row-legacy");
155
+ const logged = log.mock.calls.map((call) => String(call[0])).join("\n");
156
+ expect(logged).toContain("HIDDEN_SETTING, SLACK_APP_TOKEN, VERCEL_TOKEN");
157
+ expect(logged).not.toContain("row-vercel");
158
+ expect(logged).not.toContain("boot-vercel");
159
+ } finally {
160
+ log.mockRestore();
161
+ }
162
+ });
163
+
164
+ test("a row revoked after boot leaves process.env, so merged adapter envs lose it", async () => {
165
+ const agentId = crypto.randomUUID();
166
+ const key = "REVOKED_AFTER_BOOT_SECRET";
167
+ mockResponsesByAgentId.set(agentId, {
168
+ status: 200,
169
+ body: { configs: [{ key, value: "row-value", isSecret: true, exportToEnv: false }] },
170
+ });
171
+ // The entrypoint exported the row at boot, before it was switched off.
172
+ process.env[key] = "boot-export";
173
+ const log = spyOn(console, "log").mockImplementation(() => {});
174
+ try {
175
+ const result = await fetchResolvedEnv(testUrl, "key", agentId);
176
+ expect(result.env[key]).toBeUndefined();
177
+ expect(process.env[key]).toBeUndefined();
178
+ // The amp, dsh, grok, cursor and claude adapters spawn with this shape.
179
+ expect({ ...process.env, ...result.env }[key]).toBeUndefined();
180
+ } finally {
181
+ delete process.env[key];
182
+ log.mockRestore();
183
+ }
184
+ });
185
+
186
+ test("a masked row keeps its boot value only when the row is exported", async () => {
187
+ const agentId = crypto.randomUUID();
188
+ mockResponsesByAgentId.set(agentId, {
189
+ status: 200,
190
+ body: {
191
+ configs: [
192
+ { key: "EXPORTED_BOOT", value: MASKED_SECRET_VALUE, isSecret: true, exportToEnv: true },
193
+ { key: "WITHHELD_BOOT", value: MASKED_SECRET_VALUE, isSecret: true, exportToEnv: false },
194
+ ],
195
+ },
196
+ });
197
+ const warning = spyOn(console, "warn").mockImplementation(() => {});
198
+ const log = spyOn(console, "log").mockImplementation(() => {});
199
+ try {
200
+ const result = await fetchResolvedEnv(testUrl, "key", agentId, {
201
+ EXPORTED_BOOT: "boot-exported",
202
+ WITHHELD_BOOT: "boot-withheld",
203
+ });
204
+ expect(result.env.EXPORTED_BOOT).toBe("boot-exported");
205
+ expect(result.env.WITHHELD_BOOT).toBeUndefined();
206
+ } finally {
207
+ warning.mockRestore();
208
+ log.mockRestore();
209
+ }
210
+ });
211
+
123
212
  test("repository scope requires the requested repository identity", async () => {
124
213
  const repoId = crypto.randomUUID();
125
214
  let url = "https://github.com/another-owner/fixture";
@@ -305,6 +394,45 @@ describe("fetchResolvedEnv", () => {
305
394
  ]);
306
395
  });
307
396
 
397
+ test("HELMCODE_API_NAN alias becomes NAN_API_KEY and pools for a nan/* model", async () => {
398
+ const agentId = crypto.randomUUID();
399
+ mockResponsesByAgentId.set(agentId, {
400
+ status: 200,
401
+ body: { configs: [{ key: "HELMCODE_API_NAN", value: "fixture-nan-a,fixture-nan-b" }] },
402
+ });
403
+ const result = await fetchResolvedEnv(testUrl, "key", agentId, {}, "nan/glm5.3-flash", {
404
+ provider: "opencode",
405
+ });
406
+ expect(result.env.NAN_API_KEY).toMatch(/^fixture-nan-[ab]$/);
407
+ expect(result.credentialSelections.map((selection) => selection.keyType)).toEqual([
408
+ "NAN_API_KEY",
409
+ ]);
410
+ });
411
+
412
+ test("an explicit NAN_API_KEY wins over the HELMCODE_API_NAN alias", async () => {
413
+ const agentId = crypto.randomUUID();
414
+ mockResponsesByAgentId.set(agentId, {
415
+ status: 200,
416
+ body: {
417
+ configs: [
418
+ { key: "NAN_API_KEY", value: "fixture-nan-canonical" },
419
+ { key: "HELMCODE_API_NAN", value: "fixture-nan-alias" },
420
+ ],
421
+ },
422
+ });
423
+ const result = await fetchResolvedEnv(testUrl, "key", agentId, {}, "nan/qwen3.6", {
424
+ provider: "pi",
425
+ });
426
+ expect(result.env.NAN_API_KEY).toBe("fixture-nan-canonical");
427
+ });
428
+
429
+ test("the alias also normalizes from the boot env when the API is unreachable", async () => {
430
+ const result = await fetchResolvedEnv("", "key", "agent-1", {
431
+ HELMCODE_API_NAN: "fixture-nan-boot",
432
+ });
433
+ expect(result.env.NAN_API_KEY).toBe("fixture-nan-boot");
434
+ });
435
+
308
436
  describe("model-scoped window filter — GET /api/keys/available?model=<family>", () => {
309
437
  beforeEach(() => {
310
438
  keysAvailableRequests.length = 0;
@@ -0,0 +1,10 @@
1
+ {
2
+ "update_id": 900001,
3
+ "message": {
4
+ "message_id": 11,
5
+ "date": 1760400000,
6
+ "chat": { "id": 555001, "type": "private", "first_name": "Taras", "username": "taras_test" },
7
+ "from": { "id": 555001, "is_bot": false, "first_name": "Taras", "username": "taras_test" },
8
+ "text": "hello swarm, please summarize the open tasks"
9
+ }
10
+ }
@@ -0,0 +1,27 @@
1
+ import { modify, type SwarmExtension } from "swarm-extension";
2
+ import { z } from "zod";
3
+
4
+ export const config = z.object({ adapter: z.string(), channelId: z.string(), agentId: z.string() });
5
+
6
+ const manifest = {
7
+ name: "route-chat-channel-to-agent",
8
+ description: "Routes one Chat SDK channel to a configured agent",
9
+ version: "1.0.0",
10
+ runtime: "api",
11
+ assets: { hooks: "hooks.ts" },
12
+ config,
13
+ } as const;
14
+
15
+ const extension: SwarmExtension<typeof manifest> = (api) => {
16
+ api.on("pre.chat.route", (event, ctx) => {
17
+ if (event.adapter !== ctx.config.adapter || event.channelId !== ctx.config.channelId) return;
18
+ return modify({ target: { kind: "agent", agentId: ctx.config.agentId } });
19
+ });
20
+
21
+ api.on("post.chat.message", async (event, ctx) => {
22
+ if (event.direction !== "inbound" || !event.userId) return;
23
+ await ctx.state.set(`chat:${event.channelId}`, event.userId);
24
+ });
25
+ };
26
+
27
+ export default extension;
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "route-chat-channel-to-agent",
3
+ "description": "Routes one Chat SDK channel to a configured agent",
4
+ "version": "1.0.0",
5
+ "runtime": "api",
6
+ "assets": {
7
+ "hooks": "hooks.ts"
8
+ }
9
+ }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Proof that adding an OpenAI-compatible provider takes one registry entry.
3
+ *
4
+ * A fake `testgw` entry is added to the registry table
5
+ * (`packages/model-catalog/src/openai-compatible-providers.ts`) through
6
+ * `mock.module`, without editing any production file, and must reach
7
+ * credential routing, the opencode config, the pi models.json writer, the
8
+ * dashboard integration cards and the model picker groups.
9
+ *
10
+ * The mock is installed before any consumer below is imported (dynamic
11
+ * imports). Consumers cache derivations of the table at import (the
12
+ * integration cards, the picker constants), and `mock.module` cannot undo
13
+ * those, so this file only runs in its own `bun test` child process,
14
+ * spawned by `src/tests/openai-compatible-registry-extension.test.ts`. Its
15
+ * name keeps it out of the root test glob.
16
+ */
17
+ import { afterAll, describe, expect, mock, test } from "bun:test";
18
+ import { mkdtemp, readFile, rm } from "node:fs/promises";
19
+ import { tmpdir } from "node:os";
20
+ import { join } from "node:path";
21
+
22
+ const REGISTRY_MODULE = "../../../packages/model-catalog/src/openai-compatible-providers.ts";
23
+
24
+ const TESTGW = {
25
+ id: "testgw",
26
+ displayName: "TestGW",
27
+ description: "A fake gateway for the registry extension test.",
28
+ baseUrl: "https://testgw.invalid/v1",
29
+ docsUrl: "https://testgw.invalid/docs",
30
+ envVar: "TESTGW_API_KEY",
31
+ aliasEnvVars: [],
32
+ catalog: "registry",
33
+ npm: "@ai-sdk/openai-compatible",
34
+ models: [
35
+ {
36
+ id: "m1",
37
+ name: "Test Model 1",
38
+ contextWindow: 131072,
39
+ maxOutputTokens: 8192,
40
+ input: ["text"],
41
+ reasoning: true,
42
+ compat: { supportsDeveloperRole: true },
43
+ reasoningEfforts: ["low", "high"],
44
+ },
45
+ ],
46
+ } as const;
47
+
48
+ const real = (await import(REGISTRY_MODULE)) as {
49
+ OPENAI_COMPATIBLE_PROVIDERS: Record<string, unknown>;
50
+ };
51
+ const original = real.OPENAI_COMPATIBLE_PROVIDERS;
52
+ mock.module(REGISTRY_MODULE, () => ({
53
+ OPENAI_COMPATIBLE_PROVIDERS: { ...original, testgw: TESTGW },
54
+ }));
55
+
56
+ // The test preload already loaded `src/utils/credentials.ts`, so only its
57
+ // call-time paths (routing, pooling) see the mock; `CREDENTIAL_POOL_VARS` is
58
+ // fixed at import and covered by the registry spread in its declaration.
59
+ const { getModelAwareCredentialVars, resolveCredentialPools } = await import(
60
+ "../../utils/credentials"
61
+ );
62
+ const { applyProviderKeySplice } = await import("../../providers/opencode-adapter");
63
+ type OpencodeConfig = import("../../providers/opencode-adapter").OpencodeConfig;
64
+ const { writeSessionModelsJson } = await import("../../providers/pi-mono-adapter");
65
+ const { INTEGRATIONS } = await import("../../../apps/ui/src/lib/integrations-catalog");
66
+ const { modelGroupsForHarness } = await import("../../../apps/ui/src/lib/agent-runtime-models");
67
+
68
+ const tmpDirs: string[] = [];
69
+ afterAll(async () => {
70
+ for (const dir of tmpDirs) await rm(dir, { recursive: true, force: true });
71
+ });
72
+
73
+ describe("a new registry entry (testgw) reaches every derived surface", () => {
74
+ test("credential routing picks its key on pi and opencode", () => {
75
+ expect(getModelAwareCredentialVars("pi", "testgw/x")).toEqual(["TESTGW_API_KEY"]);
76
+ expect(getModelAwareCredentialVars("opencode", "testgw/x")).toEqual(["TESTGW_API_KEY"]);
77
+ });
78
+
79
+ test("its key pools alone for a testgw/* model", async () => {
80
+ const env: Record<string, string | undefined> = {
81
+ TESTGW_API_KEY: "example-gw-a,example-gw-b",
82
+ OPENROUTER_API_KEY: "example-or-a,example-or-b",
83
+ };
84
+ const selections = await resolveCredentialPools(env, { provider: "pi", model: "testgw/m1" });
85
+ expect(["example-gw-a", "example-gw-b"]).toContain(env.TESTGW_API_KEY ?? "");
86
+ expect(selections.map((s) => [s.keyType, s.total])).toEqual([["TESTGW_API_KEY", 2]]);
87
+ expect(env.OPENROUTER_API_KEY).toBe("example-or-a,example-or-b");
88
+ });
89
+
90
+ test("opencode gets a custom block with the picked key at the base URL", () => {
91
+ const config: OpencodeConfig = {};
92
+ const picked = applyProviderKeySplice(config, "testgw/m1", { TESTGW_API_KEY: "k1,k2" });
93
+ expect(picked).toEqual({ key: "k1", name: "TESTGW_API_KEY" });
94
+ const block = config.provider?.testgw;
95
+ expect(block?.npm).toBe("@ai-sdk/openai-compatible");
96
+ expect(block?.env).toEqual([]);
97
+ expect(block?.options).toEqual({ baseURL: "https://testgw.invalid/v1", apiKey: "k1" });
98
+ expect(block?.models?.m1).toMatchObject({ name: "Test Model 1", tool_call: true });
99
+
100
+ const overridden: OpencodeConfig = {};
101
+ applyProviderKeySplice(overridden, "testgw/m1", {
102
+ TESTGW_API_KEY: "k1",
103
+ TESTGW_BASE_URL: "http://127.0.0.1:9/v1",
104
+ });
105
+ expect(overridden.provider?.testgw?.options?.baseURL).toBe("http://127.0.0.1:9/v1");
106
+ });
107
+
108
+ test("the pi models.json writer adds its provider block", async () => {
109
+ const dir = await mkdtemp(join(tmpdir(), "testgw-pi-"));
110
+ tmpDirs.push(dir);
111
+ const models = await writeSessionModelsJson(dir, { TESTGW_API_KEY: "k1" }, "testgw/m1");
112
+ const written = JSON.parse(await readFile(models.modelsPath as string, "utf-8"));
113
+ await models.dispose();
114
+ expect(written.providers.testgw).toMatchObject({
115
+ baseUrl: "https://testgw.invalid/v1",
116
+ api: "openai-completions",
117
+ apiKey: "$TESTGW_API_KEY",
118
+ });
119
+ expect(written.providers.testgw.models.map((m: { id: string }) => m.id)).toEqual(["m1"]);
120
+ });
121
+
122
+ test("the dashboard lists an integration card with its key and base URL", () => {
123
+ const card = INTEGRATIONS.find((def) => def.id === "testgw");
124
+ expect(card?.name).toBe("TestGW");
125
+ expect(card?.providerIconKey as string | undefined).toBe("testgw");
126
+ expect(card?.fields.map((f) => [f.key, f.isSecret])).toEqual([
127
+ ["TESTGW_API_KEY", true],
128
+ ["TESTGW_BASE_URL", false],
129
+ ]);
130
+ });
131
+
132
+ test("the picker shows its models, locked without a key and enabled with one", () => {
133
+ for (const harness of ["pi", "opencode"] as const) {
134
+ const locked = modelGroupsForHarness(harness, [], {}).find((g) => g.provider === "TestGW");
135
+ expect(locked?.enabled).toBe(false);
136
+ expect(locked?.requiredKey).toBe("TESTGW_API_KEY");
137
+ expect(locked?.models.map((m) => m.id)).toEqual(["testgw/m1"]);
138
+ expect(locked?.models[0]?.reasoningLevels).toEqual(["low", "high"]);
139
+
140
+ const enabled = modelGroupsForHarness(harness, [], { TESTGW_API_KEY: true }).find(
141
+ (g) => g.provider === "TestGW",
142
+ );
143
+ expect(enabled?.enabled).toBe(true);
144
+ }
145
+ });
146
+ });