@desplega.ai/agent-swarm 2.4.4 → 2.5.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 (382) hide show
  1. package/README.md +4 -2
  2. package/dist/{acp-adapter-thq04adh.js → acp-adapter-11gp67nr.js} +8 -6
  3. package/dist/{actions-j6ywh6x5.js → actions-8p2kze8j.js} +24 -22
  4. package/dist/{amp-adapter-k8b5xmj8.js → amp-adapter-z4qvhacj.js} +2 -2
  5. package/dist/{anthropic-messages-g6pqmqk4.js → anthropic-messages-txs190dd.js} +17 -20
  6. package/dist/{app-rb7vc02s.js → app-0qp0a2gk.js} +3 -3
  7. package/dist/{assistant-ftta1zjf.js → assistant-jedd3ktm.js} +27 -25
  8. package/dist/{azure-openai-responses-9dbmj346.js → azure-openai-responses-9kfwhmn7.js} +18 -13
  9. package/dist/{boot-reembed-xns7h5h9.js → boot-reembed-4xwq5tg0.js} +1 -1
  10. package/dist/{boot-reembed-srp6v6af.js → boot-reembed-n4wq5dd2.js} +1 -1
  11. package/dist/{boot-scrub-sweep-3qqrh2mz.js → boot-scrub-sweep-69h90xcq.js} +4 -4
  12. package/dist/{boot-seeds-9ks8hce7.js → boot-seeds-3vn0m9sg.js} +4 -4
  13. package/dist/{claude-adapter-k5ksyres.js → claude-adapter-221mvad5.js} +8 -6
  14. package/dist/{claude-managed-adapter-h2y889ge.js → claude-managed-adapter-gt6pa35d.js} +2 -2
  15. package/dist/{claude-managed-setup-n5a5eqks.js → claude-managed-setup-c3hp36ph.js} +4 -4
  16. package/dist/{claude-sdk-session-2j3j4kwn.js → claude-sdk-session-8thjsk5e.js} +9729 -9628
  17. package/dist/{cli-b9fcxxev.js → cli-007cnhm1.js} +50 -4
  18. package/dist/{cli-j4rqdf2n.js → cli-03vpq45j.js} +2 -2
  19. package/dist/{cli-amyhm3gj.js → cli-0y6f0ckq.js} +1 -1
  20. package/dist/{cli-4dg55wsk.js → cli-0z3vjrpm.js} +1 -1
  21. package/dist/{cli-pccfmtb5.js → cli-179xcyky.js} +11 -11
  22. package/dist/{cli-3r8samv3.js → cli-1f0gp9vz.js} +3 -2
  23. package/dist/{cli-hj3debgw.js → cli-1pr672hz.js} +3 -3
  24. package/dist/{cli-0bqteszy.js → cli-2nf3b08t.js} +328 -25
  25. package/dist/{cli-t7cyef5g.js → cli-36j4jnk3.js} +4 -4
  26. package/dist/{cli-er83cjbn.js → cli-3qv8naq8.js} +527 -231
  27. package/dist/cli-3zwye9sn.js +1193 -0
  28. package/dist/{cli-60cgh1rv.js → cli-459fk97x.js} +7 -7
  29. package/dist/{cli-xj0re5tm.js → cli-4w24f7z3.js} +1 -1
  30. package/dist/{cli-v3yr2gbg.js → cli-5kzk3472.js} +2198 -968
  31. package/dist/{cli-h7gc0b4r.js → cli-5m84zd2k.js} +55 -18
  32. package/dist/{cli-w6nadkh4.js → cli-5rvg1ddk.js} +5 -5
  33. package/dist/{cli-dvkmp9bt.js → cli-6ctwrf1z.js} +20 -10
  34. package/dist/{cli-9qtjfd60.js → cli-6rg2jn2y.js} +3 -2
  35. package/dist/{cli-2v045xyf.js → cli-7bwtc6yx.js} +3 -3
  36. package/dist/{cli-3cgngyd8.js → cli-7g40g8hq.js} +35 -17
  37. package/dist/{cli-q6r7t299.js → cli-92mqenkc.js} +2379 -154
  38. package/dist/{cli-cynvc9fj.js → cli-9ce2xw98.js} +8 -8
  39. package/dist/{cli-gqvh8d8a.js → cli-bgdtv62r.js} +36 -22
  40. package/dist/{cli-wxbn3yfx.js → cli-cz5nmdxt.js} +2 -2
  41. package/dist/{cli-wt645v7q.js → cli-dabehxhp.js} +1 -1
  42. package/dist/{cli-vfha21c9.js → cli-e5bxnjps.js} +2 -2
  43. package/dist/{cli-p20r8w42.js → cli-fcc5qe3f.js} +763 -268
  44. package/dist/{cli-1ephgwnq.js → cli-fes51ya2.js} +3 -3
  45. package/dist/{cli-1d70crsy.js → cli-ftmq4nvk.js} +2 -2
  46. package/dist/{cli-qkhc5aqy.js → cli-fy1jyyym.js} +13 -13
  47. package/dist/{cli-s95dknxh.js → cli-g1cw7xtt.js} +1 -1
  48. package/dist/{cli-qje9k2nh.js → cli-g9ypd5en.js} +12 -2
  49. package/dist/{cli-74c1wpm0.js → cli-gpv60278.js} +1 -1
  50. package/dist/{cli-pemywhfq.js → cli-ha9rmwgq.js} +2 -1
  51. package/dist/{cli-bnz0r3kc.js → cli-j1eghqfm.js} +5 -5
  52. package/dist/{cli-2zqpkvtt.js → cli-jpxsrxcb.js} +1 -1
  53. package/dist/{cli-ejs3m1sx.js → cli-kxpfx2xr.js} +1 -1
  54. package/dist/{cli-37yb7307.js → cli-mbfj11fq.js} +1 -1
  55. package/dist/{cli-pnp2j5zg.js → cli-mmhj8vd7.js} +2 -2
  56. package/dist/{cli-xf234ws8.js → cli-n4c8e3s5.js} +2 -0
  57. package/dist/{cli-a0kcty51.js → cli-n6kmqngw.js} +2 -2
  58. package/dist/{cli-te6b5wvg.js → cli-nnvjb17s.js} +3 -3
  59. package/dist/{cli-1dmcq2eh.js → cli-nzfnt2gs.js} +1 -1
  60. package/dist/{cli-625tb7aq.js → cli-p859hcc8.js} +2 -2
  61. package/dist/{cli-smvcw9z5.js → cli-pasyvjmm.js} +1 -1
  62. package/dist/cli-pkaqk57w.js +128 -0
  63. package/dist/{cli-8nyhf7za.js → cli-qqzmjxkf.js} +129 -105
  64. package/dist/{cli-zrwpctp4.js → cli-r25qpaxt.js} +161 -63
  65. package/dist/{cli-nc35pxb7.js → cli-rh5smt8t.js} +19 -4
  66. package/dist/{cli-qwkcwjdn.js → cli-rycdaj0a.js} +1 -1
  67. package/dist/{cli-esxerzg7.js → cli-s4sntwt4.js} +2 -2
  68. package/dist/{cli-b8k7epj1.js → cli-segs7hx9.js} +8 -8
  69. package/dist/{cli-36ckw0wx.js → cli-stxdrqs2.js} +2 -2
  70. package/dist/{cli-vdm20wej.js → cli-sy8f9705.js} +4 -4
  71. package/dist/{cli-1m17dc9d.js → cli-ta8bd7th.js} +2 -2
  72. package/dist/{cli-2y91wnbh.js → cli-we0ra057.js} +222 -10
  73. package/dist/{cli-55qmbp6z.js → cli-x21dqn18.js} +5 -5
  74. package/dist/{cli-dqb4n629.js → cli-x6vf4sad.js} +11 -11
  75. package/dist/{cli-d2mb5s8a.js → cli-x77gen2p.js} +680 -440
  76. package/dist/{cli-vq97g1jq.js → cli-xawhpgc9.js} +95 -195
  77. package/dist/{cli-82d3nkc3.js → cli-xd4eqtr2.js} +2 -2
  78. package/dist/{cli-9fhkkpj5.js → cli-xt768r5m.js} +2 -2
  79. package/dist/{cli-zrk6jxv8.js → cli-y4x8ww6s.js} +2 -2
  80. package/dist/{cli-xc27fwzg.js → cli-y9jvxfhk.js} +3 -3
  81. package/dist/{cli-44159k28.js → cli-z63a269p.js} +2 -2
  82. package/dist/{cli-sm8rses9.js → cli-zfdp01hn.js} +8 -5
  83. package/dist/cli.js +31 -29
  84. package/dist/{cloudflare-workers-ai-system-one-ch2wxfcp.js → cloudflare-workers-ai-system-one-43v9qkjk.js} +7 -2
  85. package/dist/{codex-adapter-968xwfy7.js → codex-adapter-tcg1q3jk.js} +6 -4
  86. package/dist/{codex-login-m819c41r.js → codex-login-rn7c5gxt.js} +1 -1
  87. package/dist/{codex-session-runner-ht4z99fa.js → codex-session-runner-xfx4wd9x.js} +7 -5
  88. package/dist/{commands-pcymaeg9.js → commands-ew83nj70.js} +2 -2
  89. package/dist/{controller-fpjyk46r.js → controller-gnq1k1mf.js} +4 -4
  90. package/dist/{controllers-connect-mwzx1fh6.js → controllers-connect-tw1dan9r.js} +26 -24
  91. package/dist/{cursor-adapter-cp4s2e2f.js → cursor-adapter-yq5xgfcg.js} +1 -1
  92. package/dist/{db-xsjg9jqq.js → db-7gbc53fd.js} +14 -2
  93. package/dist/{db-sgd12bt9.js → db-t86xrnes.js} +3 -3
  94. package/dist/{devin-adapter-zgqadvnm.js → devin-adapter-4ycy4vrf.js} +1 -1
  95. package/dist/{dispatcher-x0jfw2c9.js → dispatcher-64bdyhps.js} +23 -21
  96. package/dist/{docker-fkgtr75t.js → docker-nfy2gc8t.js} +2 -2
  97. package/dist/{dsh-adapter-jrq7ma7h.js → dsh-adapter-tvhc4k2d.js} +1 -1
  98. package/dist/{e2b-16d6ch39.js → e2b-9gkdhm3c.js} +2 -2
  99. package/dist/{e2b-zn0b4w5w.js → e2b-cjtf8k1z.js} +2 -2
  100. package/dist/{easter-egg-3d-5cgynaja.js → easter-egg-3d-e4qv8dxj.js} +1 -1
  101. package/dist/{event-bus-9vpk8gmq.js → event-bus-3z78ep5n.js} +1 -1
  102. package/dist/{execute-t4e6m3rw.js → execute-0xpz8950.js} +85 -23
  103. package/dist/{external-channel-8njj4h3b.js → external-channel-e9nw24zv.js} +2 -2
  104. package/dist/{google-generative-ai-bmq8gkf3.js → google-generative-ai-mk9vcpe1.js} +15 -15
  105. package/dist/{google-vertex-b1rvp52s.js → google-vertex-q580kz8k.js} +20 -20
  106. package/dist/{grok-adapter-kkr97v2f.js → grok-adapter-v82srhrx.js} +9 -7
  107. package/dist/handlers-c9ajzhbn.js +41 -0
  108. package/dist/{heartbeat-wd03zhn0.js → heartbeat-2vqs1319.js} +26 -24
  109. package/dist/{hook-xzj2ts2p.js → hook-01nk13wx.js} +7 -5
  110. package/dist/{hook-f0vghj42.js → hook-jzpheegg.js} +9 -7
  111. package/dist/{http-4hwxv62p.js → http-fc1k6ec8.js} +859 -252
  112. package/dist/index-07ef7nkm.js +42 -0
  113. package/dist/{index-g6tbb6ve.js → index-31ceyf42.js} +1 -1
  114. package/dist/index-7yts8n9s.js +43 -0
  115. package/dist/{index-c4ykd0f1.js → index-dr94zbnh.js} +13348 -11611
  116. package/dist/index-q3t40ryv.js +46 -0
  117. package/dist/{index-ww2vznkp.js → index-sepja052.js} +1 -1
  118. package/dist/{index-97q4xn03.js → index-zdycv3ja.js} +135 -79
  119. package/dist/{index-g216q6jr.js → index-zsyx51ap.js} +1 -4
  120. package/dist/{keepalive-hrh7xnr9.js → keepalive-5nh2rhmr.js} +4 -4
  121. package/dist/lead-aayq64jn.js +79 -0
  122. package/dist/{llama-cpp-classify-vv43gswy.js → llama-cpp-classify-ee7vmd7q.js} +4 -1
  123. package/dist/{maintenance-e8wnwnqx.js → maintenance-hjhzdt3k.js} +1 -1
  124. package/dist/{mistral-conversations-1medfwv7.js → mistral-conversations-sd0xf1tn.js} +13 -13
  125. package/dist/{node-3v3f08f7.js → node-jzjj7r9x.js} +10 -9
  126. package/dist/{oauth-refresh-sweep-zm0yj4sj.js → oauth-refresh-sweep-y3n1knmd.js} +3 -3
  127. package/dist/{onboard-dtwcq603.js → onboard-1zq3m7db.js} +2 -2
  128. package/dist/{openai-codex-responses-de9pvs4d.js → openai-codex-responses-79s8xnbh.js} +29 -23
  129. package/dist/{openai-completions-mtqge3nf.js → openai-completions-tdbyts0x.js} +21 -20
  130. package/dist/{openai-decisions-ygk4kkxd.js → openai-decisions-bqhnjyfz.js} +2 -1
  131. package/dist/{openai-responses-dkyng71g.js → openai-responses-dm3t5vqp.js} +26 -17
  132. package/dist/{opencode-adapter-x4tqzhkh.js → opencode-adapter-e56086m1.js} +7 -5
  133. package/dist/{openrouter-images-wvv7camj.js → openrouter-images-ssttr1y4.js} +6 -3
  134. package/dist/{otel-impl-w3fbhpn0.js → otel-impl-h9yyza47.js} +2 -2
  135. package/dist/{pi-messages-nhh1pkz2.js → pi-messages-3nf8bq1a.js} +6 -4
  136. package/dist/{pi-mono-adapter-mcfkn9vf.js → pi-mono-adapter-tzps8f38.js} +9 -37
  137. package/dist/{pricing-refresh-j38dehfs.js → pricing-refresh-nevhat8h.js} +3 -3
  138. package/dist/{rbac-roles-ae8yttq4.js → rbac-roles-4y6ngvzq.js} +1 -1
  139. package/dist/{rbac-roles-ejxx9b35.js → rbac-roles-exbg8xb6.js} +2 -2
  140. package/dist/{registry-rxw10xzg.js → registry-ajhexe44.js} +1 -1
  141. package/dist/{render-v2-n16n0phe.js → render-v2-cfsh13z3.js} +3 -3
  142. package/dist/{resume-z0fgjc38.js → resume-5s2t0dbs.js} +10 -8
  143. package/dist/{runtime-xe3aqy4x.js → runtime-bt5pkpm1.js} +1 -1
  144. package/dist/{session-access-cv3mq6mc.js → session-access-dnbyp3cn.js} +6 -4
  145. package/dist/{setup-mafhj3yt.js → setup-1wj1y77j.js} +1 -1
  146. package/dist/{typesafe-system-one-2hgpayzy.js → typesafe-system-one-bre38hvs.js} +4 -2
  147. package/dist/{virtual-modules-4r3zkc20.js → virtual-modules-mnfbh5mr.js} +8 -2
  148. package/dist/{work-objects-8b86mgjp.js → work-objects-7mg6216g.js} +26 -24
  149. package/dist/worker-nwb789mg.js +79 -0
  150. package/dist/{x-y45mthwn.js → x-pbpezr0s.js} +2 -2
  151. package/openapi.json +793 -6
  152. package/package.json +11 -11
  153. package/src/apps/catalog.generated.json +206 -4
  154. package/src/apps/page-validator.ts +13 -1
  155. package/src/assets/key.ts +17 -0
  156. package/src/azure-devops/handlers.ts +1 -1
  157. package/src/be/chunking.ts +42 -9
  158. package/src/be/db/approvals.ts +18 -0
  159. package/src/be/db/session-moves.ts +164 -0
  160. package/src/be/db/session-tree.ts +384 -0
  161. package/src/be/db/tasks/draft-text.ts +15 -0
  162. package/src/be/db/tasks/read.ts +54 -5
  163. package/src/be/db/workflow-runs.ts +96 -0
  164. package/src/be/db.ts +85 -237
  165. package/src/be/memory/index-content.ts +36 -17
  166. package/src/be/memory/providers/openai-embedding.ts +115 -61
  167. package/src/be/memory/providers/sqlite-store.ts +69 -23
  168. package/src/be/memory/reranker.ts +31 -1
  169. package/src/be/memory/search-candidates.ts +47 -0
  170. package/src/be/memory/types.ts +14 -0
  171. package/src/be/onboarding.ts +3 -0
  172. package/src/be/scripts/typecheck.ts +2 -0
  173. package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +106 -54
  174. package/src/be/seed-scripts/index.ts +2 -2
  175. package/src/be/skill-sync.ts +43 -2
  176. package/src/be/swarm-config-guard.ts +25 -1
  177. package/src/be/task-attachment-store.ts +30 -0
  178. package/src/bitbucket/api.ts +81 -0
  179. package/src/bitbucket/auth.ts +64 -0
  180. package/src/bitbucket/handlers.ts +434 -0
  181. package/src/bitbucket/index.ts +28 -0
  182. package/src/bitbucket/templates.ts +134 -0
  183. package/src/bitbucket/types.ts +65 -0
  184. package/src/commands/runner.ts +55 -3
  185. package/src/e2b/env.ts +1 -0
  186. package/src/extensions/catalog.generated.json +4 -4
  187. package/src/extensions/contract-types.generated.ts +1 -1
  188. package/src/extensions/contract.ts +9 -2
  189. package/src/extensions/post-bridge.ts +1 -0
  190. package/src/fs/provider.ts +13 -1
  191. package/src/github/handlers.ts +6 -6
  192. package/src/gitlab/handlers.ts +5 -5
  193. package/src/heartbeat/heartbeat.ts +31 -11
  194. package/src/http/assets.ts +14 -2
  195. package/src/http/core.ts +12 -0
  196. package/src/http/fs.ts +54 -34
  197. package/src/http/index.ts +4 -0
  198. package/src/http/mcp-body.ts +37 -0
  199. package/src/http/mcp-user.ts +3 -5
  200. package/src/http/mcp.ts +6 -7
  201. package/src/http/memory.ts +26 -15
  202. package/src/http/onboarding.ts +1 -0
  203. package/src/http/page-proxy.ts +61 -5
  204. package/src/http/pages-public.ts +27 -44
  205. package/src/http/pages.ts +4 -4
  206. package/src/http/script-runs.ts +2 -0
  207. package/src/http/sessions.ts +366 -12
  208. package/src/http/skills.ts +48 -1
  209. package/src/http/tasks.ts +1 -1
  210. package/src/http/users.ts +13 -1
  211. package/src/http/webhooks.ts +141 -0
  212. package/src/integrations/chat-sdk/adapters.ts +24 -4
  213. package/src/integrations/chat-sdk/attachments.ts +90 -12
  214. package/src/integrations/chat-sdk/facts.ts +138 -0
  215. package/src/integrations/chat-sdk/inbound.ts +156 -8
  216. package/src/integrations/chat-sdk/limits.ts +24 -0
  217. package/src/integrations/chat-sdk/outbound-files.ts +186 -0
  218. package/src/integrations/chat-sdk/outbound.ts +272 -5
  219. package/src/integrations/chat-sdk/relay.ts +4 -1
  220. package/src/integrations/chat-sdk/router.ts +28 -2
  221. package/src/integrations/chat-sdk/steering.ts +9 -0
  222. package/src/integrations/chat-sdk/templates.ts +14 -0
  223. package/src/integrations/chat-sdk/thread-buffer.ts +4 -0
  224. package/src/mcp-limits.ts +20 -0
  225. package/src/pages/browser-sdk.ts +8 -3
  226. package/src/pages/feedback-overlay.ts +1 -1
  227. package/src/providers/acp-targets.ts +1 -1
  228. package/src/providers/amp-adapter.ts +1 -1
  229. package/src/providers/codex-adapter.ts +106 -119
  230. package/src/providers/codex-agents-md.ts +35 -229
  231. package/src/providers/cursor-adapter.ts +1 -1
  232. package/src/providers/pi-mono-adapter.ts +5 -53
  233. package/src/realtime/transport.ts +1 -2
  234. package/src/script-workflows/workflow-ctx.ts +1 -0
  235. package/src/scripts-runtime/types/stdlib.d.ts +2 -0
  236. package/src/scripts-runtime/types/swarm-extension.d.ts +14 -4
  237. package/src/scripts-runtime/types/swarm-sdk.d.ts +2 -0
  238. package/src/server-user.ts +2 -0
  239. package/src/server.ts +2 -0
  240. package/src/slack/assistant.ts +2 -2
  241. package/src/slack/files.ts +2 -0
  242. package/src/slack/handlers.test.ts +158 -0
  243. package/src/slack/handlers.ts +23 -32
  244. package/src/slack/inbound-files.ts +39 -6
  245. package/src/slack/thread-buffer.ts +2 -1
  246. package/src/tasks/context-key.ts +61 -1
  247. package/src/tasks/task-terminal-effects.ts +21 -17
  248. package/src/tests/apps-card-grid.test.ts +197 -0
  249. package/src/tests/apps-catalog-additions.test.ts +120 -0
  250. package/src/tests/apps-tree.test.ts +129 -0
  251. package/src/tests/bitbucket-auth.test.ts +156 -0
  252. package/src/tests/bitbucket-handlers.test.ts +425 -0
  253. package/src/tests/bitbucket-webhook.test.ts +220 -0
  254. package/src/tests/chat-sdk-attachments-reply.test.ts +52 -0
  255. package/src/tests/chat-sdk-config.test.ts +11 -0
  256. package/src/tests/chat-sdk-facts.test.ts +23 -0
  257. package/src/tests/chat-sdk-inbound.test.ts +597 -4
  258. package/src/tests/chat-sdk-outbound-files.test.ts +652 -0
  259. package/src/tests/chat-sdk-telegram-markdown.test.ts +92 -0
  260. package/src/tests/codex-adapter.test.ts +167 -242
  261. package/src/tests/config-api-only-keys.test.ts +9 -0
  262. package/src/tests/context-key.test.ts +50 -0
  263. package/src/tests/core-auth.test.ts +5 -0
  264. package/src/tests/cors-allowlist.test.ts +1 -1
  265. package/src/tests/db-task-read-contract.test.ts +7 -0
  266. package/src/tests/db-task-write-contract.test.ts +6 -0
  267. package/src/tests/docs-index-footprint.test.js +3 -3
  268. package/src/tests/extensions-example-slack-reply-gate.test.ts +262 -91
  269. package/src/tests/fixtures/bitbucket/pullrequest-comment-created-inline.json +174 -0
  270. package/src/tests/fixtures/bitbucket/pullrequest-comment-created.json +170 -0
  271. package/src/tests/fixtures/bitbucket/pullrequest-created-mention.json +132 -0
  272. package/src/tests/fixtures/bitbucket/pullrequest-created-reviewer.json +140 -0
  273. package/src/tests/fixtures/bitbucket/pullrequest-fulfilled.json +134 -0
  274. package/src/tests/fixtures/bitbucket/pullrequest-rejected.json +133 -0
  275. package/src/tests/generate-mcp-docs.test.ts +101 -0
  276. package/src/tests/gitlab-handlers.test.ts +3 -3
  277. package/src/tests/gitlab-vcs-db.test.ts +4 -4
  278. package/src/tests/heartbeat-checklist.test.ts +33 -0
  279. package/src/tests/heartbeat-jev-gate-credentials.test.ts +44 -0
  280. package/src/tests/heartbeat-jev-gate.test.ts +242 -1
  281. package/src/tests/integration-disable-flags.test.ts +36 -0
  282. package/src/tests/kv-page-proxy.test.ts +91 -5
  283. package/src/tests/launch-password-rejection.test.ts +2 -2
  284. package/src/tests/mcp-input-caps.test.ts +294 -0
  285. package/src/tests/mcp-tools.test.ts +1 -1
  286. package/src/tests/memory-chunked-search-starvation.test.ts +200 -0
  287. package/src/tests/memory-embedding-input-cap.test.ts +114 -0
  288. package/src/tests/memory.test.ts +14 -0
  289. package/src/tests/onboarding.test.ts +12 -0
  290. package/src/tests/page-guest-session.test.ts +19 -8
  291. package/src/tests/page-proxy-authed.test.ts +1 -1
  292. package/src/tests/page-proxy.test.ts +17 -10
  293. package/src/tests/page-session.test.ts +102 -1
  294. package/src/tests/pages-authed-mode.test.ts +82 -12
  295. package/src/tests/pages-password-mode.test.ts +5 -5
  296. package/src/tests/pi-codemode.test.ts +8 -2
  297. package/src/tests/preload-env.test.ts +57 -0
  298. package/src/tests/preload.ts +21 -0
  299. package/src/tests/prompt-template-remaining.test.ts +38 -0
  300. package/src/tests/realtime-transport.test.ts +13 -7
  301. package/src/tests/run-root-coverage.test.ts +39 -0
  302. package/src/tests/runner-bitbucket-vcs.test.ts +191 -0
  303. package/src/tests/runner-skills-refresh.test.ts +58 -0
  304. package/src/tests/script-runs-http.test.ts +84 -0
  305. package/src/tests/scripts-http.test.ts +10 -1
  306. package/src/tests/secret-scrubber.test.ts +15 -0
  307. package/src/tests/sessions-move.test.ts +576 -0
  308. package/src/tests/sessions-tree.test.ts +493 -0
  309. package/src/tests/skill-files-http.test.ts +50 -0
  310. package/src/tests/skills-bundle.test.ts +99 -0
  311. package/src/tests/swarm-config-export-to-env.test.ts +23 -3
  312. package/src/tests/task-audio-transcription.test.ts +299 -0
  313. package/src/tests/task-completion-memory-chunking.test.ts +434 -0
  314. package/src/tests/task-terminal-secret-scrub.test.ts +6 -1
  315. package/src/tests/transcription.test.ts +299 -0
  316. package/src/tests/vcs-bitbucket.test.ts +91 -0
  317. package/src/tests/vcs-provider-isolation.test.ts +123 -0
  318. package/src/tests/vcs-provider.test.ts +16 -1
  319. package/src/tests/vcs-tracking.test.ts +3 -3
  320. package/src/tests/workflow-foreach-bodies.test.ts +637 -0
  321. package/src/tests/workflow-foreach-concurrency.test.ts +342 -0
  322. package/src/tests/workflow-foreach-retry.test.ts +537 -0
  323. package/src/tests/workflow-foreach.test.ts +44 -23
  324. package/src/tests/workflow-step-claims.test.ts +6 -6
  325. package/src/tests/workflow-system-one-decision-providers.test.ts +98 -1
  326. package/src/tests/workflow-system-one-decision.test.ts +2 -1
  327. package/src/tests/workflow-tla-races.test.ts +2 -2
  328. package/src/tools/chat-access.ts +10 -0
  329. package/src/tools/chat-post.ts +31 -11
  330. package/src/tools/chat-reply.ts +32 -10
  331. package/src/tools/memory-search.ts +3 -1
  332. package/src/tools/script-delete.ts +1 -1
  333. package/src/tools/system-one-decide.ts +17 -4
  334. package/src/tools/workflows/create-workflow.ts +5 -3
  335. package/src/types.ts +9 -2
  336. package/src/utils/config-env-export.ts +4 -1
  337. package/src/utils/page-session.ts +133 -20
  338. package/src/utils/secret-scrubber.ts +4 -0
  339. package/src/utils/skills-refresh.ts +83 -43
  340. package/src/utils/transcription.ts +246 -0
  341. package/src/vcs/bitbucket.ts +58 -0
  342. package/src/vcs/index.ts +2 -0
  343. package/src/vcs/types.ts +2 -2
  344. package/src/workflows/approval-resolution.ts +4 -1
  345. package/src/workflows/checkpoint.ts +9 -1
  346. package/src/workflows/constants.ts +3 -0
  347. package/src/workflows/definition.ts +48 -17
  348. package/src/workflows/engine.ts +25 -15
  349. package/src/workflows/executors/foreach.ts +45 -99
  350. package/src/workflows/executors/system-one-decision.ts +28 -7
  351. package/src/workflows/executors/system-one-providers.ts +39 -4
  352. package/src/workflows/foreach-body.ts +146 -0
  353. package/src/workflows/foreach-driver.ts +552 -0
  354. package/src/workflows/foreach-join.ts +206 -7
  355. package/src/workflows/node-timeout.ts +21 -0
  356. package/src/workflows/recovery.ts +68 -3
  357. package/src/workflows/resume.ts +221 -43
  358. package/src/workflows/retry-poller.ts +39 -24
  359. package/src/workflows/step-input.ts +59 -0
  360. package/src/workflows/task-step-routing.ts +114 -56
  361. package/templates/extensions/slack-reply-gate/README.md +24 -22
  362. package/templates/extensions/slack-reply-gate/hooks.ts +146 -95
  363. package/templates/extensions/slack-reply-gate/manifest.json +2 -2
  364. package/templates/skills/apps/SKILL.md +28 -6
  365. package/templates/skills/apps/content.md +28 -6
  366. package/templates/skills/chat-sdk/content.md +6 -2
  367. package/templates/skills/code-quality/content.md +2 -1
  368. package/templates/skills/heartbeat-runbook/content.md +5 -5
  369. package/templates/skills/pages/content.md +6 -4
  370. package/templates/skills/slack-interaction/content.md +5 -0
  371. package/templates/skills/swarm-scripts/SKILL.md +2 -2
  372. package/templates/skills/swarm-scripts/content.md +2 -2
  373. package/templates/skills/work-on-task/SKILL.md +1 -1
  374. package/templates/skills/work-on-task/content.md +1 -1
  375. package/dist/cli-51y09c6p.js +0 -2657
  376. package/dist/cli-yw91rnmw.js +0 -13
  377. package/dist/handlers-0pvee9nw.js +0 -39
  378. package/dist/index-gdh8jrdm.js +0 -43
  379. package/dist/index-hg0x2cdv.js +0 -40
  380. package/dist/index-thm0y6a8.js +0 -41
  381. package/dist/lead-ee8vazz2.js +0 -77
  382. package/dist/worker-16s75hm5.js +0 -77
