@desplega.ai/agent-swarm 2.3.0 → 2.4.1

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 (332) hide show
  1. package/README.md +1 -0
  2. package/dist/{acp-adapter-v85a2bqy.js → acp-adapter-zvzr538d.js} +9 -9
  3. package/dist/{actions-zgcxfspj.js → actions-jhtqdw69.js} +21 -19
  4. package/dist/{amp-adapter-jw9xfz42.js → amp-adapter-bd84rhs2.js} +4 -4
  5. package/dist/{app-4e1s4675.js → app-kreq1gwg.js} +3 -3
  6. package/dist/{assistant-v238n9dk.js → assistant-3k22sbsq.js} +23 -21
  7. package/dist/{boot-reembed-p666cw5n.js → boot-reembed-e53x6b9v.js} +1 -1
  8. package/dist/{boot-reembed-w6pm64j4.js → boot-reembed-f6z923ny.js} +1 -1
  9. package/dist/{boot-scrub-sweep-jcyjagn3.js → boot-scrub-sweep-0538vsb7.js} +4 -4
  10. package/dist/{boot-seeds-gbedmamp.js → boot-seeds-6wxqkfk2.js} +4 -4
  11. package/dist/{claude-adapter-q7wspzt8.js → claude-adapter-gfts5044.js} +9 -9
  12. package/dist/{claude-managed-adapter-n7ahjyx6.js → claude-managed-adapter-h2y889ge.js} +3 -3
  13. package/dist/{claude-managed-setup-wza784fb.js → claude-managed-setup-h00hsgtz.js} +2 -2
  14. package/dist/{claude-sdk-session-yf400ptr.js → claude-sdk-session-56besmfz.js} +13 -13
  15. package/dist/{cli-65q70v2v.js → cli-0bh477zn.js} +33 -6
  16. package/dist/{cli-4ync7m60.js → cli-0ebey9hg.js} +1 -1
  17. package/dist/{cli-665j3feb.js → cli-0xk505e8.js} +1 -1
  18. package/dist/{cli-7gh7020n.js → cli-1dmcq2eh.js} +1 -1
  19. package/dist/{cli-gzvb3d4a.js → cli-1jx0srjt.js} +3 -3
  20. package/dist/{cli-zj4d6afs.js → cli-1pa3rz69.js} +21 -6
  21. package/dist/{cli-7ayprxp7.js → cli-2dfbv8qr.js} +8 -0
  22. package/dist/{cli-30m5q8mc.js → cli-3kh4kpqy.js} +6 -6
  23. package/dist/{cli-qrwp40st.js → cli-3p8fqcnh.js} +2 -2
  24. package/dist/{cli-y3dvz6ge.js → cli-3r448dqn.js} +3 -3
  25. package/dist/{cli-a17tzmn0.js → cli-3tnpj2bh.js} +7 -7
  26. package/dist/{cli-ea7chd26.js → cli-471qk6s2.js} +93 -46
  27. package/dist/{cli-a49vpbf3.js → cli-5kqnemen.js} +1 -1
  28. package/dist/{cli-0rh20bp3.js → cli-6ncjvx55.js} +3 -3
  29. package/dist/{cli-fb6t92v8.js → cli-6x9ma5w5.js} +1 -1
  30. package/dist/{cli-k28p6xh7.js → cli-7225gwj7.js} +2 -2
  31. package/dist/{cli-8cvvytad.js → cli-8nyhf7za.js} +3 -3
  32. package/dist/{cli-e09hj50q.js → cli-8wkb09q5.js} +11 -11
  33. package/dist/{cli-j0v75y13.js → cli-a0kcty51.js} +4 -4
  34. package/dist/{cli-zes4tsbb.js → cli-a4gvwttf.js} +4 -4
  35. package/dist/{cli-jhp7w1fy.js → cli-a5ebc95n.js} +2 -2
  36. package/dist/{cli-5d1m1qca.js → cli-ash6xxk7.js} +2 -2
  37. package/dist/{cli-fp4xbw07.js → cli-axh29tyr.js} +14 -12
  38. package/dist/{cli-p22zdvnt.js → cli-bn0rewsy.js} +3 -3
  39. package/dist/{cli-hz64bcnj.js → cli-bn5kevnf.js} +11 -373
  40. package/dist/{cli-8jtrekpy.js → cli-d2mb5s8a.js} +7 -7
  41. package/dist/{cli-c5sbmw1v.js → cli-d82btycs.js} +94 -15
  42. package/dist/{cli-vjfpshn3.js → cli-ejs3m1sx.js} +1 -1
  43. package/dist/{cli-yv0vgscc.js → cli-fpmsqq59.js} +18 -2
  44. package/dist/cli-fz9kewbh.js +382 -0
  45. package/dist/{cli-4s1kqrqg.js → cli-g0hgtz4p.js} +6 -6
  46. package/dist/{cli-hn3pftj2.js → cli-gf0kfg81.js} +7 -7
  47. package/dist/{cli-d43xqzbe.js → cli-hqhzg1qq.js} +2 -2
  48. package/dist/{cli-azs9bhc5.js → cli-j4rqdf2n.js} +2 -2
  49. package/dist/{cli-wtg6edvd.js → cli-jh0qcrga.js} +1 -1
  50. package/dist/{cli-kqypty34.js → cli-k9z4zd4p.js} +6 -5
  51. package/dist/{cli-jhmjwqcg.js → cli-kmqgmcnb.js} +47 -14
  52. package/dist/{cli-8qsx817b.js → cli-kr2pmfqx.js} +583 -769
  53. package/dist/{cli-4g629sgr.js → cli-m2qgh5sv.js} +20 -1
  54. package/dist/{cli-9e9kjqsj.js → cli-mfe7f7b5.js} +2 -2
  55. package/dist/{cli-x3en6ct3.js → cli-nwyw3zhg.js} +1 -1
  56. package/dist/{cli-6vcyw6v7.js → cli-nx462e4v.js} +5 -3
  57. package/dist/{cli-tr24eeny.js → cli-p0rakbrk.js} +9 -9
  58. package/dist/{cli-4hmaheb6.js → cli-p6argxh9.js} +9 -9
  59. package/dist/{cli-3avfh5az.js → cli-pemywhfq.js} +2 -2
  60. package/dist/{cli-etvn5ej2.js → cli-pnp2j5zg.js} +2 -2
  61. package/dist/{cli-wk8ccrz6.js → cli-pw7v69vp.js} +2 -2
  62. package/dist/{cli-ka01m5rb.js → cli-rdq7tzwm.js} +17 -14
  63. package/dist/{cli-re4wkbzx.js → cli-rqqsfz30.js} +677 -104
  64. package/dist/{cli-xgn4rwyv.js → cli-s48yvzb8.js} +4 -4
  65. package/dist/{cli-zv4vy2b0.js → cli-s95dknxh.js} +1 -1
  66. package/dist/{cli-g6p7c2my.js → cli-sjyetj21.js} +2 -2
  67. package/dist/{cli-rdr166ht.js → cli-smvcw9z5.js} +1 -1
  68. package/dist/{cli-fhvqt2fp.js → cli-sv5gb0yb.js} +1 -1
  69. package/dist/{cli-yv3n83x8.js → cli-syhq378v.js} +2 -2
  70. package/dist/{cli-q1812jbw.js → cli-tbhwrxpt.js} +9 -9
  71. package/dist/{cli-w7z94e6c.js → cli-tx00c0h6.js} +4 -2
  72. package/dist/{cli-dxwh4j32.js → cli-vy0fay6k.js} +46 -37
  73. package/dist/{cli-ptkpaqfy.js → cli-wt645v7q.js} +1 -1
  74. package/dist/{cli-ppapy34w.js → cli-wxbn3yfx.js} +2 -2
  75. package/dist/{cli-cby329ny.js → cli-x5mq1fyq.js} +3 -3
  76. package/dist/{cli-rc3r0bnh.js → cli-xbzvet96.js} +6 -6
  77. package/dist/{cli-4mkvwgzr.js → cli-xf234ws8.js} +1 -1
  78. package/dist/{cli-6p7ddg72.js → cli-xftrvxag.js} +5 -5
  79. package/dist/cli-z071fb95.js +848 -0
  80. package/dist/{cli-1v8w782f.js → cli-z1ew8vbq.js} +14 -7
  81. package/dist/{cli-9td5ss65.js → cli-zfg9enh9.js} +3 -3
  82. package/dist/{cli-yxqag76b.js → cli-zw88zjba.js} +1 -1
  83. package/dist/cli.js +19 -19
  84. package/dist/{codex-adapter-nhjj2veb.js → codex-adapter-p6rmm7vv.js} +6 -6
  85. package/dist/{codex-hook-x6tm6y5m.js → codex-hook-x7nejyrr.js} +8 -5
  86. package/dist/{codex-login-35p4sk85.js → codex-login-m819c41r.js} +1 -1
  87. package/dist/{codex-session-runner-6hggrjbx.js → codex-session-runner-8vd5t9t0.js} +7 -7
  88. package/dist/{commands-mrw1ph2w.js → commands-7ems2p13.js} +2 -2
  89. package/dist/{controller-pt1zam5w.js → controller-m263fg43.js} +4 -4
  90. package/dist/{controllers-connect-ymw5541d.js → controllers-connect-bg33ae53.js} +28 -24
  91. package/dist/{cursor-adapter-cf4zh5x0.js → cursor-adapter-cz6nm6mj.js} +3 -3
  92. package/dist/{db-hy80219q.js → db-w505dwa8.js} +2 -2
  93. package/dist/{db-hhpy6m4x.js → db-ys2rbdft.js} +3 -2
  94. package/dist/{devin-adapter-7egzmt0k.js → devin-adapter-zgqadvnm.js} +1 -1
  95. package/dist/{dispatcher-6pdjtkr1.js → dispatcher-0s0mckqr.js} +20 -18
  96. package/dist/{docker-njavd3fb.js → docker-fkgtr75t.js} +2 -2
  97. package/dist/{dsh-adapter-fbx06re2.js → dsh-adapter-3n3net33.js} +3 -3
  98. package/dist/{e2b-5r6a5jeb.js → e2b-7am6tmq6.js} +2 -2
  99. package/dist/{e2b-pw1nvtmh.js → e2b-thdcxtjd.js} +2 -2
  100. package/dist/{easter-egg-3d-6hg2r942.js → easter-egg-3d-5cgynaja.js} +1 -1
  101. package/dist/{event-bus-101fenkh.js → event-bus-9vpk8gmq.js} +1 -1
  102. package/dist/{execute-bcqc69a5.js → execute-t4e6m3rw.js} +1 -1
  103. package/dist/{external-channel-j6vb6ya1.js → external-channel-5b23fv8z.js} +2 -2
  104. package/dist/{google-generative-ai-phpmwxvg.js → google-generative-ai-bmq8gkf3.js} +1 -1
  105. package/dist/{google-vertex-es9ybgdy.js → google-vertex-b1rvp52s.js} +1 -1
  106. package/dist/{grok-adapter-1193sprp.js → grok-adapter-q9fghe5q.js} +10 -10
  107. package/dist/handlers-mqrym347.js +39 -0
  108. package/dist/{heartbeat-h1w1ktce.js → heartbeat-tww6yx1w.js} +23 -21
  109. package/dist/{hook-ejja4bpe.js → hook-dx0pkvar.js} +16 -9
  110. package/dist/{hook-skrbja18.js → hook-pvmh9b6m.js} +6 -6
  111. package/dist/{http-mzh41mfj.js → http-k2ejbt05.js} +975 -292
  112. package/dist/{index-6rmnn89d.js → index-2z710v4x.js} +10 -10
  113. package/dist/{index-xmmkyx82.js → index-dgz61nff.js} +20 -18
  114. package/dist/{index-wgsw9xcz.js → index-g6tbb6ve.js} +1 -1
  115. package/dist/index-h2ek08d6.js +40 -0
  116. package/dist/index-pqfwh9nn.js +41 -0
  117. package/dist/{index-epr2gnbw.js → index-x5aktmp8.js} +1 -1
  118. package/dist/{index-x0q2a4f9.js → index-ysd45kgc.js} +1 -1
  119. package/dist/{index-g909y089.js → index-zgvqzjsw.js} +8 -8
  120. package/dist/{keepalive-4djvbm2j.js → keepalive-nhvpw10z.js} +4 -4
  121. package/dist/lead-jb6sf69z.js +78 -0
  122. package/dist/{maintenance-f3xhnjsm.js → maintenance-agakc519.js} +1 -1
  123. package/dist/{oauth-refresh-sweep-fpd6x6rw.js → oauth-refresh-sweep-4dcbhtn0.js} +3 -3
  124. package/dist/{onboard-196tjgyn.js → onboard-4n2kwa4v.js} +3 -3
  125. package/dist/{opencode-adapter-eeesgfqk.js → opencode-adapter-3s7ax02e.js} +7 -7
  126. package/dist/{otel-impl-g8fh4c21.js → otel-impl-tfa12080.js} +2 -2
  127. package/dist/{pi-mono-adapter-zasp3ze3.js → pi-mono-adapter-ytp7gz8b.js} +24 -13
  128. package/dist/{pricing-refresh-sdcf5yaz.js → pricing-refresh-3cqx3f3x.js} +3 -3
  129. package/dist/{rbac-roles-bsp5657v.js → rbac-roles-9fae5rnv.js} +5 -3
  130. package/dist/{rbac-roles-2x4g1wb5.js → rbac-roles-y6zbfzve.js} +2 -2
  131. package/dist/{render-v2-0pyb39bb.js → render-v2-emawjvc4.js} +3 -3
  132. package/dist/{resume-a86tkj6r.js → resume-zhgts7h7.js} +10 -8
  133. package/dist/{runtime-ar8310ba.js → runtime-xe3aqy4x.js} +1 -1
  134. package/dist/session-access-cm823e06.js +25 -0
  135. package/dist/{setup-ypbzcapd.js → setup-tkp99w1e.js} +1 -1
  136. package/dist/{virtual-modules-wzk6jf3a.js → virtual-modules-4r3zkc20.js} +2 -2
  137. package/dist/{work-objects-yjp8de12.js → work-objects-f722h263.js} +23 -21
  138. package/dist/worker-p7f336rq.js +78 -0
  139. package/dist/{x-8ewt8gxn.js → x-y45mthwn.js} +2 -2
  140. package/openapi.json +1086 -305
  141. package/package.json +2 -2
  142. package/src/apps/action-caller.ts +6 -0
  143. package/src/apps/action-runner.ts +146 -0
  144. package/src/apps/row-store.ts +4 -2
  145. package/src/apps/script-types.ts +17 -4
  146. package/src/be/db/active-sessions.ts +5 -0
  147. package/src/be/db/session-visibility.ts +29 -0
  148. package/src/be/db/tasks/read.ts +61 -3
  149. package/src/be/db/tasks/write.ts +15 -6
  150. package/src/be/db.ts +106 -37
  151. package/src/be/events.ts +11 -0
  152. package/src/be/fleet-benchmark-stats.ts +10 -5
  153. package/src/be/migrations/230_session_shares.sql +23 -0
  154. package/src/be/models-catalog.ts +4 -0
  155. package/src/be/onboarding.ts +6 -0
  156. package/src/be/rbac-roles.ts +2 -0
  157. package/src/be/scripts/typecheck.ts +13 -0
  158. package/src/be/session-shares.ts +244 -0
  159. package/src/be/swarm-config-guard.ts +33 -3
  160. package/src/be/telemetry-enable.ts +1 -1
  161. package/src/commands/provider-credentials.ts +12 -2
  162. package/src/commands/runner.ts +14 -0
  163. package/src/heartbeat/heartbeat.ts +2 -2
  164. package/src/hooks/codex-hook.ts +6 -2
  165. package/src/hooks/hook.ts +7 -0
  166. package/src/hooks/pr-body-guard.ts +38 -11
  167. package/src/hooks/pr-room-guard.ts +88 -0
  168. package/src/http/active-sessions.ts +13 -2
  169. package/src/http/agents.ts +11 -3
  170. package/src/http/all-routes.ts +1 -0
  171. package/src/http/api-keys.ts +3 -1
  172. package/src/http/approval-requests.ts +2 -1
  173. package/src/http/apps.ts +118 -190
  174. package/src/http/config.ts +3 -0
  175. package/src/http/context.ts +2 -0
  176. package/src/http/core.ts +39 -2
  177. package/src/http/events.ts +6 -2
  178. package/src/http/favorite-owner.ts +1 -1
  179. package/src/http/fleet-benchmark.ts +79 -30
  180. package/src/http/fs.ts +38 -3
  181. package/src/http/global-config.ts +7 -0
  182. package/src/http/index.ts +3 -1
  183. package/src/http/memory.ts +3 -1
  184. package/src/http/ranking.ts +351 -0
  185. package/src/http/realtime.ts +26 -21
  186. package/src/http/route-def.ts +77 -11
  187. package/src/http/script-runs.ts +2 -1
  188. package/src/http/scripts.ts +8 -0
  189. package/src/http/session-access.ts +179 -0
  190. package/src/http/session-data.ts +10 -2
  191. package/src/http/sessions.ts +212 -12
  192. package/src/http/stats.ts +7 -3
  193. package/src/http/tasks.ts +85 -8
  194. package/src/http/telemetry-ui.ts +1 -1
  195. package/src/http/used-by.ts +64 -20
  196. package/src/http/users.ts +2 -2
  197. package/src/prompts/session-templates.ts +3 -1
  198. package/src/providers/amp-adapter.ts +1 -1
  199. package/src/providers/pi-mono-adapter.ts +15 -0
  200. package/src/providers/reasoning-effort.ts +2 -1
  201. package/src/providers/types.ts +2 -0
  202. package/src/ranking/telemetry-ranking.ts +119 -0
  203. package/src/ranking/usage.ts +57 -0
  204. package/src/rbac/admission.ts +22 -7
  205. package/src/rbac/legacy-policy.ts +2 -0
  206. package/src/rbac/permissions.ts +9 -0
  207. package/src/realtime/auth.ts +69 -0
  208. package/src/realtime/tickets.ts +4 -1
  209. package/src/realtime/transport.ts +77 -9
  210. package/src/scripts-runtime/sdk-allowlist.ts +7 -0
  211. package/src/scripts-runtime/types/stdlib.d.ts +31 -0
  212. package/src/scripts-runtime/types/swarm-sdk.d.ts +31 -0
  213. package/src/server.ts +4 -0
  214. package/src/slack/blocks.ts +18 -0
  215. package/src/slack/dm.ts +29 -0
  216. package/src/telemetry-context.ts +19 -1
  217. package/src/telemetry-contract/EVENTS.md +45 -8
  218. package/src/telemetry-contract/SOURCE.json +38 -4
  219. package/src/telemetry-contract/catalog.json +294 -5
  220. package/src/telemetry-contract/ranking/fixtures/expected.json +30 -0
  221. package/src/telemetry-contract/ranking/fixtures/invalid/bad-category.json +11 -0
  222. package/src/telemetry-contract/ranking/fixtures/invalid/bad-claim.json +11 -0
  223. package/src/telemetry-contract/ranking/fixtures/invalid/blank-description.json +11 -0
  224. package/src/telemetry-contract/ranking/fixtures/invalid/blank-name.json +11 -0
  225. package/src/telemetry-contract/ranking/fixtures/invalid/description-too-long.json +11 -0
  226. package/src/telemetry-contract/ranking/fixtures/invalid/domain-uppercase.json +11 -0
  227. package/src/telemetry-contract/ranking/fixtures/invalid/domain-with-scheme.json +11 -0
  228. package/src/telemetry-contract/ranking/fixtures/invalid/domain-with-www.json +11 -0
  229. package/src/telemetry-contract/ranking/fixtures/invalid/http-source-url.json +11 -0
  230. package/src/telemetry-contract/ranking/fixtures/invalid/missing-description.json +10 -0
  231. package/src/telemetry-contract/ranking/fixtures/invalid/missing-version.json +10 -0
  232. package/src/telemetry-contract/ranking/fixtures/invalid/name-not-string.json +11 -0
  233. package/src/telemetry-contract/ranking/fixtures/invalid/name-too-long.json +11 -0
  234. package/src/telemetry-contract/ranking/fixtures/invalid/not-object.json +1 -0
  235. package/src/telemetry-contract/ranking/fixtures/invalid/null.json +1 -0
  236. package/src/telemetry-contract/ranking/fixtures/invalid/other-with-subcategory.json +11 -0
  237. package/src/telemetry-contract/ranking/fixtures/invalid/oversize.json +1 -0
  238. package/src/telemetry-contract/ranking/fixtures/invalid/short-claim.json +11 -0
  239. package/src/telemetry-contract/ranking/fixtures/invalid/subcategory-other-category.json +11 -0
  240. package/src/telemetry-contract/ranking/fixtures/invalid/trailing-data.json +1 -0
  241. package/src/telemetry-contract/ranking/fixtures/invalid/unknown-field.json +12 -0
  242. package/src/telemetry-contract/ranking/fixtures/invalid/unsupported-version.json +11 -0
  243. package/src/telemetry-contract/ranking/fixtures/invalid/updated-at-not-date.json +11 -0
  244. package/src/telemetry-contract/ranking/fixtures/invalid/updated-at-offset.json +11 -0
  245. package/src/telemetry-contract/ranking/fixtures/invalid/wrong-case-field.json +11 -0
  246. package/src/telemetry-contract/ranking/fixtures/valid/category-without-subcategory.json +11 -0
  247. package/src/telemetry-contract/ranking/fixtures/valid/full.json +11 -0
  248. package/src/telemetry-contract/ranking/fixtures/valid/minimal.json +11 -0
  249. package/src/telemetry-contract/ranking/fixtures/valid/no-claim.json +10 -0
  250. package/src/telemetry-contract/ranking/fixtures/valid/padded-text.json +11 -0
  251. package/src/telemetry-contract/ranking/fixtures/valid/unicode-name.json +11 -0
  252. package/src/telemetry-contract/ranking/listing.v1.schema.json +61 -0
  253. package/src/telemetry-contract/ranking/taxonomy.json +8 -0
  254. package/src/telemetry-contract/ranking-taxonomy.gen.ts +11 -0
  255. package/src/telemetry-contract/types.gen.ts +963 -58
  256. package/src/telemetry-identity.ts +47 -9
  257. package/src/telemetry-snapshot.ts +9 -1
  258. package/src/telemetry-ui-events.ts +9 -2
  259. package/src/telemetry.ts +40 -5
  260. package/src/tests/app-action-run-tool.test.ts +178 -0
  261. package/src/tests/app-row-tools.test.ts +360 -0
  262. package/src/tests/apps-rbac.test.ts +189 -2
  263. package/src/tests/apps-script-types.test.ts +16 -3
  264. package/src/tests/credential-check.test.ts +38 -0
  265. package/src/tests/db-extraction-contract.test.ts +1 -0
  266. package/src/tests/db-task-read-contract.test.ts +1 -0
  267. package/src/tests/docs-index-footprint.test.js +2 -2
  268. package/src/tests/fleet-benchmark.test.ts +122 -2
  269. package/src/tests/http-users.test.ts +62 -1
  270. package/src/tests/model-catalog-table.test.ts +63 -0
  271. package/src/tests/model-profiles-api.test.ts +19 -7
  272. package/src/tests/models-catalog.test.ts +50 -0
  273. package/src/tests/onboarding.test.ts +21 -0
  274. package/src/tests/org-identity.test.ts +44 -0
  275. package/src/tests/org-website.test.ts +62 -0
  276. package/src/tests/pi-mono-adapter.test.ts +23 -0
  277. package/src/tests/pr-body-leaks.test.ts +4 -0
  278. package/src/tests/pr-room-guard.test.ts +242 -0
  279. package/src/tests/ranking-listing.test.ts +97 -0
  280. package/src/tests/ranking-route.test.ts +321 -0
  281. package/src/tests/ranking-usage.test.ts +50 -0
  282. package/src/tests/rbac-admission-e2e.test.ts +8 -0
  283. package/src/tests/rbac-admission.test.ts +38 -0
  284. package/src/tests/rbac-charact-http.test.ts +4 -3
  285. package/src/tests/rbac-engine.test.ts +2 -0
  286. package/src/tests/rbac-roles.test.ts +2 -0
  287. package/src/tests/realtime-room-surfaces.test.ts +3 -1
  288. package/src/tests/realtime-session-join-race.test.ts +142 -0
  289. package/src/tests/realtime-tickets.test.ts +24 -17
  290. package/src/tests/realtime-transport.test.ts +91 -0
  291. package/src/tests/reasoning-effort.test.ts +39 -3
  292. package/src/tests/runner-harness-telemetry-env.test.ts +56 -1
  293. package/src/tests/session-route-posture.test.ts +639 -0
  294. package/src/tests/session-sharing-routes.test.ts +282 -0
  295. package/src/tests/session-sharing.test.ts +526 -0
  296. package/src/tests/sessions-detail-batch.test.ts +7 -1
  297. package/src/tests/slack-tool-blocks.test.ts +101 -0
  298. package/src/tests/slack-update.test.ts +55 -0
  299. package/src/tests/slack-user-dm.test.ts +177 -0
  300. package/src/tests/swarm-tool-result-gate.test.ts +2 -4
  301. package/src/tests/telemetry-context.test.ts +128 -2
  302. package/src/tests/telemetry-ui-route.test.ts +123 -0
  303. package/src/tests/tool-annotations.test.ts +3 -2
  304. package/src/tests/used-by-route.test.ts +153 -9
  305. package/src/tests/workflow-agent-task-retry.test.ts +130 -4
  306. package/src/tests/workflow-wait-recovery.test.ts +116 -4
  307. package/src/tools/app-action-run.ts +63 -0
  308. package/src/tools/app-rows.ts +234 -0
  309. package/src/tools/model-catalog/index.ts +1 -1
  310. package/src/tools/slack-post.ts +18 -12
  311. package/src/tools/slack-read.ts +13 -1
  312. package/src/tools/slack-reply.ts +7 -10
  313. package/src/tools/slack-start-thread.ts +18 -11
  314. package/src/tools/slack-update.ts +2 -9
  315. package/src/tools/tool-config.ts +8 -0
  316. package/src/tools/utils.ts +1 -1
  317. package/src/tools/workflows/retry-workflow-run.ts +1 -1
  318. package/src/types.ts +52 -0
  319. package/src/utils/company-profile.ts +24 -0
  320. package/src/utils/org-identity.ts +33 -0
  321. package/src/utils/org-website.ts +58 -21
  322. package/src/workflows/recovery.ts +10 -22
  323. package/src/workflows/resume.ts +16 -0
  324. package/src/workflows/task-step-routing.ts +47 -7
  325. package/templates/skills/apps/SKILL.md +58 -2
  326. package/templates/skills/apps/content.md +58 -2
  327. package/dist/handlers-tb3xgjnm.js +0 -37
  328. package/dist/index-nyfcdpky.js +0 -39
  329. package/dist/index-vjn3wbzf.js +0 -38
  330. package/dist/lead-61kqp604.js +0 -76
  331. package/dist/worker-4ptnn1ge.js +0 -76
  332. package/dist/{cli-n5mrsg0f.js → cli-60cgh1rv.js} +3 -3
