@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,119 @@
1
+ /**
2
+ * The `telemetry_ranking` listing: what an install opted into the public ranking
3
+ * shows on agent-swarm.dev/ranking. Stored stringified in the global
4
+ * `swarm_config` key `telemetry_ranking` and sent as the `listing` property of
5
+ * `ranking.opted_in`.
6
+ *
7
+ * The proxy owns the contract (`src/telemetry-contract/ranking/`, vendored by
8
+ * `bun run sync:telemetry-contract`). This schema and the proxy's Go decoder are
9
+ * both held to its fixtures. Pure: imported by the API and the dashboard.
10
+ */
11
+ import { z } from "zod";
12
+ import { RANKING_TAXONOMY } from "../telemetry-contract/ranking-taxonomy.gen";
13
+ import type { OrgIdentityStatus } from "../utils/org-identity";
14
+ import { normalizeOrgWebsite } from "../utils/org-website";
15
+
16
+ export { RANKING_TAXONOMY };
17
+
18
+ export const TELEMETRY_RANKING_KEY = "telemetry_ranking";
19
+ export const PUBLIC_RANKING_ENABLED_KEY = "PUBLIC_RANKING_ENABLED";
20
+ export const TELEMETRY_RANKING_MAX_BYTES = 2048;
21
+
22
+ export const RANKING_CATEGORIES = Object.keys(RANKING_TAXONOMY) as [
23
+ keyof typeof RANKING_TAXONOMY,
24
+ ...(keyof typeof RANKING_TAXONOMY)[],
25
+ ];
26
+ const hostname = z.string().refine((v) => normalizeOrgWebsite(v) === v, "bare hostname");
27
+ const nullableText = (max: number) => z.string().trim().min(1).max(max).nullable();
28
+
29
+ function inCategory(category: string, subcategory: string | null): boolean {
30
+ if (subcategory === null) return true;
31
+ const subs: readonly string[] = RANKING_TAXONOMY[category as keyof typeof RANKING_TAXONOMY];
32
+ return subs?.includes(subcategory) ?? false;
33
+ }
34
+
35
+ export const telemetryRankingV1 = z
36
+ .strictObject({
37
+ v: z.literal(1),
38
+ /** Minted by v2.4.0 for the retired ranking PR. Still parsed, never written; the proxy ignores it. */
39
+ claim: z
40
+ .string()
41
+ .regex(/^rk_[0-9a-f]{32}$/)
42
+ .optional(),
43
+ name: z.string().trim().min(1).max(60),
44
+ domain: hostname,
45
+ category: z.enum(RANKING_CATEGORIES),
46
+ subcategory: z
47
+ .string()
48
+ .regex(/^[a-z0-9]+(-[a-z0-9]+)*$/)
49
+ .max(40)
50
+ .nullable(),
51
+ description: nullableText(160),
52
+ sourceCodeUrl: z
53
+ .url({ protocol: /^https$/ })
54
+ .max(200)
55
+ .nullable(),
56
+ updatedAt: z.iso.datetime(),
57
+ })
58
+ .refine((l) => inCategory(l.category, l.subcategory), {
59
+ path: ["subcategory"],
60
+ message: "not in this category",
61
+ });
62
+
63
+ /** Discriminated on `v`; a v2 is added here, never by editing v1. */
64
+ export const telemetryRankingSchema = z.discriminatedUnion("v", [telemetryRankingV1]);
65
+ export type TelemetryRanking = z.infer<typeof telemetryRankingSchema>;
66
+
67
+ export function parseTelemetryRanking(raw: string): TelemetryRanking {
68
+ if (new TextEncoder().encode(raw).length > TELEMETRY_RANKING_MAX_BYTES) {
69
+ throw new Error(`telemetry_ranking exceeds ${TELEMETRY_RANKING_MAX_BYTES} bytes`);
70
+ }
71
+ return telemetryRankingSchema.parse(JSON.parse(raw));
72
+ }
73
+
74
+ /** The stored listing, or null when it is missing or no longer valid. */
75
+ export function readTelemetryRanking(raw: string | null | undefined): TelemetryRanking | null {
76
+ if (!raw?.trim()) return null;
77
+ try {
78
+ return parseTelemetryRanking(raw);
79
+ } catch {
80
+ return null;
81
+ }
82
+ }
83
+
84
+ /** What the admin picks in the ranking form. Name and domain are never typed here. */
85
+ export const rankingFormSchema = z.object({
86
+ category: z.enum(RANKING_CATEGORIES),
87
+ subcategory: z.string().nullable().optional(),
88
+ description: z.string().nullable().optional(),
89
+ sourceCodeUrl: z.string().nullable().optional(),
90
+ });
91
+ export type RankingForm = z.infer<typeof rankingFormSchema>;
92
+
93
+ const blankToNull = (value: string | null | undefined) => value?.trim() || null;
94
+
95
+ /**
96
+ * Build and validate the listing. Name and domain are copied from the org
97
+ * identity (`SWARM_ORG_NAME`, `SWARM_ORG_WEBSITE`); throws when the identity is
98
+ * not set or a form field breaks the contract.
99
+ */
100
+ export function buildListing(
101
+ identity: OrgIdentityStatus,
102
+ form: RankingForm,
103
+ now: Date = new Date(),
104
+ ): TelemetryRanking {
105
+ if (!identity.ok || !identity.name || !identity.website) {
106
+ throw new Error(`Org identity is missing: ${identity.missing.join(", ")}`);
107
+ }
108
+ const listing = {
109
+ v: 1 as const,
110
+ name: identity.name,
111
+ domain: identity.website,
112
+ category: form.category,
113
+ subcategory: blankToNull(form.subcategory),
114
+ description: blankToNull(form.description),
115
+ sourceCodeUrl: blankToNull(form.sourceCodeUrl),
116
+ updatedAt: now.toISOString(),
117
+ };
118
+ return parseTelemetryRanking(JSON.stringify(listing));
119
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Worker side of the public ranking: the `ranking.usage` properties for one
3
+ * session. Pure, so the runner and its test share the gate.
4
+ */
5
+ import { isEnvFlagEnabled } from "../utils/env-flag";
6
+ import { orgIdentityStatus } from "../utils/org-identity";
7
+
8
+ export interface SessionTokens {
9
+ inputTokens?: number | null;
10
+ outputTokens?: number | null;
11
+ cacheReadTokens?: number | null;
12
+ cacheWriteTokens?: number | null;
13
+ }
14
+
15
+ export interface RankingUsageProps {
16
+ /** The same model and provider `session.cost` reports for the session. */
17
+ model: string;
18
+ provider: string;
19
+ totalTokens: number;
20
+ inputTokens: number;
21
+ outputTokens: number;
22
+ cacheReadTokens?: number;
23
+ cacheWriteTokens?: number;
24
+ }
25
+
26
+ const tokens = (value: number | null | undefined) =>
27
+ typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
28
+
29
+ /**
30
+ * Null unless telemetry is on, `PUBLIC_RANKING_ENABLED` is set and both org
31
+ * identity envs are set. Carries token counts plus the model and provider
32
+ * (for the per-model chart), never the task, agent or cost.
33
+ */
34
+ export function rankingUsageProps(
35
+ cost: SessionTokens,
36
+ session: { model: string; provider: string },
37
+ env: NodeJS.ProcessEnv = process.env,
38
+ ): RankingUsageProps | null {
39
+ if (!isEnvFlagEnabled("ANONYMIZED_TELEMETRY", true, env)) return null;
40
+ if (!isEnvFlagEnabled("PUBLIC_RANKING_ENABLED", false, env)) return null;
41
+ const identity = { SWARM_ORG_NAME: env.SWARM_ORG_NAME, SWARM_ORG_WEBSITE: env.SWARM_ORG_WEBSITE };
42
+ if (!orgIdentityStatus(identity).ok) return null;
43
+ const input = tokens(cost.inputTokens);
44
+ const output = tokens(cost.outputTokens);
45
+ const cacheRead = tokens(cost.cacheReadTokens);
46
+ const cacheWrite = tokens(cost.cacheWriteTokens);
47
+ return {
48
+ model: session.model,
49
+ provider: session.provider,
50
+ // The unified sum, as computeContextUsedUnified counts it.
51
+ totalTokens: input + cacheRead + cacheWrite + output,
52
+ inputTokens: input,
53
+ outputTokens: output,
54
+ ...(cost.cacheReadTokens != null ? { cacheReadTokens: cacheRead } : {}),
55
+ ...(cost.cacheWriteTokens != null ? { cacheWriteTokens: cacheWrite } : {}),
56
+ };
57
+ }
@@ -8,7 +8,20 @@ export type AdmissionGrant = {
8
8
  };
9
9
  };