@@ -134,17 +134,21 @@ afterAll(async () => {
134
134
  describe("proxy allowlist for a guest session", () => {
135
135
  test("allows the page's own record and its KV", async () => {
136
136
  const own = await fetch(`${BASE}/@swarm/api/pages/${pageId}`, {
137
- headers: { Cookie: `page_session=${guestCookie}` },
137
+ headers: { Cookie: `page_session_${pageId}=${guestCookie}`, "X-Swarm-Page-Id": pageId },
138
138
  });
139
139
  expect(own.status).toBe(200);
140
140
  const put = await fetch(`${BASE}/@swarm/api/kv/probe`, {
141
141
  method: "PUT",
142
- headers: { Cookie: `page_session=${guestCookie}`, "Content-Type": "application/json" },
142
+ headers: {
143
+ Cookie: `page_session_${pageId}=${guestCookie}`,
144
+ "X-Swarm-Page-Id": pageId,
145
+ "Content-Type": "application/json",
146
+ },
143
147
  body: JSON.stringify({ value: "v" }),
144
148
  });
145
149
  expect(put.status).toBe(200);
146
150
  const get = await fetch(`${BASE}/@swarm/api/kv/probe`, {
147
- headers: { Cookie: `page_session=${guestCookie}` },
151
+ headers: { Cookie: `page_session_${pageId}=${guestCookie}`, "X-Swarm-Page-Id": pageId },
148
152
  });
149
153
  expect(get.status).toBe(200);
150
154
  });
@@ -162,7 +166,11 @@ describe("proxy allowlist for a guest session", () => {
162
166
  test(`refuses ${method} ${path} with 403`, async () => {
163
167
  const res = await fetch(`${BASE}/@swarm/api/${path}`, {
164
168
  method,
165
- headers: { Cookie: `page_session=${guestCookie}`, "Content-Type": "application/json" },
169
+ headers: {
170
+ Cookie: `page_session_${pageId}=${guestCookie}`,
171
+ "X-Swarm-Page-Id": pageId,
172
+ "Content-Type": "application/json",
173
+ },
166
174
  body: method === "POST" ? "{}" : undefined,
167
175
  });
168
176
  expect(res.status).toBe(403);
@@ -206,13 +214,16 @@ describe("a password-page guest is isolated from another page", () => {
206
214
 
207
215
  const unlock = await fetch(`${BASE}/p/${pageA}?key=open-sesame`);
208
216
  expect(unlock.status).toBe(200);
209
- const cookieA = /page_session=([^;]+)/.exec(unlock.headers.get("set-cookie") ?? "")?.[1];
217
+ const cookieA = /page_session_[A-Za-z0-9_-]+=([^;]+)/.exec(
218
+ unlock.headers.get("set-cookie") ?? "",
219
+ )?.[1];
210
220
  expect(cookieA).toBeTruthy();
211
221
  const asGuest = (path: string, init: RequestInit = {}) =>
212
222
  fetch(`${BASE}/@swarm/api/${path}`, {
213
223
  ...init,
214
224
  headers: {
215
- Cookie: `page_session=${cookieA}`,
225
+ Cookie: `page_session_${pageA}=${cookieA}`,
226
+ "X-Swarm-Page-Id": pageA,
216
227
  "Content-Type": "application/json",
217
228
  "X-Page-Id": pageB,
218
229
  "X-Agent-ID": agentId,
@@ -377,12 +388,12 @@ describe("server-side gates deny a guest and keep the operator and user", () =>
377
388
  test("a user session through the proxy and an operator-launched session still work", async () => {
378
389
  for (const cookie of [userCookie, operatorCookie]) {
379
390
  const res = await fetch(`${BASE}/@swarm/api/tasks`, {
380
- headers: { Cookie: `page_session=${cookie}` },
391
+ headers: { Cookie: `page_session_${pageId}=${cookie}`, "X-Swarm-Page-Id": pageId },
381
392
  });
382
393
  expect(res.status).toBe(200);
383
394
  }
384
395
  const whoami = await fetch(`${BASE}/@swarm/api/whoami`, {
385
- headers: { Cookie: `page_session=${userCookie}` },
396
+ headers: { Cookie: `page_session_${pageId}=${userCookie}`, "X-Swarm-Page-Id": pageId },
386
397
  });
387
398
  expect(((await whoami.json()) as { kind: string }).kind).toBe("user");
388
399
  });
@@ -122,7 +122,7 @@ describe("/@swarm/api/* proxy — authed-mode page", () => {
122
122
  expect(launch.status).toBe(204);
123
123
  const setCookie = launch.headers.get("set-cookie");
124
124
  expect(setCookie).toBeTruthy();
125
- const cookieValue = /page_session=([^;]+)/.exec(setCookie!)?.[1];
125
+ const cookieValue = /page_session_[A-Za-z0-9_-]+=([^;]+)/.exec(setCookie!)?.[1];
126
126
  expect(cookieValue).toBeTruthy();
127
127
 
128
128
  // Drive the proxy with the cookie — should resolve to the page owner's
@@ -182,11 +182,13 @@ type BrowserSdk = {
182
182
  };
183
183
  };
184
184
 
185
- function browserSdk(cookie: string, spoofedAgentId?: string): BrowserSdk {
185
+ function browserSdk(pageId: string, cookie: string, spoofedAgentId?: string): BrowserSdk {
186
186
  const window: { swarmSdk?: BrowserSdk } = {};
187
187
  const pageFetch = (input: string, init?: RequestInit) => {
188
188
  const headers = new Headers(init?.headers);
189
- headers.set("Cookie", `page_session=${cookie}`);
189
+ // The SDK must name its own page; the test only supplies the cookie jar.
190
+ if (headers.get("X-Swarm-Page-Id") !== pageId) throw new Error("SDK did not name its page");
191
+ headers.set("Cookie", `page_session_${pageId}=${cookie}`);
190
192
  if (spoofedAgentId) headers.set("X-Agent-ID", spoofedAgentId);
191
193
  return fetch(new URL(input, BASE), { ...init, headers });
192
194
  };
@@ -195,6 +197,7 @@ function browserSdk(cookie: string, spoofedAgentId?: string): BrowserSdk {
195
197
  encodeURIComponent,
196
198
  fetch: pageFetch,
197
199
  URLSearchParams,
200
+ location: { pathname: `/p/${pageId}` },
198
201
  window,
199
202
  });
200
203
  if (!window.swarmSdk) throw new Error("browser SDK did not initialize");
@@ -207,7 +210,9 @@ async function launchPageCookie(id: string, bearer = API_KEY): Promise<string> {
207
210
  headers: { Authorization: `Bearer ${bearer}` },
208
211
  });
209
212
  expect(launch.status).toBe(204);
210
- const cookie = /page_session=([^;]+)/.exec(launch.headers.get("set-cookie") ?? "")?.[1];
213
+ const cookie = /page_session_[A-Za-z0-9_-]+=([^;]+)/.exec(
214
+ launch.headers.get("set-cookie") ?? "",
215
+ )?.[1];
211
216
  if (!cookie) throw new Error("failed to mint page session cookie");
212
217
  return cookie;
213
218
  }
@@ -243,14 +248,14 @@ describe("/api/pages/:id/launch", () => {
243
248
  expect(res.status).toBe(204);
244
249
  const cookie = res.headers.get("set-cookie");
245
250
  expect(cookie).toBeTruthy();
246
- expect(cookie!).toContain("page_session=");
251
+ expect(cookie!).toContain("page_session_");
247
252
  expect(cookie!).toContain("HttpOnly");
248
253
  expect(cookie!).toContain("Path=/");
249
254
  expect(cookie!).toContain("Max-Age=3600");
250
255
  // In dev (NODE_ENV != production) the cookie should be SameSite=Lax sans Secure.
251
256
  expect(cookie!).toContain("SameSite=Lax");
252
257
  expect(cookie!).not.toMatch(/\bSecure\b/);
253
- const cookieValue = /page_session=([^;]+)/.exec(cookie!)?.[1];
258
+ const cookieValue = /page_session_[A-Za-z0-9_-]+=([^;]+)/.exec(cookie!)?.[1];
254
259
  const payload = await verifyPageSession(cookieValue);
255
260
  expect(payload?.uid).toBeUndefined();
256
261
  expect(payload?.name).toMatch(/^guest-/);
@@ -294,7 +299,9 @@ describe("/@swarm/api/* proxy", () => {
294
299
  headers: { Authorization: `Bearer ${viewer.token}` },
295
300
  });
296
301
  expect(launch.status).toBe(204);
297
- const cookieValue = /page_session=([^;]+)/.exec(launch.headers.get("set-cookie") ?? "")?.[1];
302
+ const cookieValue = /page_session_[A-Za-z0-9_-]+=([^;]+)/.exec(
303
+ launch.headers.get("set-cookie") ?? "",
304
+ )?.[1];
298
305
  expect(cookieValue).toBeTruthy();
299
306
  const payload = await verifyPageSession(cookieValue);
300
307
  expect(payload?.uid).toBe(viewer.id);
@@ -322,7 +329,7 @@ describe("/@swarm/api/* proxy", () => {
322
329
  const setCookie = launch.headers.get("set-cookie");
323
330
  expect(setCookie).toBeTruthy();
324
331
 
325
- const cookieValue = /page_session=([^;]+)/.exec(setCookie!)?.[1];
332
+ const cookieValue = /page_session_[A-Za-z0-9_-]+=([^;]+)/.exec(setCookie!)?.[1];
326
333
  expect(cookieValue).toBeTruthy();
327
334
 
328
335
  const res = await fetch(`${BASE}/@swarm/api/agents/${agentId}`, {
@@ -410,7 +417,7 @@ describe("/@swarm/api/* page execution context", () => {
410
417
 
411
418
  // Execute the shipped browser SDK with a forged client identity. The
412
419
  // proxy must ignore that header and derive memory scope from the page.
413
- const sdk = browserSdk(cookie, otherAgentId);
420
+ const sdk = browserSdk(pageId, cookie, otherAgentId);
414
421
  const search = await sdk.memory.search({ query: searchToken, scope: "agent", limit: 10 });
415
422
  expect(search.results.map((memory) => memory.id)).toContain(ownerMemoryId);
416
423
  expect(search.results.map((memory) => memory.id)).not.toContain(otherMemoryId);
@@ -432,7 +439,7 @@ describe("/@swarm/api/* page execution context", () => {
432
439
  const viewer = await createUserToken(`Page Viewer ${randomUUID().slice(0, 8)}`);
433
440
  const pageId = await createPage();
434
441
  const cookie = await launchPageCookie(pageId, viewer.token);
435
- const sdk = browserSdk(cookie);
442
+ const sdk = browserSdk(pageId, cookie);
436
443
 
437
444
  const search = await sdk.memory.search({ query: searchToken, scope: "agent", limit: 5 });
438
445
  expect(search.results.map((memory) => memory.id)).toContain(ownerMemoryId);
@@ -506,7 +513,7 @@ describe("/@swarm/api/* proxy — suffix normalization", () => {
506
513
  headers: { Authorization: `Bearer ${API_KEY}`, "X-Agent-ID": agentId },
507
514
  });
508
515
  const setCookie = launch.headers.get("set-cookie");
509
- const cookieValue = /page_session=([^;]+)/.exec(setCookie!)?.[1];
516
+ const cookieValue = /page_session_[A-Za-z0-9_-]+=([^;]+)/.exec(setCookie!)?.[1];
510
517
  if (!cookieValue) throw new Error("failed to mint test cookie");
511
518
  return cookieValue;
512
519
  }
@@ -3,7 +3,15 @@ import { existsSync, mkdtempSync, rmSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import path from "node:path";
5
5
  import type { PageSessionPayload } from "../utils/page-session";
6
- import { parseCookieHeader, signPageSession, verifyPageSession } from "../utils/page-session";
6
+ import {
7
+ extractPageSession,
8
+ issuePageSessionCookie,
9
+ MAX_PAGE_SESSION_COOKIES,
10
+ pageSessionSetCookies,
11
+ parseCookieHeader,
12
+ signPageSession,
13
+ verifyPageSession,
14
+ } from "../utils/page-session";
7
15
 
8
16
  const ORIGINAL_SECRET = process.env.PAGE_SESSION_SECRET;
9
17
  const ORIGINAL_API_KEY = process.env.API_KEY;
@@ -266,3 +274,96 @@ describe("parseCookieHeader", () => {
266
274
  expect(parseCookieHeader("xxpage_session=evil; other=ok", "page_session")).toBeUndefined();
267
275
  });
268
276
  });
277
+
278
+ describe("per-page session cookies", () => {
279
+ const exp = () => Math.floor(Date.now() / 1000) + 3600;
280
+ const req = (cookie: string) => ({ headers: { cookie } });
281
+
282
+ test("issuance names the cookie after the page", async () => {
283
+ const header = await issuePageSessionCookie("pagea", { dev: true });
284
+ expect(header.startsWith("page_session_pagea=")).toBe(true);
285
+ });
286
+
287
+ test("each page reads its own cookie from a shared jar", async () => {
288
+ const a = await signPageSession({ pageId: "pagea", exp: exp() });
289
+ const b = await signPageSession({ pageId: "pageb", exp: exp() });
290
+ const jar = req(`page_session_pagea=${a}; page_session_pageb=${b}`);
291
+ expect((await extractPageSession(jar, "pagea"))?.token).toBe(a);
292
+ expect((await extractPageSession(jar, "pageb"))?.token).toBe(b);
293
+ });
294
+
295
+ test("the legacy page_session cookie is accepted only for the page it names", async () => {
296
+ const a = await signPageSession({ pageId: "pagea", exp: exp() });
297
+ expect((await extractPageSession(req(`page_session=${a}`), "pagea"))?.payload.pageId).toBe(
298
+ "pagea",
299
+ );
300
+ expect(await extractPageSession(req(`page_session=${a}`), "pageb")).toBeNull();
301
+ });
302
+
303
+ test("a token for another page under this page's name is ignored", async () => {
304
+ const a = await signPageSession({ pageId: "pagea", exp: exp() });
305
+ expect(await extractPageSession(req(`page_session_pageb=${a}`), "pageb")).toBeNull();
306
+ });
307
+
308
+ test("an id that is not cookie-name safe is refused", async () => {
309
+ const a = await signPageSession({ pageId: "a=b", exp: exp() });
310
+ expect(await extractPageSession(req(`page_session=${a}`), "a=b")).toBeNull();
311
+ });
312
+ });
313
+
314
+ describe("pageSessionSetCookies", () => {
315
+ const now = () => Math.floor(Date.now() / 1000);
316
+ const cookieFor = async (pageId: string, exp = now() + 3600) =>
317
+ `page_session_${pageId}=${await signPageSession({ pageId, exp })}`;
318
+ const evicted = (lines: string[]) =>
319
+ lines.slice(1).map((line) => {
320
+ expect(line).toContain("Max-Age=0");
321
+ return line.split("=")[0];
322
+ });
323
+
324
+ test("issues only the new session when the jar is under the cap", async () => {
325
+ const jar = await cookieFor("pagea");
326
+ const lines = await pageSessionSetCookies({ headers: { cookie: jar } }, "pageb", { dev: true });
327
+ expect(lines).toHaveLength(1);
328
+ expect(lines[0]!.startsWith("page_session_pageb=")).toBe(true);
329
+ });
330
+
331
+ test("evicts the oldest sessions beyond the cap", async () => {
332
+ const ids = Array.from({ length: MAX_PAGE_SESSION_COOKIES + 4 }, (_, i) => `page${i}`);
333
+ // page0 expires soonest, so it is the oldest launch.
334
+ const jar = (await Promise.all(ids.map((id, i) => cookieFor(id, now() + 1000 + i)))).join("; ");
335
+ const lines = await pageSessionSetCookies({ headers: { cookie: jar } }, "fresh", { dev: true });
336
+ expect(evicted(lines).sort()).toEqual(
337
+ ["page0", "page1", "page2", "page3", "page4"].map((id) => `page_session_${id}`).sort(),
338
+ );
339
+ });
340
+
341
+ test("breaks same-second ties toward the cookie listed later", async () => {
342
+ const exp = now() + 3600;
343
+ const ids = Array.from({ length: MAX_PAGE_SESSION_COOKIES }, (_, i) => `tie${i}`);
344
+ const jar = (await Promise.all(ids.map((id) => cookieFor(id, exp)))).join("; ");
345
+ const lines = await pageSessionSetCookies({ headers: { cookie: jar } }, "fresh", { dev: true });
346
+ expect(evicted(lines)).toEqual(["page_session_tie0"]);
347
+ });
348
+
349
+ test("evicts expired, forged and misnamed sessions but keeps legacy and own cookies", async () => {
350
+ const jar = [
351
+ await cookieFor("expired", now() - 10),
352
+ `page_session_forged=${await forgeToken({ pageId: "forged", exp: now() + 3600 }, "x")}`,
353
+ `page_session_misnamed=${await signPageSession({ pageId: "other", exp: now() + 3600 })}`,
354
+ `page_session=${await signPageSession({ pageId: "legacy", exp: now() + 3600 })}`,
355
+ await cookieFor("fresh"),
356
+ await cookieFor("kept"),
357
+ ].join("; ");
358
+ const lines = await pageSessionSetCookies({ headers: { cookie: jar } }, "fresh", {
359
+ dev: false,
360
+ });
361
+ expect(evicted(lines).sort()).toEqual([
362
+ "page_session_expired",
363
+ "page_session_forged",
364
+ "page_session_misnamed",
365
+ ]);
366
+ // Prod eviction cookies carry the same SameSite=None; Secure attributes.
367
+ for (const line of lines) expect(line).toContain("SameSite=None; Secure");
368
+ });
369
+ });
@@ -5,8 +5,9 @@
5
5
  * 2. `GET /p/:id` without cookie → 401.
6
6
  * 3. `POST /api/pages/:id/launch` (bearer) → 204 + Set-Cookie.
7
7
  * 4. `GET /p/:id` with cookie → 200 + SDK injected.
8
- * 5. `GET /p/:id` with cookie scoped to a DIFFERENT page id → 403.
9
- * 6. `GET /p/:id.json` with cookie → 200 + JSON metadata.
8
+ * 5. A session signed for a DIFFERENT page id is ignored → 401.
9
+ * 6. Two launched pages both load from one cookie jar.
10
+ * 7. `GET /p/:id.json` with cookie → 200 + JSON metadata.
10
11
  *
11
12
  * Uses the same in-process handler wiring as `pages-public-html.test.ts` so
12
13
  * we don't have to boot the full http server. The proxy in
@@ -26,7 +27,7 @@ import { closeDb, initDb } from "../be/db";
26
27
  import { handlePages } from "../http/pages";
27
28
  import { handlePagesPublic } from "../http/pages-public";
28
29
  import { getPathSegments, parseQueryParams } from "../http/utils";
29
- import { signPageSession } from "../utils/page-session";
30
+ import { MAX_PAGE_SESSION_COOKIES, signPageSession } from "../utils/page-session";
30
31
  import { listenOnFreePort } from "./test-net";
31
32
 
32
33
  const TEST_DB_PATH = "./test-pages-authed-mode.sqlite";
@@ -47,7 +48,7 @@ function createTestServer(): Server {
47
48
  /** Pull the cookie value out of a Set-Cookie header line. */
48
49
  function extractCookieValue(setCookie: string | null): string {
49
50
  expect(setCookie).toBeTruthy();
50
- const match = /page_session=([^;]+)/.exec(setCookie!);
51
+ const match = /page_session_[A-Za-z0-9_-]+=([^;]+)/.exec(setCookie!);
51
52
  expect(match).toBeTruthy();
52
53
  return match![1]!;
53
54
  }
@@ -132,7 +133,7 @@ describe("GET /p/:id — authed mode cookie gate (step-4)", () => {
132
133
  expect(text).toContain("class SwarmSDK");
133
134
  });
134
135
 
135
- test("cookie scoped to a different page id → 403", async () => {
136
+ test("a session signed for another page is ignored → 401", async () => {
136
137
  const idA = await createAuthedPage("page-a");
137
138
  const idB = await createAuthedPage("page-b");
138
139
 
@@ -140,13 +141,82 @@ describe("GET /p/:id — authed mode cookie gate (step-4)", () => {
140
141
  const exp = Math.floor(Date.now() / 1000) + 3600;
141
142
  const tokenForA = await signPageSession({ pageId: idA, exp });
142
143
 
143
- // … and try to use it for page B.
144
- const res = await fetch(`${BASE}/p/${idB}`, {
145
- headers: { Cookie: `page_session=${tokenForA}` },
146
- });
147
- expect(res.status).toBe(403);
148
- const body = (await res.json()) as { error: string };
149
- expect(body.error).toContain("different page id");
144
+ // … and try to use it for page B, under the legacy name and under B's own name.
145
+ for (const cookie of [`page_session=${tokenForA}`, `page_session_${idB}=${tokenForA}`]) {
146
+ const res = await fetch(`${BASE}/p/${idB}`, { headers: { Cookie: cookie } });
147
+ expect(res.status).toBe(401);
148
+ }
149
+ });
150
+
151
+ test("two launched pages both load from one cookie jar", async () => {
152
+ const idA = await createAuthedPage("tab-a");
153
+ const idB = await createAuthedPage("tab-b");
154
+ const jar: string[] = [];
155
+ for (const id of [idA, idB]) {
156
+ const launch = await fetch(`${BASE}/api/pages/${id}/launch`, {
157
+ method: "POST",
158
+ headers: { "X-Agent-ID": agentId },
159
+ });
160
+ expect(launch.status).toBe(204);
161
+ const setCookie = launch.headers.get("set-cookie")!;
162
+ expect(setCookie.startsWith(`page_session_${id}=`)).toBe(true);
163
+ jar.push(setCookie.split(";")[0]!);
164
+ }
165
+ const cookie = jar.join("; ");
166
+ for (const id of [idA, idB]) {
167
+ const res = await fetch(`${BASE}/p/${id}`, { headers: { Cookie: cookie } });
168
+ expect(res.status).toBe(200);
169
+ }
170
+ });
171
+
172
+ test("90 launches from one browser keep the Cookie header bounded", async () => {
173
+ // A browser jar: Set-Cookie with Max-Age=0 deletes, anything else upserts.
174
+ const jar = new Map<string, string>();
175
+ const cookieHeader = () => [...jar].map(([name, value]) => `${name}=${value}`).join("; ");
176
+ const ids: string[] = [];
177
+ for (let i = 0; i < 90; i++) {
178
+ const id = await createAuthedPage(`overflow-${i}`);
179
+ ids.push(id);
180
+ const launch = await fetch(`${BASE}/api/pages/${id}/launch`, {
181
+ method: "POST",
182
+ headers: { "X-Agent-ID": agentId, Cookie: cookieHeader() },
183
+ });
184
+ expect(launch.status).toBe(204);
185
+ for (const line of launch.headers.getSetCookie()) {
186
+ const [pair] = line.split(";");
187
+ const eq = pair!.indexOf("=");
188
+ const name = pair!.slice(0, eq);
189
+ if (/Max-Age=0(;|$)/.test(line)) jar.delete(name);
190
+ else jar.set(name, pair!.slice(eq + 1));
191
+ }
192
+ }
193
+
194
+ expect(jar.size).toBe(MAX_PAGE_SESSION_COOKIES);
195
+ expect(cookieHeader().length).toBeLessThan(8 * 1024);
196
+ // The newest launches keep their sessions; the oldest were evicted.
197
+ for (const id of ids.slice(-MAX_PAGE_SESSION_COOKIES)) {
198
+ const res = await fetch(`${BASE}/p/${id}`, { headers: { Cookie: cookieHeader() } });
199
+ expect(res.status).toBe(200);
200
+ }
201
+ expect(jar.has(`page_session_${ids[0]}`)).toBe(false);
202
+
203
+ // Control: the same sessions, operator-shaped as the dashboard mints them,
204
+ // overflow the 16 KB header limit when nothing evicts them.
205
+ const unbounded = (
206
+ await Promise.all(
207
+ ids.map(async (id) => {
208
+ const token = await signPageSession({
209
+ pageId: id,
210
+ exp: Math.floor(Date.now() / 1000) + 3600,
211
+ name: "guest-0123abcd",
212
+ op: true,
213
+ });
214
+ return `page_session_${id}=${token}`;
215
+ }),
216
+ )
217
+ ).join("; ");
218
+ const overflow = await fetch(`${BASE}/p/${ids.at(-1)}`, { headers: { Cookie: unbounded } });
219
+ expect(overflow.status).toBe(431);
150
220
  });
151
221
 
152
222
  test("/p/:id.json with cookie → 200 + JSON metadata", async () => {
@@ -47,7 +47,7 @@ function createTestServer(): Server {
47
47
 
48
48
  function extractCookieValue(setCookie: string | null): string {
49
49
  expect(setCookie).toBeTruthy();
50
- const match = /page_session=([^;]+)/.exec(setCookie!);
50
+ const match = /page_session_[A-Za-z0-9_-]+=([^;]+)/.exec(setCookie!);
51
51
  expect(match).toBeTruthy();
52
52
  return match![1]!;
53
53
  }
@@ -140,7 +140,7 @@ describe("GET /p/:id — password mode (step-5)", () => {
140
140
  expect(res.status).toBe(200);
141
141
  expect(res.headers.get("content-type")?.toLowerCase()).toContain("text/html");
142
142
  const cookieHeader = res.headers.get("set-cookie");
143
- expect(cookieHeader).toContain("page_session=");
143
+ expect(cookieHeader).toContain("page_session_");
144
144
  const text = await res.text();
145
145
  expect(text).toContain("<h1>open sesame</h1>");
146
146
  // BROWSER_SDK_JS sentinel.
@@ -158,7 +158,7 @@ describe("GET /p/:id — password mode (step-5)", () => {
158
158
  headers: { Authorization: `Basic ${basic}` },
159
159
  });
160
160
  expect(res.status).toBe(200);
161
- expect(res.headers.get("set-cookie")).toContain("page_session=");
161
+ expect(res.headers.get("set-cookie")).toContain("page_session_");
162
162
  const text = await res.text();
163
163
  expect(text).toContain("<h1>via basic</h1>");
164
164
  });
@@ -222,7 +222,7 @@ describe("GET /p/:id — password mode (step-5)", () => {
222
222
  const id = await createPasswordPage("json-key", "swordfish");
223
223
  const res = await fetch(`${BASE}/p/${id}.json?key=swordfish`);
224
224
  expect(res.status).toBe(200);
225
- expect(res.headers.get("set-cookie")).toContain("page_session=");
225
+ expect(res.headers.get("set-cookie")).toContain("page_session_");
226
226
  });
227
227
 
228
228
  test("cross-page cookie reuse → 401 + password prompt (cookie is silently ignored for password mode)", async () => {
@@ -252,7 +252,7 @@ describe("GET /p/:id — password mode (step-5)", () => {
252
252
  headers: { Cookie: `page_session=${cookieForA}` },
253
253
  });
254
254
  expect(recover.status).toBe(200);
255
- expect(recover.headers.get("set-cookie")).toContain("page_session=");
255
+ expect(recover.headers.get("set-cookie")).toContain("page_session_");
256
256
  });
257
257
 
258
258
  test("malformed Basic header (no colon) → 401 (no crash)", async () => {
@@ -340,8 +340,14 @@ describe("bounded codemode", () => {
340
340
  undefined,
341
341
  fakeCtx().ctx,
342
342
  );
343
- expect(Date.now() - started).toBeLessThan(5_000);
344
- expect(textOf(result)).toContain("Codemode worker limit reached");
343
+ const elapsed = Date.now() - started;
344
+ expect(elapsed).toBeGreaterThanOrEqual(250);
345
+ expect(elapsed).toBeLessThan(5_000);
346
+ // pi 1.4 queues the script for a worker; our deadline ends the wait.
347
+ expect(textOf(result)).toContain(
348
+ "Script aborted: Codemode script exceeded its 300 ms deadline",
349
+ );
350
+ expect(textOf(result)).toContain("The script did not start.");
345
351
  } finally {
346
352
  holders.abort(new Error("test done"));
347
353
  await Promise.all(holding);
@@ -0,0 +1,57 @@
1
+ import { expect, test } from "bun:test";
2
+ import { mkdtemp, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { CHILD_PROCESS_TEST_BUDGET_MS, expectChildOk, runChild } from "./test-proc";
6
+
7
+ test(
8
+ "preload clears inherited worker configuration before test files load",
9
+ async () => {
10
+ const directory = await mkdtemp(join(tmpdir(), "swarm-preload-env-"));
11
+ const fixture = join(directory, "worker-env.test.ts");
12
+ const keys = [
13
+ "SLACK_RENDER_V2",
14
+ "SLACK_STATUS_ICON_COMPLETED",
15
+ "SLACK_FUTURE_SETTING",
16
+ "COMB_ENABLED",
17
+ "APP_URL",
18
+ "AGENT_FS_API_URL",
19
+ "AGENT_FS_LIVE_URL",
20
+ "AGENT_FS_DEFAULT_ORG_ID",
21
+ "AGENT_FS_DEFAULT_DRIVE_ID",
22
+ "SWARM_ORG_NAME",
23
+ "SWARM_ORG_WEBSITE",
24
+ ];
25
+ try {
26
+ await Bun.write(
27
+ fixture,
28
+ `import { expect, test } from "bun:test";
29
+ const inherited = ${JSON.stringify(keys)}.filter(key => process.env[key] !== undefined);
30
+ test("worker defaults are cleared before module evaluation", () => {
31
+ expect(inherited).toEqual([]);
32
+ expect(process.env.PRELOAD_ENV_CONTROL).toBe("preserved");
33
+ process.env.SLACK_STATUS_ICON_COMPLETED = "test-icon";
34
+ });
35
+ test("suite configuration survives between tests", () => {
36
+ expect(process.env.SLACK_STATUS_ICON_COMPLETED).toBe("test-icon");
37
+ });
38
+ `,
39
+ );
40
+ const result = expectChildOk(
41
+ await runChild([process.execPath, "test", fixture], {
42
+ cwd: join(import.meta.dir, "../.."),
43
+ env: {
44
+ ...process.env,
45
+ ...Object.fromEntries(keys.map((key) => [key, "inherited-worker-setting"])),
46
+ PRELOAD_ENV_CONTROL: "preserved",
47
+ },
48
+ }),
49
+ "test runner with inherited worker env",
50
+ );
51
+ expect(result.stderr).toContain("2 pass");
52
+ } finally {
53
+ await rm(directory, { recursive: true, force: true });
54
+ }
55
+ },
56
+ CHILD_PROCESS_TEST_BUDGET_MS,
57
+ );
@@ -22,6 +22,27 @@ import { startHangWatchdog } from "./hang-watchdog";
22
22
 
23
23
  startHangWatchdog();
24
24
 
25
+ // Worker deployments configure Slack rendering, links, and organization defaults.
26
+ // Start tests from the same unset defaults as CI; suites can still opt in by
27
+ // setting their own values after the preload runs.
28
+ for (const key of Object.keys(process.env)) {
29
+ if (
30
+ key.startsWith("SLACK_") ||
31
+ [
32
+ "COMB_ENABLED",
33
+ "APP_URL",
34
+ "AGENT_FS_API_URL",
35
+ "AGENT_FS_LIVE_URL",
36
+ "AGENT_FS_DEFAULT_ORG_ID",
37
+ "AGENT_FS_DEFAULT_DRIVE_ID",
38
+ "SWARM_ORG_NAME",
39
+ "SWARM_ORG_WEBSITE",
40
+ ].includes(key)
41
+ ) {
42
+ delete process.env[key];
43
+ }
44
+ }
45
+
25
46
  // The API drain (src/be/api-drain.ts) is off unless API_DRAIN_MAX_MS is set,
26
47
  // but a shell can inherit it (a swarm worker container gets the deploy's value).
27
48
  // Test servers are stopped with SIGTERM and their fixtures leave in_progress rows
@@ -6,6 +6,7 @@ import { resolveTemplate } from "../prompts/resolver";
6
6
 
7
7
  // Side-effect imports: register all templates from each source
8
8
  import "../gitlab/templates";
9
+ import "../bitbucket/templates";
9
10
  import "../agentmail/templates";
10
11
  import "../linear/templates";
11
12
  import "../heartbeat/templates";
@@ -28,6 +29,7 @@ async function ensureTemplatesRegistered(): Promise<void> {
28
29
  // module state across parallel test files, a single-template check is racy.
29
30
  const ts = Date.now();
30
31
  await import(`../gitlab/templates?t=${ts}`);
32
+ await import(`../bitbucket/templates?t=${ts}`);
31
33
  await import(`../agentmail/templates?t=${ts}`);
32
34
  await import(`../linear/templates?t=${ts}`);
33
35
  await import(`../heartbeat/templates?t=${ts}`);
@@ -94,6 +96,42 @@ describe("template registration — all sources", () => {
94
96
  expect(eventTypes).toContain("gitlab.pipeline.failed");
95
97
  });
96
98
 
99
+ test("Bitbucket templates are registered (2 common + 3 event)", () => {
100
+ const eventTypes = getAllTemplateDefinitions().map((d) => d.eventType);
101
+ expect(eventTypes).toContain("common.delegation_instruction.bitbucket");
102
+ expect(eventTypes).toContain("common.command_suggestions.bitbucket_pr");
103
+ expect(eventTypes).toContain("bitbucket.pull_request.opened");
104
+ expect(eventTypes).toContain("bitbucket.pull_request.review_requested");
105
+ expect(eventTypes).toContain("bitbucket.comment.mentioned");
106
+ });
107
+
108
+ test("Bitbucket templates resolve with no unresolved variables", () => {
109
+ const shared = {
110
+ pr_id: 42,
111
+ pr_title: "Fix retry",
112
+ repo: "acme-ws/widgets",
113
+ username: "Dana",
114
+ pr_url: "https://bitbucket.org/acme-ws/widgets/pull-requests/42",
115
+ };
116
+ const pr = { ...shared, source_branch: "feat", target_branch: "main", context_section: "" };
117
+ for (const id of ["bitbucket.pull_request.opened", "bitbucket.pull_request.review_requested"]) {
118
+ const result = resolveTemplate(id, pr);
119
+ expect(result.skipped).toBe(false);
120
+ expect(result.unresolved).toEqual([]);
121
+ expect(result.text).toContain("[Bitbucket PR #42]");
122
+ expect(result.text).toContain("api.bitbucket.org/2.0/repositories/acme-ws/widgets");
123
+ }
124
+ const comment = resolveTemplate("bitbucket.comment.mentioned", {
125
+ ...shared,
126
+ location_section: "File: src/a.ts:3\n",
127
+ context: "please fix",
128
+ existing_task_note: "",
129
+ });
130
+ expect(comment.unresolved).toEqual([]);
131
+ expect(comment.text).toContain("File: src/a.ts:3");
132
+ expect(comment.text).toContain("please fix");
133
+ });
134
+
97
135
  test("AgentMail templates are registered (5 event)", () => {
98
136
  const all = getAllTemplateDefinitions();
99
137
  const eventTypes = all.map((d) => d.eventType);