@@ -0,0 +1,526 @@
1
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
2
+ import { Readable } from "node:stream";
3
+ import {
4
+ closeDb,
5
+ createAgent,
6
+ createTaskExtended,
7
+ createUser,
8
+ getDbClient,
9
+ initDb,
10
+ } from "../be/db";
11
+ import {
12
+ getSessionRoot,
13
+ listSessionShareHistory,
14
+ sessionAccess,
15
+ setSessionShares,
16
+ } from "../be/session-shares";
17
+ import { handleRealtimeTicket } from "../http/realtime";
18
+ import { handleSessions } from "../http/sessions";
19
+ import { handleTasks } from "../http/tasks";
20
+ import { getPathSegments, parseQueryParams } from "../http/utils";
21
+ import { authorizeRoomNamespace } from "../realtime/auth";
22
+ import { redeemRealtimeTicket, sweepRealtimeTickets } from "../realtime/tickets";
23
+ import type { HttpRequestAuth } from "../utils/request-auth-context";
24
+ import { setRequestAuth } from "../utils/request-auth-context";
25
+
26
+ const OPERATOR: HttpRequestAuth = { kind: "operator", fingerprint: "op:test" };
27
+
28
+ type Call = {
29
+ auth?: HttpRequestAuth;
30
+ body?: unknown;
31
+ /** Dashboard picked identity (`X-Swarm-User-Id`). */
32
+ dashboardUserId?: string;
33
+ agentId?: string;
34
+ };
35
+
36
+ function jsonReq(method: string, url: string, body?: unknown) {
37
+ const raw = body === undefined ? "" : JSON.stringify(body);
38
+ const req = Readable.from(raw ? [Buffer.from(raw)] : []) as Readable & {
39
+ method: string;
40
+ url: string;
41
+ headers: Record<string, string>;
42
+ };
43
+ req.method = method;
44
+ req.url = url;
45
+ req.headers = { "content-type": "application/json" };
46
+ return req;
47
+ }
48
+
49
+ async function call(
50
+ handler: "sessions" | "tasks" | "ticket",
51
+ method: string,
52
+ url: string,
53
+ opts: Call = {},
54
+ ): Promise<{ status: number; body: any }> {
55
+ const req = jsonReq(method, url, opts.body);
56
+ if (opts.dashboardUserId) req.headers["x-swarm-user-id"] = opts.dashboardUserId;
57
+ if (opts.agentId) req.headers["x-agent-id"] = opts.agentId;
58
+ setRequestAuth(req as never, opts.auth ?? OPERATOR);
59
+ let status = 200;
60
+ const chunks: string[] = [];
61
+ const res = {
62
+ setHeader: () => {},
63
+ writeHead: (code: number) => {
64
+ status = code;
65
+ },
66
+ end: (chunk?: string) => {
67
+ if (chunk) chunks.push(chunk);
68
+ },
69
+ };
70
+ const segments = getPathSegments(url);
71
+ const query = parseQueryParams(url);
72
+ const handled =
73
+ handler === "sessions"
74
+ ? await handleSessions(req as never, res as never, segments, query, opts.agentId)
75
+ : handler === "tasks"
76
+ ? await handleTasks(req as never, res as never, segments, query, opts.agentId)
77
+ : await handleRealtimeTicket(req as never, res as never, segments, query);
78
+ expect(handled).toBe(true);
79
+ return { status, body: chunks.length > 0 ? JSON.parse(chunks.join("")) : null };
80
+ }
81
+
82
+ let owner: { id: string };
83
+ let eze: { id: string };
84
+ let mar: { id: string };
85
+ let ezeAuth: HttpRequestAuth;
86
+
87
+ beforeAll(async () => {
88
+ initDb(":memory:");
89
+ owner = await createUser({ name: "Owner" });
90
+ eze = await createUser({ name: "Eze" });
91
+ mar = await createUser({ name: "Mar" });
92
+ ezeAuth = { kind: "user", userId: eze.id, user: eze as never };
93
+ });
94
+
95
+ afterAll(() => {
96
+ sweepRealtimeTickets(Number.MAX_SAFE_INTEGER);
97
+ closeDb();
98
+ });
99
+
100
+ beforeEach(async () => {
101
+ await getDbClient().run("DELETE FROM session_shares");
102
+ });
103
+
104
+ async function session(requestedByUserId?: string) {
105
+ const root = await createTaskExtended("Fix flaky deploy", { requestedByUserId, source: "ui" });
106
+ const child = await createTaskExtended("follow-up", {
107
+ requestedByUserId,
108
+ parentTaskId: root.id,
109
+ source: "ui",
110
+ });
111
+ return { root, child, rootRef: { id: root.id, requestedByUserId: requestedByUserId ?? null } };
112
+ }
113
+
114
+ describe("sessionAccess", () => {
115
+ test("owner, ownerless roots, private by default, best active share, child resolves to root", async () => {
116
+ const { root, child, rootRef } = await session(owner.id);
117
+ const ownerless = await session();
118
+
119
+ expect(await sessionAccess(owner.id, root.id)).toBe("owner");
120
+ expect(await sessionAccess(eze.id, ownerless.child.id)).toBe("owner");
121
+ // No rows: private to the owner.
122
+ expect(await sessionAccess(eze.id, root.id)).toBe("none");
123
+ expect(await sessionAccess(null, root.id)).toBe("none");
124
+
125
+ await setSessionShares(
126
+ rootRef,
127
+ { everyone: "read", users: [{ userId: eze.id, access: "write" }] },
128
+ owner.id,
129
+ );
130
+ expect(await sessionAccess(eze.id, child.id)).toBe("write");
131
+ expect(await sessionAccess(mar.id, child.id)).toBe("read");
132
+ expect(await sessionAccess(null, root.id)).toBe("read");
133
+ expect((await getSessionRoot(child.id))?.id).toBe(root.id);
134
+
135
+ // Revoked rows grant nothing.
136
+ await setSessionShares(rootRef, { everyone: "none", users: [] }, owner.id);
137
+ expect(await sessionAccess(eze.id, child.id)).toBe("none");
138
+ expect(await sessionAccess(mar.id, child.id)).toBe("none");
139
+ });
140
+
141
+ test("a chain deeper than 1000 tasks still resolves to its actual root", async () => {
142
+ const { root, child, rootRef } = await session(owner.id);
143
+ let leaf = child;
144
+ for (let depth = 2; depth <= 1002; depth++) {
145
+ leaf = await createTaskExtended(`step ${depth}`, {
146
+ requestedByUserId: owner.id,
147
+ parentTaskId: leaf.id,
148
+ source: "ui",
149
+ });
150
+ }
151
+ await setSessionShares(
152
+ rootRef,
153
+ { everyone: "none", users: [{ userId: eze.id, access: "write" }] },
154
+ owner.id,
155
+ );
156
+
157
+ expect((await getSessionRoot(leaf.id))?.id).toBe(root.id);
158
+ expect(await sessionAccess(eze.id, leaf.id)).toBe("write");
159
+ });
160
+ });
161
+
162
+ describe("setSessionShares", () => {
163
+ test("an access change revokes and re-inserts; untouched rows keep their id; nothing is deleted", async () => {
164
+ const { rootRef } = await session(owner.id);
165
+ await setSessionShares(
166
+ rootRef,
167
+ {
168
+ everyone: "none",
169
+ users: [
170
+ { userId: eze.id, access: "read" },
171
+ { userId: mar.id, access: "read" },
172
+ ],
173
+ },
174
+ owner.id,
175
+ );
176
+ const first = await listSessionShareHistory(rootRef.id);
177
+ const marRow = first.find((row) => row.userId === mar.id)!;
178
+
179
+ const state = await setSessionShares(
180
+ rootRef,
181
+ {
182
+ everyone: "none",
183
+ users: [
184
+ { userId: eze.id, access: "write" },
185
+ { userId: mar.id, access: "read" },
186
+ // The owner is never stored.
187
+ { userId: owner.id, access: "read" },
188
+ ],
189
+ },
190
+ owner.id,
191
+ );
192
+ expect(state.users.map((u) => [u.userId, u.access]).sort()).toEqual(
193
+ [
194
+ [eze.id, "write"],
195
+ [mar.id, "read"],
196
+ ].sort(),
197
+ );
198
+
199
+ const history = await listSessionShareHistory(rootRef.id);
200
+ const ezeRows = history.filter((row) => row.userId === eze.id);
201
+ expect(ezeRows).toHaveLength(2);
202
+ expect(ezeRows[0]).toMatchObject({ access: "read", revokedBy: owner.id });
203
+ expect(ezeRows[0]!.revokedAt).not.toBeNull();
204
+ expect(ezeRows[1]).toMatchObject({ access: "write", revokedAt: null });
205
+ expect(history.find((row) => row.userId === mar.id && !row.revokedAt)?.id).toBe(marRow.id);
206
+ expect(history.some((row) => row.userId === owner.id)).toBe(false);
207
+
208
+ // Remove = revoke: the row stays.
209
+ await setSessionShares(rootRef, { everyone: "none", users: [] }, owner.id);
210
+ const after = await listSessionShareHistory(rootRef.id);
211
+ expect(after).toHaveLength(3);
212
+ expect(after.every((row) => row.revokedAt !== null)).toBe(true);
213
+ });
214
+
215
+ test("the partial unique index allows one active row per target", async () => {
216
+ const { rootRef } = await session(owner.id);
217
+ await setSessionShares(rootRef, { everyone: "read", users: [] }, owner.id);
218
+ const insert = () =>
219
+ getDbClient().run(
220
+ "INSERT INTO session_shares (id, rootTaskId, userId, access, createdAt) VALUES (?, ?, NULL, 'write', ?)",
221
+ [crypto.randomUUID(), rootRef.id, new Date().toISOString()],
222
+ );
223
+ await expect(insert()).rejects.toThrow();
224
+ await setSessionShares(rootRef, { everyone: "none", users: [] }, owner.id);
225
+ await insert();
226
+ });
227
+ });
228
+
229
+ describe("session routes", () => {
230
+ test("detail: 403 without a share, access + owner with one, owner for agents and the bare operator key", async () => {
231
+ const { root } = await session(owner.id);
232
+
233
+ const denied = await call("sessions", "GET", `/api/sessions/${root.id}`, {
234
+ dashboardUserId: eze.id,
235
+ });
236
+ expect(denied.status).toBe(403);
237
+
238
+ const asOwner = await call("sessions", "GET", `/api/sessions/${root.id}`, {
239
+ dashboardUserId: owner.id,
240
+ });
241
+ expect(asOwner.status).toBe(200);
242
+ expect(asOwner.body).toMatchObject({ access: "owner", owner: { id: owner.id, name: "Owner" } });
243
+
244
+ const put = await call("sessions", "PUT", `/api/sessions/${root.id}/shares`, {
245
+ dashboardUserId: owner.id,
246
+ body: { everyone: "none", users: [{ userId: eze.id, access: "read" }] },
247
+ });
248
+ expect(put.status).toBe(200);
249
+ expect(put.body).toMatchObject({
250
+ everyone: "none",
251
+ users: [{ userId: eze.id, access: "read" }],
252
+ });
253
+
254
+ const shared = await call("sessions", "GET", `/api/sessions/${root.id}`, { auth: ezeAuth });
255
+ expect(shared.body.access).toBe("read");
256
+
257
+ for (const opts of [{}, { agentId: crypto.randomUUID(), dashboardUserId: eze.id }]) {
258
+ const bypass = await call("sessions", "GET", `/api/sessions/${root.id}`, opts);
259
+ expect(bypass.status).toBe(200);
260
+ expect(bypass.body.access).toBe("owner");
261
+ }
262
+ });
263
+
264
+ test("shares: owner only, unknown users rejected, ownerless sessions refused", async () => {
265
+ const { root } = await session(owner.id);
266
+ const ownerless = await session();
267
+
268
+ const byEze = await call("sessions", "PUT", `/api/sessions/${root.id}/shares`, {
269
+ auth: ezeAuth,
270
+ body: { everyone: "write", users: [] },
271
+ });
272
+ expect(byEze.status).toBe(403);
273
+
274
+ const unknown = await call("sessions", "PUT", `/api/sessions/${root.id}/shares`, {
275
+ dashboardUserId: owner.id,
276
+ body: { everyone: "none", users: [{ userId: "nobody", access: "read" }] },
277
+ });
278
+ expect(unknown.status).toBe(400);
279
+
280
+ const open = await call("sessions", "PUT", `/api/sessions/${ownerless.root.id}/shares`, {
281
+ body: { everyone: "read", users: [] },
282
+ });
283
+ expect(open.status).toBe(409);
284
+
285
+ const read = await call("sessions", "GET", `/api/sessions/${root.id}/shares`, {
286
+ dashboardUserId: eze.id,
287
+ });
288
+ expect(read.status).toBe(403);
289
+ });
290
+
291
+ test("list: default is mine + ownerless + shared; scope narrows it", async () => {
292
+ const mine = await session(eze.id);
293
+ const shared = await session(owner.id);
294
+ const privateOne = await session(owner.id);
295
+ const ownerless = await session();
296
+ await setSessionShares(
297
+ shared.rootRef,
298
+ { everyone: "none", users: [{ userId: eze.id, access: "read" }] },
299
+ owner.id,
300
+ );
301
+
302
+ const ids = async (query: string) => {
303
+ const res = await call("sessions", "GET", `/api/sessions?limit=100${query}`, {
304
+ dashboardUserId: eze.id,
305
+ });
306
+ return res.body.sessions.map((s: { root: { id: string } }) => s.root.id) as string[];
307
+ };
308
+ const all = await ids("");
309
+ expect(all).toContain(mine.root.id);
310
+ expect(all).toContain(shared.root.id);
311
+ expect(all).toContain(ownerless.root.id);
312
+ expect(all).not.toContain(privateOne.root.id);
313
+ expect(await ids("&scope=mine")).toEqual([mine.root.id]);
314
+ expect(await ids("&scope=shared")).toEqual([shared.root.id]);
315
+
316
+ const operator = await call("sessions", "GET", "/api/sessions?limit=100");
317
+ const operatorIds = operator.body.sessions.map((s: { root: { id: string } }) => s.root.id);
318
+ expect(operatorIds).toContain(privateOne.root.id);
319
+ const operatorScoped = await call("sessions", "GET", "/api/sessions?scope=shared");
320
+ expect(operatorScoped.body.sessions).toEqual([]);
321
+ });
322
+ });
323
+
324
+ describe("task routes", () => {
325
+ test("GET /api/tasks/:id needs read access for a user, not for agents or the bare operator key", async () => {
326
+ const { root, child, rootRef } = await session(owner.id);
327
+ const userToken: HttpRequestAuth = ezeAuth;
328
+
329
+ for (const task of [root, child]) {
330
+ expect(
331
+ (await call("tasks", "GET", `/api/tasks/${task.id}`, { auth: userToken })).status,
332
+ ).toBe(403);
333
+ expect(
334
+ (await call("tasks", "GET", `/api/tasks/${task.id}`, { dashboardUserId: eze.id })).status,
335
+ ).toBe(403);
336
+ expect(
337
+ (await call("tasks", "GET", `/api/tasks/${task.id}`, { dashboardUserId: owner.id })).status,
338
+ ).toBe(200);
339
+ expect((await call("tasks", "GET", `/api/tasks/${task.id}`)).status).toBe(200);
340
+ expect(
341
+ (
342
+ await call("tasks", "GET", `/api/tasks/${task.id}`, {
343
+ agentId: crypto.randomUUID(),
344
+ dashboardUserId: eze.id,
345
+ })
346
+ ).status,
347
+ ).toBe(200);
348
+ }
349
+
350
+ await setSessionShares(
351
+ rootRef,
352
+ { everyone: "none", users: [{ userId: eze.id, access: "read" }] },
353
+ owner.id,
354
+ );
355
+ const shared = await call("tasks", "GET", `/api/tasks/${child.id}`, { auth: userToken });
356
+ expect(shared.status).toBe(200);
357
+ expect(shared.body.task).toBe("follow-up");
358
+ });
359
+
360
+ test("GET /api/tasks drops a user's unreadable sessions from every shape, and total counts what is shown", async () => {
361
+ const root = await createTaskExtended("PRIVATE ROOT ALPHA", {
362
+ requestedByUserId: owner.id,
363
+ source: "ui",
364
+ });
365
+ const child = await createTaskExtended("PRIVATE CHILD BETA", {
366
+ requestedByUserId: owner.id,
367
+ parentTaskId: root.id,
368
+ source: "ui",
369
+ });
370
+ const rootRef = { id: root.id, requestedByUserId: owner.id };
371
+ const ownerless = await session();
372
+ const shapes = ["", "&fields=slim", "&fields=timeline", "&fields=full"];
373
+ const list = async (query: string, opts: Call) => {
374
+ const { status, body } = await call("tasks", "GET", `/api/tasks?limit=1000${query}`, opts);
375
+ expect(status).toBe(200);
376
+ return body as { tasks: { id: string; task?: string }[]; total?: number };
377
+ };
378
+
379
+ for (const shape of shapes) {
380
+ const body = await list(`${shape}&includeTotal=true`, { auth: ezeAuth });
381
+ const ids = body.tasks.map((task) => task.id);
382
+ expect(ids).not.toContain(root.id);
383
+ expect(ids).not.toContain(child.id);
384
+ expect(ids).toContain(ownerless.child.id);
385
+ expect(JSON.stringify(body.tasks)).not.toContain("PRIVATE");
386
+ expect(body.total).toBe(body.tasks.length);
387
+ }
388
+ // A search hit on hidden text counts nothing, so `total` is no oracle.
389
+ for (const shape of shapes) {
390
+ const hit = await list(`${shape}&search=PRIVATE%20ROOT&includeTotal=true`, { auth: ezeAuth });
391
+ expect(hit).toEqual({ tasks: [], total: 0 });
392
+ }
393
+
394
+ for (const opts of [{}, { dashboardUserId: owner.id }, { agentId: crypto.randomUUID() }]) {
395
+ for (const shape of shapes) {
396
+ const body = await list(`${shape}&search=PRIVATE&includeTotal=true`, opts);
397
+ expect(body.tasks.map((task) => task.id).sort()).toEqual([root.id, child.id].sort());
398
+ expect(body.total).toBe(2);
399
+ }
400
+ }
401
+
402
+ await setSessionShares(rootRef, { everyone: "read", users: [] }, owner.id);
403
+ for (const shape of shapes) {
404
+ const body = await list(`${shape}&search=PRIVATE&includeTotal=true`, { auth: ezeAuth });
405
+ expect(body.tasks.map((task) => task.id).sort()).toEqual([root.id, child.id].sort());
406
+ expect(body.total).toBe(2);
407
+ }
408
+ });
409
+ });
410
+
411
+ describe("write gate", () => {
412
+ test("POST /api/tasks under someone else's session needs a write share", async () => {
413
+ const { root, child, rootRef } = await session(owner.id);
414
+ const followUp = (opts: Call) =>
415
+ call("tasks", "POST", "/api/tasks", {
416
+ ...opts,
417
+ body: { task: "please also check the logs", parentTaskId: child.id, source: "ui" },
418
+ });
419
+
420
+ expect((await followUp({ dashboardUserId: eze.id })).status).toBe(403);
421
+ await setSessionShares(
422
+ rootRef,
423
+ { everyone: "read", users: [{ userId: mar.id, access: "write" }] },
424
+ owner.id,
425
+ );
426
+ const readOnly = await followUp({ dashboardUserId: eze.id });
427
+ expect(readOnly.status).toBe(403);
428
+ expect(readOnly.body.error).toContain("can't write in this session");
429
+
430
+ expect((await followUp({ dashboardUserId: mar.id })).status).toBe(201);
431
+ expect((await followUp({ dashboardUserId: owner.id })).status).toBe(201);
432
+ // No picked user: the operator key keeps working.
433
+ expect((await followUp({})).status).toBe(201);
434
+ // Agents continue chains without a share.
435
+ const agent = await createAgent({ name: "chain worker", isLead: true, status: "idle" });
436
+ expect((await followUp({ agentId: agent.id, dashboardUserId: eze.id })).status).toBe(201);
437
+ expect(root.id).toBeTruthy();
438
+ });
439
+
440
+ test("POST /api/tasks/:id/steer needs a write share, and a write share lets a user token steer", async () => {
441
+ const previous = process.env.STEERING_ENABLED;
442
+ process.env.STEERING_ENABLED = "true";
443
+ try {
444
+ const { child, rootRef } = await session(owner.id);
445
+ const steer = (opts: Call) =>
446
+ call("tasks", "POST", `/api/tasks/${child.id}/steer`, {
447
+ ...opts,
448
+ body: { message: "use the staging cluster" },
449
+ });
450
+
451
+ const none = await steer({ auth: ezeAuth });
452
+ expect(none.status).toBe(403);
453
+ expect(none.body.error).toContain("can't write in this session");
454
+
455
+ await setSessionShares(rootRef, { everyone: "read", users: [] }, owner.id);
456
+ expect((await steer({ dashboardUserId: eze.id })).status).toBe(403);
457
+
458
+ await setSessionShares(
459
+ rootRef,
460
+ { everyone: "read", users: [{ userId: eze.id, access: "write" }] },
461
+ owner.id,
462
+ );
463
+ // Past both gates: whatever the steering store answers, it is not a 403.
464
+ expect((await steer({ auth: ezeAuth })).status).not.toBe(403);
465
+ expect((await steer({ dashboardUserId: eze.id })).status).not.toBe(403);
466
+
467
+ // An ownerless (Slack, schedule) session reads as `owner` for everyone,
468
+ // but a user token still may not steer a task it did not request.
469
+ const ownerless = await session();
470
+ const slackSteer = await call("tasks", "POST", `/api/tasks/${ownerless.child.id}/steer`, {
471
+ auth: ezeAuth,
472
+ body: { message: "use the staging cluster" },
473
+ });
474
+ expect(slackSteer.status).toBe(403);
475
+ expect(slackSteer.body.error).toContain("cannot steer this task");
476
+ } finally {
477
+ if (previous === undefined) delete process.env.STEERING_ENABLED;
478
+ else process.env.STEERING_ENABLED = previous;
479
+ }
480
+ });
481
+ });
482
+
483
+ describe("session presence", () => {
484
+ test("rooms need read access, dashboard auth, and presence-only ops", async () => {
485
+ const { root, child, rootRef } = await session(owner.id);
486
+ const ns = `presence:session:${root.id}`;
487
+
488
+ expect(await authorizeRoomNamespace(ns, { userId: owner.id }, "join")).toBeNull();
489
+ expect(await authorizeRoomNamespace(ns, { userId: eze.id }, "join")).toContain("access");
490
+ expect(await authorizeRoomNamespace(ns, { isOperator: true }, "join")).toBeNull();
491
+ expect(
492
+ await authorizeRoomNamespace(ns, { isOperator: true, dashboardUserId: eze.id }, "presence"),
493
+ ).toContain("access");
494
+ expect(await authorizeRoomNamespace(ns, { agentId: "a1", isOperator: true }, "join")).toContain(
495
+ "dashboard",
496
+ );
497
+ expect(await authorizeRoomNamespace(ns, { userId: owner.id }, "update")).toContain(
498
+ "only allow",
499
+ );
500
+ expect(
501
+ await authorizeRoomNamespace(`presence:session:${child.id}`, { userId: owner.id }, "join"),
502
+ ).toContain("not found");
503
+
504
+ await setSessionShares(rootRef, { everyone: "read", users: [] }, owner.id);
505
+ expect(await authorizeRoomNamespace(ns, { userId: eze.id }, "join")).toBeNull();
506
+ expect(
507
+ await authorizeRoomNamespace(ns, { isOperator: true, dashboardUserId: eze.id }, "presence"),
508
+ ).toBeNull();
509
+ });
510
+
511
+ test("the ticket route works with Comb off and carries the picked dashboard user", async () => {
512
+ const previous = process.env.COMB_ENABLED;
513
+ process.env.COMB_ENABLED = "false";
514
+ try {
515
+ const res = await call("ticket", "POST", "/api/realtime/ticket", { dashboardUserId: eze.id });
516
+ expect(res.status).toBe(200);
517
+ expect(redeemRealtimeTicket(res.body.ticket)).toMatchObject({
518
+ kind: "operator",
519
+ dashboardUserId: eze.id,
520
+ });
521
+ } finally {
522
+ if (previous === undefined) delete process.env.COMB_ENABLED;
523
+ else process.env.COMB_ENABLED = previous;
524
+ }
525
+ });
526
+ });
@@ -50,7 +50,13 @@ async function unbatchedSession(id: string) {
50
50
  ...(await getTaskSteeringFields(task)),
51
51
  citations: await getTaskCitations(task.id),
52
52
  });