10
10
 
11
- export type AdmissionRbac = { permission: PermissionVerb } | { ungated: string };
11
+ /**
12
+ * `alsoAdmits` names verbs that admit the route as well as `permission`, for a
13
+ * route that serves two features (each handler check still names its own verb).
14
+ */
15
+ export type AdmissionRbac =
16
+ | { permission: PermissionVerb; alsoAdmits?: readonly PermissionVerb[] }
17
+ | { ungated: string };
18
+
19
+ function grantedVerb(
20
+ rbac: { permission: PermissionVerb; alsoAdmits?: readonly PermissionVerb[] },
21
+ grant: AdmissionGrant,
22
+ ): PermissionVerb | undefined {
23
+ return [rbac.permission, ...(rbac.alsoAdmits ?? [])].find((verb) => grant.verbs.has(verb));
24
+ }
12
25
 
13
26
  export type AdmissionDecision =
14
27
  | { allow: true; verb?: PermissionVerb }
@@ -29,10 +42,11 @@ export function decideAdmission(input: {
29
42
  }
30
43
 
31
44
  if (input.rbac && "permission" in input.rbac) {
32
- const verb = input.rbac.permission;
33
- if (input.grant.verbs.has(verb)) {
34
- return { allow: true, verb };
45
+ const granted = grantedVerb(input.rbac, input.grant);
46
+ if (granted) {
47
+ return { allow: true, verb: granted };
35
48
  }
49
+ const verb = input.rbac.permission;
36
50
  return { allow: false, reason: `admission: missing permission '${verb}'`, verb };
37
51
  }
38
52
 
@@ -54,10 +68,11 @@ export function decideToolAdmission(input: {
54
68
  }
55
69
 
56
70
  if (input.rbac && "permission" in input.rbac) {
57
- const verb = input.rbac.permission;
58
- if (input.grant.verbs.has(verb)) {
59
- return { allow: true, verb };
71
+ const granted = grantedVerb(input.rbac, input.grant);
72
+ if (granted) {
73
+ return { allow: true, verb: granted };
60
74
  }
75
+ const verb = input.rbac.permission;
61
76
  return { allow: false, reason: `admission: missing permission '${verb}'`, verb };
62
77
  }
63
78
 
@@ -285,6 +285,8 @@ export const LEGACY_POLICY = {
285
285
  "task.action.own": requesterOwnsTask,
286
286
  "task.fs.mutate": taskFsMutate,
287
287
  "task.parent.attach": taskFsMutate,
288
+ "session.share": operatorOrUser,
289
+ "session.presence": operatorOrUser,
288
290
  "task.progress.write": taskProgressWrite,
289
291
  "favorite.write.own": anyAuthenticated,
290
292
  "memory.learning.inject": leadOnly,
@@ -89,6 +89,15 @@ export const PERMISSIONS = {
89
89
  "Write a task's progress, status, or output (its assignee, a lead, a human, or anyone while unassigned).",
90
90
  namespace: "task",
91
91
  },
92
+ "session.share": {
93
+ description:
94
+ "Change who can view or edit a session the principal owns (the owner check runs in the handler).",
95
+ namespace: "task",
96
+ },
97
+ "session.presence": {
98
+ description: "Join the presence room of a session the principal can view.",
99
+ namespace: "task",
100
+ },
92
101
  "favorite.write.own": {
93
102
  description: "Set a favorite the caller owns.",
94
103
  namespace: "favorite",
@@ -1,6 +1,7 @@
1
1
  import type { IncomingMessage } from "node:http";
2
2
  import { getAgentById, getTaskById } from "../be/db";
3
3
  import { getUserGrant } from "../be/rbac-roles";
4
+ import { getSessionRoot, sessionAccessForRoot } from "../be/session-shares";
4
5
  import { getCombConfig } from "../comb/config";
5
6
  import { mcpOverflowAuthError } from "../kv-overflow";
6
7
  import { reservedNamespaceError } from "../kv-reserved-namespaces";
@@ -19,6 +20,8 @@ export type RoomAuthorizationInfo = RoomNamespaceInfo & {
19
20
  callOrigin?: "http" | "mcp" | "ws";
20
21
  userId?: string;
21
22
  isOperator?: boolean;
23
+ /** The user an operator-key dashboard tab picked (`X-Swarm-User-Id`), captured on its ticket. */
24
+ dashboardUserId?: string;
22
25
  };
23
26
 
24
27
  export type ResolvedRoomNamespace = {
@@ -39,6 +42,7 @@ export type RoomNamespaceOperation =
39
42
 
40
43
  const COMB_PRESENCE_NAMESPACE = /^presence:comb:[A-Za-z0-9_-]{1,64}:[A-Za-z0-9_-]{1,64}$/;
41
44
  const COMB_PRESENCE_OPERATIONS = new Set<RoomNamespaceOperation>(["join", "leave", "presence"]);
45
+ const SESSION_PRESENCE_NAMESPACE = /^presence:session:([A-Za-z0-9_-]{1,64})$/;
42
46
 
43
47
  function pageNamespace(pageId: string): string | null {
44
48
  try {
@@ -162,6 +166,10 @@ export async function authorizeRoomNamespace(
162
166
  return null;
163
167
  }
164
168
 
169
+ if (namespace.startsWith("presence:session:")) {
170
+ return authorizeSessionPresence(namespace, info, access);
171
+ }
172
+
165
173
  const reservedError = reservedNamespaceError(namespace);
166
174
  if (reservedError) return reservedError;
167
175
 
@@ -203,3 +211,64 @@ export async function authorizeRoomNamespace(
203
211
 
204
212
  return null;
205
213
  }
214
+
215
+ /**
216
+ * Bus topic published after a session's shares change. Sockets in that
217
+ * session's presence room re-check access and are evicted when it ended.
218
+ */
219
+ export function sessionAccessTopic(rootTaskId: string): string {
220
+ return `session-access:${rootTaskId}`;
221
+ }
222
+
223
+ /** The root task id of a `presence:session:<rootTaskId>` namespace, else undefined. */
224
+ export function sessionPresenceRoot(namespace: string): string | undefined {
225
+ return SESSION_PRESENCE_NAMESPACE.exec(namespace)?.[1];
226
+ }
227
+
228
+ /**
229
+ * `presence:session:<rootTaskId>`: who is viewing (and typing in) a session.
230
+ * Presence-only, dashboard-only, and gated on read access so it never reveals
231
+ * who is inside a private session. An operator-key tab with no picked user
232
+ * keeps the access it has everywhere else.
233
+ */
234
+ async function authorizeSessionPresence(
235
+ namespace: string,
236
+ info: RoomAuthorizationInfo,
237
+ access: boolean | RoomNamespaceOperation,
238
+ ): Promise<string | null> {
239
+ const match = SESSION_PRESENCE_NAMESPACE.exec(namespace);
240
+ if (!match) return "invalid session presence namespace";
241
+ if (info.pageId || info.agentId || (!info.isOperator && !info.userId)) {
242
+ return "Session presence requires dashboard authentication";
243
+ }
244
+ if (typeof access !== "string" || !COMB_PRESENCE_OPERATIONS.has(access)) {
245
+ return "Session presence rooms only allow join, leave, and presence";
246
+ }
247
+ const principal = info.isOperator
248
+ ? ({ kind: "operator" } as const)
249
+ : ({ kind: "user", userId: info.userId! } as const);
250
+ if (
251
+ !can({
252
+ principal,
253
+ verb: "session.presence",
254
+ resource: { kind: "kv-namespace", namespace },
255
+ source: info.callOrigin === "mcp" ? "mcp" : "http",
256
+ }).allow
257
+ ) {
258
+ return "Session presence requires the session.presence permission";
259
+ }
260
+ if (info.userId && isRbacEnabled()) {
261
+ const grant = await getUserGrant(info.userId);
262
+ if (!grant.grantsAll && !grant.verbs.has("session.presence")) {
263
+ return "Session presence requires the session.presence permission";
264
+ }
265
+ }
266
+ const viewerId = info.isOperator ? info.dashboardUserId : info.userId;
267
+ if (info.isOperator && !viewerId) return null;
268
+ const root = await getSessionRoot(match[1]!);
269
+ if (!root || root.id !== match[1]) return "session not found";
270
+ if ((await sessionAccessForRoot(viewerId ?? null, root)) === "none") {
271
+ return "You don't have access to this session";
272
+ }
273
+ return null;
274
+ }
@@ -3,7 +3,10 @@ import type { HttpRequestAuth } from "../utils/request-auth-context";
3
3
  export const REALTIME_TICKET_TTL_MS = 60_000;
4
4
  export const REALTIME_TICKET_CAPACITY = 10_000;
5
5
 
6
- export type RealtimeTicketAuth = Extract<HttpRequestAuth, { kind: "operator" | "user" }>;
6
+ export type RealtimeTicketAuth = Extract<HttpRequestAuth, { kind: "operator" | "user" }> & {
7
+ /** The user an operator-key dashboard tab picked (`X-Swarm-User-Id`), for session presence. */
8
+ dashboardUserId?: string;
9
+ };
7
10
 
8
11
  type StoredTicket = {
9
12
  auth: RealtimeTicketAuth;
@@ -10,7 +10,12 @@ import { extractAndVerifyCookie } from "../utils/page-session";
10
10
  import type { HttpRequestAuth } from "../utils/request-auth-context";
11
11
  import { scrubSecrets } from "../utils/secret-scrubber";
12
12
  import { registerSessionTokenConnection } from "../utils/session-token-connections";
13
- import { authorizeRoomNamespace, resolveRoomNamespace } from "./auth";
13
+ import {
14
+ authorizeRoomNamespace,
15
+ resolveRoomNamespace,
16
+ sessionAccessTopic,
17
+ sessionPresenceRoot,
18
+ } from "./auth";
14
19
  import { realtimeBus } from "./bus";
15
20
  import {
16
21
  addWorkerSubscription,
@@ -64,6 +69,7 @@ type Identity = {
64
69
  pageId?: string;
65
70
  userId?: string;
66
71
  isOperator?: boolean;
72
+ dashboardUserId?: string;
67
73
  };
68
74
  type Viewer = { userId: string; name: string; kind: "user" | "guest" | "agent" };
69
75
  type AuthenticatedConnection = {
@@ -81,7 +87,7 @@ function header(req: IncomingMessage, name: string): string | undefined {
81
87
 
82
88
  function identityFromHttpAuth(
83
89
  req: IncomingMessage,
84
- auth: HttpRequestAuth,
90
+ auth: HttpRequestAuth & { dashboardUserId?: string },
85
91
  ): AuthenticatedConnection {
86
92
  if (auth.kind === "controller") throw new Error("Controller tokens cannot use realtime");
87
93
  if (auth.kind === "agent") {
@@ -110,6 +116,7 @@ function identityFromHttpAuth(
110
116
  auth.kind === "agent" ? (auth.taskId ?? undefined) : header(req, "x-source-task-id"),
111
117
  userId: auth.kind === "user" ? auth.userId : undefined,
112
118
  isOperator: auth.kind === "operator",
119
+ dashboardUserId: auth.kind === "operator" ? auth.dashboardUserId : undefined,
113
120
  } satisfies Identity,
114
121
  me: {
115
122
  userId: auth.kind === "user" ? auth.userId : (agentId ?? guestId),
@@ -228,7 +235,12 @@ function connect(
228
235
  )
229
236
  : undefined;
230
237
  void auth.sessionToken?.check().catch(() => ws.close(1008, "Session token revoked or expired"));
231
- const rooms = new Map<string, { room: LiveRoom; version: number; stop: () => void }>();
238
+ // `suspended`: a session presence room whose access is being re-checked
239
+ // after a share change; nothing is forwarded until the check passes.
240
+ const rooms = new Map<
241
+ string,
242
+ { room: LiveRoom; version: number; stop: () => void; suspended?: boolean }
243
+ >();
232
244
  const channels = new Map<string, () => void>();
233
245
  const pending = new Map<number, number>();
234
246
  const clientId = crypto.getRandomValues(new Uint32Array(1))[0]!;
@@ -313,6 +325,9 @@ function connect(
313
325
  if (namespace.startsWith("presence:comb:") && msg.name !== "default") {
314
326
  throw new Error("Comb presence uses the default room");
315
327
  }
328
+ if (namespace.startsWith("presence:session:") && msg.name !== "default") {
329
+ throw new Error("Session presence uses the default room");
330
+ }
316
331
  const denied = await authorizeRoomNamespace(namespace, auth.identity, msg.op, msg.name);
317
332
  if (denied) throw new Error(typeof denied === "string" ? denied : "Forbidden");
318
333
  if (closed) return;
@@ -352,12 +367,14 @@ function connect(
352
367
  room = await getRoom(namespace, msg.name, msg.schemaVersion, { create: false });
353
368
  } catch (error) {
354
369
  if (!(error instanceof Error) || error.message !== "room does not exist") throw error;
355
- // Creating a room is a write. Comb presence rooms take only presence
356
- // operations, so they check creation as a join.
370
+ // Creating a room is a write. Comb and session presence rooms take
371
+ // only presence operations, so they check creation as a join.
357
372
  const creationDenied = await authorizeRoomNamespace(
358
373
  namespace,
359
374
  auth.identity,
360
- namespace.startsWith("presence:comb:") ? "join" : true,
375
+ namespace.startsWith("presence:comb:") || namespace.startsWith("presence:session:")
376
+ ? "join"
377
+ : true,
361
378
  );
362
379
  if (creationDenied) throw new Error(creationDenied);
363
380
  if (closed) return;
@@ -365,6 +382,7 @@ function connect(
365
382
  }
366
383
  if (closed) return;
367
384
  const stopRoom = realtimeBus.subscribe(key, (event) => {
385
+ if (rooms.get(key)?.suspended) return;
368
386
  const view = event as ReturnType<typeof roomView> & { type: string };
369
387
  const type = view.type;
370
388
  if (type === "deleted") {
@@ -387,17 +405,67 @@ function connect(
387
405
  .catch(() => ws.close(1011, "Room reset failed"));
388
406
  }
389
407
  });
390
- const stopPresence = realtimeBus.subscribe(`presence:${key}`, (peers) =>
391
- send({ type: "presence", name: msg.name, namespace, peers }),
392
- );
408
+ const stopPresence = realtimeBus.subscribe(`presence:${key}`, (peers) => {
409
+ if (rooms.get(key)?.suspended) return;
410
+ send({ type: "presence", name: msg.name, namespace, peers });
411
+ });
412
+ const sessionRoot = sessionPresenceRoot(namespace);
413
+ let accessCheck = 0;
414
+ // A share change can end this viewer's read access: stop forwarding
415
+ // at once, then evict, or resume if access remains.
416
+ const stopAccess = sessionRoot
417
+ ? realtimeBus.subscribe(sessionAccessTopic(sessionRoot), () => {
418
+ const joined = rooms.get(key);
419
+ if (!joined) return;
420
+ joined.suspended = true;
421
+ const check = ++accessCheck;
422
+ void authorizeRoomNamespace(namespace, auth.identity, "join")
423
+ .catch(() => "Session access check failed")
424
+ .then((denied) => {
425
+ const current = rooms.get(key);
426
+ if (closed || !current || check !== accessCheck) return;
427
+ if (denied) {
428
+ leave(key);
429
+ send({ type: "error", name: msg.name, namespace, error: denied });
430
+ return;
431
+ }
432
+ current.suspended = false;
433
+ send({
434
+ type: "presence",
435
+ name: msg.name,
436
+ namespace,
437
+ peers: [...current.room.awareness.getStates().values()],
438
+ });
439
+ });
440
+ })
441
+ : undefined;
393
442
  rooms.set(key, {
394
443
  room,
395
444
  version: msg.schemaVersion,
396
445
  stop: () => {
397
446
  stopRoom();
398
447
  stopPresence();
448
+ stopAccess?.();
399
449
  },
450
+ suspended: sessionRoot !== undefined,
400
451
  });
452
+ if (sessionRoot) {
453
+ // A share change published while the room loaded reached no
454
+ // subscriber, so check again now that the access subscriber is
455
+ // live. Anything later is that subscriber's to handle.
456
+ const check = ++accessCheck;
457
+ const denied = await authorizeRoomNamespace(namespace, auth.identity, "join").catch(
458
+ () => "Session access check failed",
459
+ );
460
+ if (closed) return;
461
+ if (denied) {
462
+ leave(key);
463
+ throw new Error(denied);
464
+ }
465
+ const current = rooms.get(key);
466
+ if (!current) throw new Error("You don't have access to this session");
467
+ if (check === accessCheck) current.suspended = false;
468
+ }
401
469
  room.awareness.states.set(clientId, { ...auth.me, data: {} });
402
470
  room.awareness.meta.set(clientId, { clock: 1, lastUpdated: Date.now() });
403
471
  publishPresence(room);
@@ -180,6 +180,13 @@ export const SDK_TOOL_NAME_MAP = {
180
180
  app_list: "app-list",
181
181
  app_patch: "app-patch",
182
182
  app_query: "app-query",
183
+ app_rows_list: "app-rows-list",
184
+ app_row_get: "app-row-get",
185
+ app_row_create: "app-row-create",
186
+ app_rows_create: "app-rows-create",
187
+ app_row_patch: "app-row-patch",
188
+ app_row_delete: "app-row-delete",
189
+ app_rows_delete: "app-rows-delete",
183
190
  app_rollback: "app-rollback",
184
191
  app_sync: "app-sync",
185
192
  app_upsert: "app-upsert",
@@ -658,6 +658,37 @@ declare module "swarm-sdk" {
658
658
  query: string;
659
659
  params?: Record<string, string | number | boolean>;
660
660
  }): Promise<unknown>;
661
+ app_rows_list(args: {
662
+ appId: string;
663
+ model: string;
664
+ equals?: Record<string, string | number | boolean>;
665
+ sort?: `${string}:asc` | `${string}:desc`;
666
+ limit?: number;
667
+ }): Promise<unknown>;
668
+ app_row_get(args: { appId: string; model: string; rowId: string }): Promise<unknown>;
669
+ app_row_create(args: {
670
+ appId: string;
671
+ model: string;
672
+ values: Record<string, unknown>;
673
+ }): Promise<unknown>;
674
+ app_rows_create(args: {
675
+ appId: string;
676
+ model: string;
677
+ rows: Record<string, unknown>[];
678
+ }): Promise<unknown>;
679
+ app_row_patch(args: {
680
+ appId: string;
681
+ model: string;
682
+ rowId: string;
683
+ values: Record<string, unknown>;
684
+ }): Promise<unknown>;
685
+ app_row_delete(args: { appId: string; model: string; rowId: string }): Promise<unknown>;
686
+ app_rows_delete(args: {
687
+ appId: string;
688
+ model: string;
689
+ equals: Record<string, string | number | boolean>;
690
+ limit?: number;
691
+ }): Promise<unknown>;
661
692
  app_rollback(args: {
662
693
  appId: string;
663
694
  version: number;
@@ -640,6 +640,37 @@ declare module "swarm-sdk" {
640
640
  query: string;
641
641
  params?: Record<string, string | number | boolean>;
642
642
  }): Promise<unknown>;
643
+ app_rows_list(args: {
644
+ appId: string;
645
+ model: string;
646
+ equals?: Record<string, string | number | boolean>;
647
+ sort?: `${string}:asc` | `${string}:desc`;
648
+ limit?: number;
649
+ }): Promise<unknown>;
650
+ app_row_get(args: { appId: string; model: string; rowId: string }): Promise<unknown>;
651
+ app_row_create(args: {
652
+ appId: string;
653
+ model: string;
654
+ values: Record<string, unknown>;
655
+ }): Promise<unknown>;
656
+ app_rows_create(args: {
657
+ appId: string;
658
+ model: string;
659
+ rows: Record<string, unknown>[];
660
+ }): Promise<unknown>;
661
+ app_row_patch(args: {
662
+ appId: string;
663
+ model: string;
664
+ rowId: string;
665
+ values: Record<string, unknown>;
666
+ }): Promise<unknown>;
667
+ app_row_delete(args: { appId: string; model: string; rowId: string }): Promise<unknown>;
668
+ app_rows_delete(args: {
669
+ appId: string;
670
+ model: string;
671
+ equals: Record<string, string | number | boolean>;
672
+ limit?: number;
673
+ }): Promise<unknown>;
643
674
  app_rollback(args: {
644
675
  appId: string;
645
676
  version: number;
package/src/server.ts CHANGED
@@ -11,6 +11,7 @@ import { resolveTemplate } from "./prompts/resolver";
11
11
  import { isRbacEnabled } from "./rbac";
12
12
  import { getSlackConfiguration } from "./slack/config";
13
13
  import { registerAcceptSteerTool } from "./tools/accept-steer";
14
+ import { registerAppActionRunTool } from "./tools/app-action-run";
14
15
  import { registerAppDiffTool } from "./tools/app-diff";
15
16
  import { registerAppGetTool } from "./tools/app-get";
16
17
  import { registerAppHistoryTool } from "./tools/app-history";
@@ -18,6 +19,7 @@ import { registerAppListTool } from "./tools/app-list";
18
19
  import { registerAppPatchTool } from "./tools/app-patch";
19
20
  import { registerAppQueryTool } from "./tools/app-query";
20
21
  import { registerAppRollbackTool } from "./tools/app-rollback";
22
+ import { registerAppRowTools } from "./tools/app-rows";
21
23
  import { registerAppSyncTool } from "./tools/app-sync";
22
24
  import { registerAppUpsertTool } from "./tools/app-upsert";
23
25
  import { registerCancelApprovalRequestTool } from "./tools/cancel-approval-request";
@@ -643,6 +645,8 @@ export async function createServer(
643
645
  // Pages capability - DB-backed lightweight artifacts (HTML / JSON specs).
644
646
  if (hasCapability("pages")) {
645
647
  registerAppGetTool(server);
648
+ registerAppRowTools(server);
649
+ registerAppActionRunTool(server);
646
650
  registerAppHistoryTool(server);
647
651
  registerAppDiffTool(server);
648
652
  registerAppListTool(server);
@@ -97,6 +97,10 @@ export function splitSlackSectionText(text: string): string[] {
97
97
  if (splitIdx < MAX_SECTION_LENGTH / 2) {
98
98
  // No good break point at all, hard split
99
99
  splitIdx = MAX_SECTION_LENGTH;
100
+ // Keep a surrogate pair (emoji, astral char) together: never end a
101
+ // chunk on a lone high surrogate.
102
+ const prev = remaining.charCodeAt(splitIdx - 1);
103
+ if (prev >= 0xd800 && prev <= 0xdbff) splitIdx -= 1;
100
104
  }
101
105
  chunks.push(remaining.slice(0, splitIdx));
102
106
  remaining = remaining.slice(splitIdx).trimStart();
@@ -104,6 +108,20 @@ export function splitSlackSectionText(text: string): string[] {
104
108
  return chunks;
105
109
  }
106
110
 
111
+ // Slack's hard cap on section text.
112
+ const SLACK_SECTION_TEXT_LIMIT = 3000;
113
+
114
+ /**
115
+ * Default blocks for a tool message sent without explicit `blocks`: one
116
+ * mrkdwn section per chunk, so text over the section limit still sends.
117
+ * Text Slack accepts in one section stays one section, so links and code
118
+ * fences are never split below the limit.
119
+ */
120
+ export function buildMrkdwnSectionBlocks(text: string): SlackBlock[] {
121
+ if (text.length <= SLACK_SECTION_TEXT_LIMIT) return [sectionBlock(text)];
122
+ return splitSlackSectionText(text).map((chunk) => sectionBlock(chunk));
123
+ }
124
+
107
125
  // --- Block primitives ---
108
126
 
109
127
  function contextBlock(...elements: string[]): SlackBlock {