53
- return { root: await decorate(root), chain: await Promise.all(chain.map(decorate)) };
53
+ return {
54
+ root: await decorate(root),
55
+ chain: await Promise.all(chain.map(decorate)),
56
+ // Session sharing: the bare operator key bypasses access checks, and the fixture root has no owner.
57
+ access: "owner",
58
+ owner: null,
59
+ };
54
60
  }
55
61
 
56
62
  beforeAll(async () => {
@@ -10,6 +10,7 @@ import {
10
10
  initDb,
11
11
  recordSlackMessage,
12
12
  } from "../be/db";
13
+ import { splitSlackSectionText } from "../slack/blocks";
13
14
  import { slackContextKey } from "../tasks/context-key";
14
15
  import { registerSlackPostTool } from "../tools/slack-post";
15
16
  import { registerSlackReplyTool } from "../tools/slack-reply";
@@ -104,6 +105,35 @@ afterAll(async () => {
104
105
  await removeDbFiles();
105
106
  });
106
107
 
108
+ // 3,946 chars of multi-line text: over Slack's 3,000-char section cap, under
109
+ // the tools' 4,000-char message cap (the size that failed with invalid_blocks).
110
+ const LONG_MESSAGE = Array.from(
111
+ { length: 120 },
112
+ (_, index) => `line ${String(index).padStart(3, "0")} radar item with some words`,
113
+ )
114
+ .join("\n")
115
+ .slice(0, 3946);
116
+
117
+ type PostedBlock = { type: string; text?: { type: string; text: string } };
118
+
119
+ function expectSplitSections(posted: { text: string; blocks: PostedBlock[] }) {
120
+ const sections = posted.blocks.filter((block) => block.type === "section");
121
+ expect(sections.length).toBeGreaterThanOrEqual(2);
122
+ for (const section of sections) {
123
+ expect(section.text?.type).toBe("mrkdwn");
124
+ expect(section.text!.text.length).toBeLessThanOrEqual(2900);
125
+ }
126
+ // Chunks only drop the whitespace at split points; every other char survives in order.
127
+ const joined = sections.map((section) => section.text!.text).join("");
128
+ expect(joined.replace(/\s/g, "")).toBe(posted.text.replace(/\s/g, ""));
129
+ let cursor = 0;
130
+ for (const section of sections) {
131
+ const at = posted.text.indexOf(section.text!.text, cursor);
132
+ expect(at).toBeGreaterThanOrEqual(cursor);
133
+ cursor = at + section.text!.text.length;
134
+ }
135
+ }
136
+
107
137
  describe("Slack tool Block Kit support", () => {
108
138
  test("every direct engine Slack post/update disables link and media unfurls", async () => {
109
139
  const callSites: Array<{ path: string; line: number; source: string }> = [];
@@ -283,6 +313,77 @@ describe("Slack tool Block Kit support", () => {
283
313
  expect((await getSlackMessageByChannelTs("C_START", lastPostedTs))?.kind).toBe("agent");
284
314
  });
285
315
 
316
+ test("default blocks split a 3,946-char message into sections under Slack's cap", async () => {
317
+ expect(LONG_MESSAGE.length).toBe(3946);
318
+ const tools = buildTools();
319
+ const cases = [
320
+ ["slack-post", { channelId: "C_LONG_POST", message: LONG_MESSAGE }, leadId],
321
+ ["slack-reply", { taskId, message: LONG_MESSAGE }, workerId],
322
+ ["slack-start-thread", { channelId: "C_LONG_START", message: LONG_MESSAGE }, leadId],
323
+ ] as const;
324
+
325
+ for (const [name, args, agentId] of cases) {
326
+ mockChatPostMessage.mockClear();
327
+ const result = await tools[name]!.handler(args, meta(agentId));
328
+ expect(result.structuredContent.success).toBe(true);
329
+ const posted = mockChatPostMessage.mock.calls[0]![0] as {
330
+ text: string;
331
+ blocks: PostedBlock[];
332
+ };
333
+ expect(posted.text.length).toBe(3946);
334
+ expectSplitSections(posted);
335
+ }
336
+ });
337
+
338
+ test("default blocks keep a 2,931-char message with an emoji at 2,900 in one intact section", async () => {
339
+ // 2,931 UTF-16 units: under Slack's 3,000-char cap, with a surrogate pair
340
+ // straddling the 2,900-char hard-split index.
341
+ const message = `${"x".repeat(2899)}😀${"y".repeat(30)}`;
342
+ expect(message.length).toBe(2931);
343
+ const tools = buildTools();
344
+ const cases = [
345
+ ["slack-post", { channelId: "C_EMOJI_POST", message }, leadId],
346
+ ["slack-reply", { taskId, message }, workerId],
347
+ ["slack-start-thread", { channelId: "C_EMOJI_START", message }, leadId],
348
+ ] as const;
349
+
350
+ for (const [name, args, agentId] of cases) {
351
+ mockChatPostMessage.mockClear();
352
+ const result = await tools[name]!.handler(args, meta(agentId));
353
+ expect(result.structuredContent.success).toBe(true);
354
+ const posted = mockChatPostMessage.mock.calls[0]![0] as { blocks: PostedBlock[] };
355
+ const sections = posted.blocks.filter((block) => block.type === "section");
356
+ expect(sections).toEqual([{ type: "section", text: { type: "mrkdwn", text: message } }]);
357
+ }
358
+ });
359
+
360
+ test("splitter never cuts a surrogate pair on a hard split", () => {
361
+ const text = `${"x".repeat(2899)}😀${"y".repeat(200)}`;
362
+ const chunks = splitSlackSectionText(text);
363
+ expect(chunks.length).toBeGreaterThanOrEqual(2);
364
+ for (const chunk of chunks) {
365
+ const last = chunk.charCodeAt(chunk.length - 1);
366
+ const first = chunk.charCodeAt(0);
367
+ expect(last >= 0xd800 && last <= 0xdbff).toBe(false);
368
+ expect(first >= 0xdc00 && first <= 0xdfff).toBe(false);
369
+ expect(chunk.length).toBeLessThanOrEqual(2900);
370
+ }
371
+ expect(chunks.join("")).toBe(text);
372
+ });
373
+
374
+ test("default blocks keep a short message in exactly one section", async () => {
375
+ const tools = buildTools();
376
+ const result = await tools["slack-post"]!.handler(
377
+ { channelId: "C_SHORT", message: "short message" },
378
+ meta(leadId),
379
+ );
380
+
381
+ expect(result.structuredContent.success).toBe(true);
382
+ expect(mockChatPostMessage.mock.calls[0]![0].blocks).toEqual([
383
+ { type: "section", text: { type: "mrkdwn", text: "short message" } },
384
+ ]);
385
+ });
386
+
286
387
  test("rejects 50 supplied blocks when the task provenance footer would exceed Slack's cap", async () => {
287
388
  const tools = buildTools();
288
389
  const blocks = Array.from({ length: 50 }, (_, index) => ({