@desplega.ai/agent-swarm 2.2.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (266) hide show
  1. package/README.md +1 -0
  2. package/dist/{acp-adapter-k0vwfyvn.js → acp-adapter-kedstxx6.js} +6 -6
  3. package/dist/{actions-w4pnb8vz.js → actions-vget6r1k.js} +18 -16
  4. package/dist/{amp-adapter-0cmxb2t5.js → amp-adapter-xeb5mktv.js} +2 -2
  5. package/dist/{app-zneem7g9.js → app-mqap8t8m.js} +3 -3
  6. package/dist/{assistant-wbzd2rnr.js → assistant-h0gv97mv.js} +20 -18
  7. package/dist/{boot-reembed-4kx69j3g.js → boot-reembed-qr8a894z.js} +1 -1
  8. package/dist/{boot-reembed-db0wwgy1.js → boot-reembed-rj85jjhw.js} +1 -1
  9. package/dist/{boot-scrub-sweep-tfv4d3mm.js → boot-scrub-sweep-eyfvn6v9.js} +3 -3
  10. package/dist/{boot-seeds-w2hmkxn5.js → boot-seeds-xs9a8dx1.js} +4 -4
  11. package/dist/{claude-adapter-5qrc2paq.js → claude-adapter-bm23cye7.js} +5 -5
  12. package/dist/{claude-managed-adapter-n7ahjyx6.js → claude-managed-adapter-1mwrsm9d.js} +2 -2
  13. package/dist/{claude-managed-setup-7ra32em4.js → claude-managed-setup-gkfgks4j.js} +1 -1
  14. package/dist/{claude-sdk-session-cd02gc76.js → claude-sdk-session-apb4ypz3.js} +5 -5
  15. package/dist/cli-02vhhssk.js +848 -0
  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-8c1m9mhy.js → cli-1cevz46c.js} +5 -5
  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-vmbakbq0.js → cli-2rwet3r7.js} +1 -1
  22. package/dist/{cli-azs9bhc5.js → cli-3wjs22x3.js} +2 -2
  23. package/dist/{cli-tba8fxf9.js → cli-4fhyq711.js} +45 -36
  24. package/dist/{cli-ak4c12mh.js → cli-5ff2sjyt.js} +1 -1
  25. package/dist/{cli-6q7pyd3k.js → cli-5s5hx57y.js} +5 -3
  26. package/dist/{cli-h23mr2s6.js → cli-6ftrrwm1.js} +9 -371
  27. package/dist/{cli-9ac0mkg5.js → cli-8b9wvjps.js} +1 -1
  28. package/dist/cli-8cxkw1f5.js +382 -0
  29. package/dist/{cli-xhgtay7w.js → cli-8rwvksbc.js} +2 -2
  30. package/dist/{cli-e09hj50q.js → cli-8wkb09q5.js} +11 -11
  31. package/dist/{cli-ksrphveb.js → cli-a1esmmee.js} +14 -2
  32. package/dist/{cli-xnp2vkx7.js → cli-avmqhatn.js} +7 -7
  33. package/dist/{cli-xx13gqf3.js → cli-b80a1100.js} +1 -1
  34. package/dist/{cli-z1hh99j0.js → cli-bfc3k1x5.js} +3 -3
  35. package/dist/{cli-0k9rcwfy.js → cli-bhwa8rzc.js} +33 -15
  36. package/dist/{cli-8cvvytad.js → cli-bn26xbcs.js} +2 -2
  37. package/dist/{cli-k28p6xh7.js → cli-c8fwpgqb.js} +1 -1
  38. package/dist/{cli-rc3r0bnh.js → cli-ccn0eybf.js} +1 -1
  39. package/dist/{cli-9td5ss65.js → cli-dsgj3e7a.js} +2 -2
  40. package/dist/{cli-jrk8mj0t.js → cli-ecshf504.js} +2 -2
  41. package/dist/{cli-raejy0mp.js → cli-eqgfs202.js} +1 -1
  42. package/dist/{cli-ppapy34w.js → cli-f3nx1h8z.js} +2 -2
  43. package/dist/{cli-c9vrwcpb.js → cli-ffbrkh1r.js} +74 -35
  44. package/dist/{cli-mnry16kh.js → cli-kk2r80n5.js} +4 -4
  45. package/dist/{cli-c0dpzwnx.js → cli-kz116kvw.js} +3 -3
  46. package/dist/{cli-e5y285rg.js → cli-mvg2hf8p.js} +4 -4
  47. package/dist/{cli-2jm992rm.js → cli-mz1ezzzh.js} +2 -2
  48. package/dist/{cli-dw0njd9q.js → cli-nme7wery.js} +6 -6
  49. package/dist/{cli-mvsfbbgq.js → cli-nrrg0fvy.js} +18 -4
  50. package/dist/{cli-8jtrekpy.js → cli-p1m2b6gc.js} +7 -7
  51. package/dist/{cli-4c7k75rq.js → cli-rg7ntypm.js} +378 -84
  52. package/dist/{cli-z3dk189k.js → cli-rsd7ga7v.js} +10 -8
  53. package/dist/{cli-etvn5ej2.js → cli-sppc0cmw.js} +1 -1
  54. package/dist/{cli-8w65vwee.js → cli-tg9ksbn8.js} +1 -1
  55. package/dist/{cli-th8krqbf.js → cli-tgvpmnwz.js} +2 -2
  56. package/dist/{cli-hwc4a9xm.js → cli-tj4j2ts4.js} +6 -6
  57. package/dist/{cli-jhhb2v9n.js → cli-ttaz6c7m.js} +2 -2
  58. package/dist/{cli-w7z94e6c.js → cli-tx00c0h6.js} +4 -2
  59. package/dist/{cli-4g629sgr.js → cli-wrv09v7q.js} +20 -1
  60. package/dist/{cli-cby329ny.js → cli-wyrr38xm.js} +2 -2
  61. package/dist/{cli-5ks6x36q.js → cli-x1p5dhq2.js} +3 -3
  62. package/dist/{cli-bddxamg7.js → cli-x719j3vs.js} +220 -629
  63. package/dist/{cli-ztcxv5nv.js → cli-xfe63zx3.js} +1 -1
  64. package/dist/{cli-q6gw45wj.js → cli-ywyrffff.js} +1 -1
  65. package/dist/{cli-p46xdr1m.js → cli-zvze9rbm.js} +1 -1
  66. package/dist/cli.js +15 -15
  67. package/dist/{codex-adapter-m0fx3v3s.js → codex-adapter-pp2gq8gr.js} +3 -3
  68. package/dist/{codex-hook-x6tm6y5m.js → codex-hook-qwzccd64.js} +1 -1
  69. package/dist/{codex-session-runner-j9m2gfyy.js → codex-session-runner-0vzn09t8.js} +3 -3
  70. package/dist/{commands-ktnqwrv4.js → commands-psqhkttb.js} +2 -2
  71. package/dist/{controller-nex2he8v.js → controller-t20vz2k8.js} +2 -2
  72. package/dist/{controllers-connect-6yhwqv9v.js → controllers-connect-gkxmqw9d.js} +24 -20
  73. package/dist/{cursor-adapter-02s5ekva.js → cursor-adapter-3v542qe5.js} +1 -1
  74. package/dist/{db-jvr86v7f.js → db-2xbyenjr.js} +2 -2
  75. package/dist/{db-19tw9r5s.js → db-nc1rd869.js} +3 -2
  76. package/dist/{dispatcher-d3mnsg2y.js → dispatcher-ewceb8ts.js} +17 -15
  77. package/dist/{dsh-adapter-63rvej88.js → dsh-adapter-75xwh78t.js} +1 -1
  78. package/dist/{easter-egg-3d-6hg2r942.js → easter-egg-3d-xsc47jwf.js} +1 -1
  79. package/dist/{execute-bcqc69a5.js → execute-gzmw1mnw.js} +1 -1
  80. package/dist/{external-channel-44cwer32.js → external-channel-a5dsmxn4.js} +2 -2
  81. package/dist/{google-generative-ai-phpmwxvg.js → google-generative-ai-bmq8gkf3.js} +1 -1
  82. package/dist/{google-vertex-es9ybgdy.js → google-vertex-b1rvp52s.js} +1 -1
  83. package/dist/{grok-adapter-ysp907f6.js → grok-adapter-vj88xr23.js} +7 -7
  84. package/dist/handlers-zbs90bny.js +39 -0
  85. package/dist/{heartbeat-2etacsqq.js → heartbeat-t2m9r3wa.js} +20 -18
  86. package/dist/{hook-mzxxs5n2.js → hook-8femx1bg.js} +4 -4
  87. package/dist/{hook-3kvmax6d.js → hook-xa677f8r.js} +5 -5
  88. package/dist/{http-fn978m0g.js → http-xpmmq028.js} +940 -269
  89. package/dist/{index-e4tsa1h7.js → index-8pwc1jc8.js} +17 -15
  90. package/dist/{index-w2wr89xd.js → index-cdzyfrsp.js} +1 -1
  91. package/dist/{index-1r8hw4vd.js → index-dg2tmj99.js} +17 -15
  92. package/dist/index-nxa6me0q.js +41 -0
  93. package/dist/{index-039x2sag.js → index-s7ckdajq.js} +42 -14
  94. package/dist/{index-wgsw9xcz.js → index-syztqdt6.js} +1 -1
  95. package/dist/{index-x0q2a4f9.js → index-ysd45kgc.js} +1 -1
  96. package/dist/{index-g909y089.js → index-zgvqzjsw.js} +8 -8
  97. package/dist/{keepalive-d4nzs76x.js → keepalive-3nr4st3r.js} +3 -3
  98. package/dist/{lead-8h5n9k5f.js → lead-td8h2sfn.js} +33 -31
  99. package/dist/{maintenance-ea6jfw42.js → maintenance-verwpe2c.js} +1 -1
  100. package/dist/{oauth-refresh-sweep-3f25djq3.js → oauth-refresh-sweep-9q2t0y3q.js} +2 -2
  101. package/dist/{onboard-pvk7hdb2.js → onboard-aytqedmq.js} +2 -2
  102. package/dist/{opencode-adapter-eeesgfqk.js → opencode-adapter-f4hryjny.js} +4 -4
  103. package/dist/{otel-impl-qb7nhgdp.js → otel-impl-hckw4wt1.js} +1 -1
  104. package/dist/{pi-mono-adapter-zasp3ze3.js → pi-mono-adapter-hxby0ce1.js} +6 -6
  105. package/dist/{pricing-refresh-6ehwkcmp.js → pricing-refresh-5nvme3sm.js} +3 -3
  106. package/dist/{rbac-roles-km0f9cmm.js → rbac-roles-pd8c26m1.js} +5 -3
  107. package/dist/{rbac-roles-9648nh00.js → rbac-roles-vbpy21mg.js} +2 -2
  108. package/dist/{render-v2-xkze1wef.js → render-v2-7nqgvk2q.js} +3 -3
  109. package/dist/{resume-tq0bbvt5.js → resume-b6djcj3c.js} +6 -6
  110. package/dist/{runtime-ar8310ba.js → runtime-txrxc9za.js} +1 -1
  111. package/dist/session-access-fc3f9y12.js +25 -0
  112. package/dist/{setup-rnrazxvd.js → setup-n39szm6b.js} +1 -1
  113. package/dist/{virtual-modules-wzk6jf3a.js → virtual-modules-wyp2k6z3.js} +2 -2
  114. package/dist/{work-objects-e75q2rqs.js → work-objects-jem2fre0.js} +20 -18
  115. package/dist/{worker-4ww4hjef.js → worker-wr6xgfr2.js} +33 -31
  116. package/openapi.json +1011 -115
  117. package/package.json +2 -2
  118. package/src/apps/action-caller.ts +6 -0
  119. package/src/apps/script-types.ts +16 -3
  120. package/src/be/asset-key-audit.ts +4 -13
  121. package/src/be/db/active-sessions.ts +5 -0
  122. package/src/be/db/session-visibility.ts +29 -0
  123. package/src/be/db/tasks/read.ts +61 -3
  124. package/src/be/db/tasks/write.ts +15 -6
  125. package/src/be/db.ts +149 -66
  126. package/src/be/events.ts +11 -0
  127. package/src/be/migrations/227_inbox_item_state_last_read_at.sql +1 -0
  128. package/src/be/migrations/230_session_shares.sql +23 -0
  129. package/src/be/rbac-roles.ts +2 -0
  130. package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +22 -4
  131. package/src/be/session-shares.ts +244 -0
  132. package/src/be/swarm-config-guard.ts +23 -2
  133. package/src/commands/runner.ts +11 -0
  134. package/src/heartbeat/heartbeat.ts +25 -7
  135. package/src/http/active-sessions.ts +13 -2
  136. package/src/http/agents.ts +10 -3
  137. package/src/http/all-routes.ts +1 -0
  138. package/src/http/api-keys.ts +3 -1
  139. package/src/http/approval-requests.ts +2 -1
  140. package/src/http/apps.ts +36 -11
  141. package/src/http/assets.ts +26 -4
  142. package/src/http/config.ts +3 -0
  143. package/src/http/context.ts +2 -0
  144. package/src/http/core.ts +39 -2
  145. package/src/http/events.ts +6 -2
  146. package/src/http/favorite-owner.ts +1 -1
  147. package/src/http/fs.ts +38 -3
  148. package/src/http/inbox-state.ts +58 -8
  149. package/src/http/index.ts +3 -1
  150. package/src/http/memory.ts +3 -1
  151. package/src/http/ranking.ts +367 -0
  152. package/src/http/realtime.ts +26 -21
  153. package/src/http/route-def.ts +77 -11
  154. package/src/http/script-runs.ts +2 -1
  155. package/src/http/scripts.ts +8 -0
  156. package/src/http/session-access.ts +179 -0
  157. package/src/http/session-data.ts +10 -2
  158. package/src/http/sessions.ts +212 -12
  159. package/src/http/stats.ts +7 -3
  160. package/src/http/tasks.ts +83 -7
  161. package/src/http/used-by.ts +11 -3
  162. package/src/kv-reserved-namespaces.ts +1 -0
  163. package/src/prompts/session-templates.ts +3 -1
  164. package/src/providers/amp-adapter.ts +1 -1
  165. package/src/ranking/telemetry-ranking.ts +147 -0
  166. package/src/ranking/usage.ts +57 -0
  167. package/src/rbac/admission.ts +22 -7
  168. package/src/rbac/legacy-policy.ts +2 -0
  169. package/src/rbac/permissions.ts +9 -0
  170. package/src/realtime/auth.ts +69 -0
  171. package/src/realtime/tickets.ts +4 -1
  172. package/src/realtime/transport.ts +77 -9
  173. package/src/slack/blocks.ts +18 -0
  174. package/src/slack/dm.ts +29 -0
  175. package/src/telemetry-contract/EVENTS.md +3 -0
  176. package/src/telemetry-contract/SOURCE.json +37 -4
  177. package/src/telemetry-contract/catalog.json +69 -0
  178. package/src/telemetry-contract/ranking/fixtures/expected.json +30 -0
  179. package/src/telemetry-contract/ranking/fixtures/invalid/bad-category.json +11 -0
  180. package/src/telemetry-contract/ranking/fixtures/invalid/bad-claim.json +11 -0
  181. package/src/telemetry-contract/ranking/fixtures/invalid/blank-description.json +11 -0
  182. package/src/telemetry-contract/ranking/fixtures/invalid/blank-name.json +11 -0
  183. package/src/telemetry-contract/ranking/fixtures/invalid/description-too-long.json +11 -0
  184. package/src/telemetry-contract/ranking/fixtures/invalid/domain-uppercase.json +11 -0
  185. package/src/telemetry-contract/ranking/fixtures/invalid/domain-with-scheme.json +11 -0
  186. package/src/telemetry-contract/ranking/fixtures/invalid/domain-with-www.json +11 -0
  187. package/src/telemetry-contract/ranking/fixtures/invalid/http-source-url.json +11 -0
  188. package/src/telemetry-contract/ranking/fixtures/invalid/missing-description.json +10 -0
  189. package/src/telemetry-contract/ranking/fixtures/invalid/missing-version.json +10 -0
  190. package/src/telemetry-contract/ranking/fixtures/invalid/name-not-string.json +11 -0
  191. package/src/telemetry-contract/ranking/fixtures/invalid/name-too-long.json +11 -0
  192. package/src/telemetry-contract/ranking/fixtures/invalid/not-object.json +1 -0
  193. package/src/telemetry-contract/ranking/fixtures/invalid/null.json +1 -0
  194. package/src/telemetry-contract/ranking/fixtures/invalid/other-with-subcategory.json +11 -0
  195. package/src/telemetry-contract/ranking/fixtures/invalid/oversize.json +1 -0
  196. package/src/telemetry-contract/ranking/fixtures/invalid/short-claim.json +11 -0
  197. package/src/telemetry-contract/ranking/fixtures/invalid/subcategory-other-category.json +11 -0
  198. package/src/telemetry-contract/ranking/fixtures/invalid/trailing-data.json +1 -0
  199. package/src/telemetry-contract/ranking/fixtures/invalid/unknown-field.json +12 -0
  200. package/src/telemetry-contract/ranking/fixtures/invalid/unsupported-version.json +11 -0
  201. package/src/telemetry-contract/ranking/fixtures/invalid/updated-at-not-date.json +11 -0
  202. package/src/telemetry-contract/ranking/fixtures/invalid/updated-at-offset.json +11 -0
  203. package/src/telemetry-contract/ranking/fixtures/invalid/wrong-case-field.json +11 -0
  204. package/src/telemetry-contract/ranking/fixtures/valid/category-without-subcategory.json +11 -0
  205. package/src/telemetry-contract/ranking/fixtures/valid/full.json +11 -0
  206. package/src/telemetry-contract/ranking/fixtures/valid/minimal.json +11 -0
  207. package/src/telemetry-contract/ranking/fixtures/valid/padded-text.json +11 -0
  208. package/src/telemetry-contract/ranking/fixtures/valid/unicode-name.json +11 -0
  209. package/src/telemetry-contract/ranking/listing.v1.schema.json +61 -0
  210. package/src/telemetry-contract/ranking/taxonomy.json +8 -0
  211. package/src/telemetry-contract/ranking-taxonomy.gen.ts +11 -0
  212. package/src/telemetry-contract/types.gen.ts +708 -1
  213. package/src/telemetry.ts +26 -2
  214. package/src/tests/apps-rbac.test.ts +189 -2
  215. package/src/tests/apps-script-types.test.ts +16 -3
  216. package/src/tests/asset-key-api.test.ts +128 -0
  217. package/src/tests/asset-key-invariants.test.ts +179 -3
  218. package/src/tests/asset-key-migration.test.ts +35 -0
  219. package/src/tests/check-pr-body.test.ts +23 -0
  220. package/src/tests/check-ranking-entry.test.ts +146 -0
  221. package/src/tests/db-extraction-contract.test.ts +1 -0
  222. package/src/tests/db-task-read-contract.test.ts +1 -0
  223. package/src/tests/docs-index-footprint.test.js +2 -2
  224. package/src/tests/heartbeat-checklist.test.ts +88 -0
  225. package/src/tests/heartbeat-jev-gate.test.ts +394 -10
  226. package/src/tests/inbox-state.test.ts +167 -0
  227. package/src/tests/kv-tool.test.ts +15 -0
  228. package/src/tests/org-identity.test.ts +44 -0
  229. package/src/tests/ranking-listing.test.ts +112 -0
  230. package/src/tests/ranking-route.test.ts +319 -0
  231. package/src/tests/ranking-usage.test.ts +50 -0
  232. package/src/tests/rbac-admission-e2e.test.ts +8 -0
  233. package/src/tests/rbac-admission.test.ts +38 -0
  234. package/src/tests/rbac-charact-http.test.ts +4 -3
  235. package/src/tests/rbac-engine.test.ts +2 -0
  236. package/src/tests/rbac-roles.test.ts +2 -0
  237. package/src/tests/realtime-room-surfaces.test.ts +3 -1
  238. package/src/tests/realtime-session-join-race.test.ts +142 -0
  239. package/src/tests/realtime-tickets.test.ts +24 -17
  240. package/src/tests/realtime-transport.test.ts +91 -0
  241. package/src/tests/script-connections.test.ts +2 -0
  242. package/src/tests/session-route-posture.test.ts +639 -0
  243. package/src/tests/session-sharing-routes.test.ts +282 -0
  244. package/src/tests/session-sharing.test.ts +526 -0
  245. package/src/tests/sessions-detail-batch.test.ts +7 -1
  246. package/src/tests/slack-tool-blocks.test.ts +101 -0
  247. package/src/tests/slack-update.test.ts +55 -0
  248. package/src/tests/slack-user-dm.test.ts +177 -0
  249. package/src/tests/used-by-route.test.ts +10 -0
  250. package/src/tools/slack-post.ts +18 -12
  251. package/src/tools/slack-read.ts +13 -1
  252. package/src/tools/slack-reply.ts +7 -10
  253. package/src/tools/slack-start-thread.ts +18 -11
  254. package/src/tools/slack-update.ts +2 -9
  255. package/src/types.ts +37 -0
  256. package/src/utils/org-identity.ts +33 -0
  257. package/templates/skills/apps/SKILL.md +17 -0
  258. package/templates/skills/apps/content.md +17 -0
  259. package/templates/skills/code-quality/content.md +2 -2
  260. package/templates/skills/pages/content.md +4 -0
  261. package/templates/skills/swarm-scripts/SKILL.md +6 -0
  262. package/templates/skills/swarm-scripts/content.md +6 -0
  263. package/dist/handlers-t5pbabwf.js +0 -37
  264. package/dist/index-qyjbb7br.js +0 -39
  265. package/dist/{cli-n5mrsg0f.js → cli-60cgh1rv.js} +3 -3
  266. /package/dist/{cli-j0v75y13.js → cli-y83zk5sb.js} +0 -0
@@ -0,0 +1,142 @@
1
+ import { afterAll, beforeAll, expect, test } from "bun:test";
2
+ import { createServer, type Server } from "node:http";
3
+ import WebSocket from "ws";
4
+ import { closeDb, createTaskExtended, createUser, initDb } from "../be/db";
5
+ import { setSessionShares } from "../be/session-shares";
6
+ import { mintToken } from "../be/users";
7
+ import { resolveHttpRequestAuth } from "../http/auth";
8
+ import { handleSessions } from "../http/sessions";
9
+ import { getPathSegments, parseQueryParams } from "../http/utils";
10
+ import { closeRooms, flushRooms, getRoom, sweepRooms } from "../realtime/rooms";
11
+ import { issueRealtimeTicket } from "../realtime/tickets";
12
+ import { attachRealtimeTransport } from "../realtime/transport";
13
+ import { beginRequestAuthScope, setRequestAuth } from "../utils/request-auth-context";
14
+ import { listenOnFreePort } from "./test-net";
15
+
16
+ type Frame = Record<string, any>;
17
+ type User = Awaited<ReturnType<typeof createUser>>;
18
+
19
+ let server: Server;
20
+ let detach: () => void;
21
+ let port: number;
22
+
23
+ beforeAll(async () => {
24
+ initDb(":memory:");
25
+ server = createServer(async (req, res) => {
26
+ beginRequestAuthScope();
27
+ const auth = await resolveHttpRequestAuth(req, undefined);
28
+ if (!auth) {
29
+ res.writeHead(401);
30
+ res.end();
31
+ return;
32
+ }
33
+ setRequestAuth(req, auth);
34
+ const url = req.url ?? "/";
35
+ if (await handleSessions(req, res, getPathSegments(url), parseQueryParams(url), undefined))
36
+ return;
37
+ res.writeHead(404);
38
+ res.end();
39
+ });
40
+ detach = attachRealtimeTransport(server);
41
+ port = await listenOnFreePort(server, "127.0.0.1");
42
+ });
43
+
44
+ afterAll(async () => {
45
+ detach();
46
+ await new Promise<void>((resolve) => server.close(() => resolve()));
47
+ await closeRooms();
48
+ closeDb();
49
+ });
50
+
51
+ async function openSocket(user: User) {
52
+ const { ticket } = issueRealtimeTicket({ kind: "user", userId: user.id, user: user as never });
53
+ const ws = new WebSocket(`ws://127.0.0.1:${port}/api/realtime?ticket=${ticket}`);
54
+ const frames: Frame[] = [];
55
+ ws.on("message", (raw) => {
56
+ const frame = JSON.parse(raw.toString()) as Frame;
57
+ frames.push(frame);
58
+ if (frame.seq) ws.send(JSON.stringify({ ack: frame.seq }));
59
+ });
60
+ await new Promise<void>((resolve, reject) => {
61
+ ws.once("open", () => resolve());
62
+ ws.once("error", reject);
63
+ });
64
+ const next = async (match: (frame: Frame) => boolean) => {
65
+ for (let i = 0; i < 400; i++) {
66
+ const index = frames.findIndex(match);
67
+ if (index >= 0) return frames.splice(index, 1)[0]!;
68
+ await Bun.sleep(5);
69
+ }
70
+ throw new Error(`No matching frame. Buffered: ${JSON.stringify(frames)}`);
71
+ };
72
+ return { ws, frames, next, send: (message: Frame) => ws.send(JSON.stringify(message)) };
73
+ }
74
+
75
+ test("a revoke that lands while a session presence join loads its room rejects the join", async () => {
76
+ const owner = await createUser({ name: "Race Owner" });
77
+ const viewer = await createUser({ name: "Race Viewer" });
78
+ const ownerToken = await mintToken(owner.id, "test", { kind: "system", id: "test" });
79
+ const root = await createTaskExtended("Private incident", {
80
+ requestedByUserId: owner.id,
81
+ source: "ui",
82
+ });
83
+ await setSessionShares(
84
+ { id: root.id, requestedByUserId: owner.id },
85
+ { everyone: "read", users: [] },
86
+ owner.id,
87
+ );
88
+ const namespace = `presence:session:${root.id}`;
89
+
90
+ // Persist the room, then idle-evict it, so the next join reloads it
91
+ // through the room creation queue.
92
+ const room = await getRoom(namespace, "default");
93
+ await flushRooms();
94
+ room.lastActivity = 0;
95
+ await sweepRooms(Date.now());
96
+
97
+ const state = (globalThis as { __swarmRoomsState?: { creationChain: Promise<void> } })
98
+ .__swarmRoomsState!;
99
+ let release!: () => void;
100
+ state.creationChain = new Promise<void>((resolve) => {
101
+ release = resolve;
102
+ });
103
+
104
+ const viewerSocket = await openSocket(viewer);
105
+ const ownerSocket = await openSocket(owner);
106
+ try {
107
+ await viewerSocket.next((frame) => frame.type === "hello");
108
+ await ownerSocket.next((frame) => frame.type === "hello");
109
+ viewerSocket.send({ id: 1, op: "join", name: "default", namespace, schemaVersion: 1 });
110
+ const pending = (globalThis as { __swarmRoomsState?: { pending: Map<string, unknown> } })
111
+ .__swarmRoomsState!.pending;
112
+ for (let i = 0; i < 400 && pending.size === 0; i++) await Bun.sleep(5);
113
+ expect(pending.size).toBeGreaterThan(0);
114
+
115
+ const revoked = await fetch(`http://127.0.0.1:${port}/api/sessions/${root.id}/shares`, {
116
+ method: "PUT",
117
+ headers: {
118
+ Authorization: `Bearer ${ownerToken.plaintext}`,
119
+ "Content-Type": "application/json",
120
+ },
121
+ body: JSON.stringify({ everyone: "none", users: [] }),
122
+ });
123
+ expect(revoked.status).toBe(200);
124
+ release();
125
+
126
+ const joined = await viewerSocket.next((frame) => frame.id === 1);
127
+ expect(joined.type).toBe("error");
128
+ expect(joined.error).toContain("access");
129
+
130
+ ownerSocket.send({ id: 2, op: "join", name: "default", namespace, schemaVersion: 1 });
131
+ expect((await ownerSocket.next((frame) => frame.id === 2)).type).toBe("result");
132
+ viewerSocket.frames.splice(0);
133
+ ownerSocket.send({ id: 3, op: "presence", namespace, data: { typing: true } });
134
+ await ownerSocket.next((frame) => frame.type === "result" && frame.id === 3);
135
+ await Bun.sleep(100);
136
+ expect(viewerSocket.frames.filter((frame) => frame.type === "presence")).toEqual([]);
137
+ } finally {
138
+ release();
139
+ viewerSocket.ws.close();
140
+ ownerSocket.ws.close();
141
+ }
142
+ });
@@ -8,6 +8,7 @@ import {
8
8
  redeemRealtimeTicket,
9
9
  sweepRealtimeTickets,
10
10
  } from "../realtime/tickets";
11
+ import { type HttpRequestAuth, setRequestAuth } from "../utils/request-auth-context";
11
12
 
12
13
  const auth = { kind: "operator", fingerprint: "test-operator" } as const;
13
14
 
@@ -46,26 +47,32 @@ describe("realtime tickets", () => {
46
47
  expect(redeemRealtimeTicket(newest.ticket, 4_001)).toEqual(auth);
47
48
  });
48
49
 
49
- test("the ticket route answers 404 while Comb is off", async () => {
50
+ test("the ticket route refuses agents and serves the dashboard with Comb off", async () => {
50
51
  const previous = process.env.COMB_ENABLED;
51
52
  process.env.COMB_ENABLED = "false";
52
53
  try {
53
- let status = 0;
54
- const res = {
55
- writeHead: (code: number) => {
56
- status = code;
57
- },
58
- end: () => {},
59
- } as unknown as ServerResponse;
60
- const req = { method: "POST", headers: {} } as IncomingMessage;
61
- const handled = await handleRealtimeTicket(
62
- req,
63
- res,
64
- ["api", "realtime", "ticket"],
65
- new URLSearchParams(),
66
- );
67
- expect(handled).toBe(true);
68
- expect(status).toBe(404);
54
+ const ticketStatus = async (requestAuth: HttpRequestAuth | null) => {
55
+ let status = 0;
56
+ const res = {
57
+ writeHead: (code: number) => {
58
+ status = code;
59
+ },
60
+ end: () => {},
61
+ } as unknown as ServerResponse;
62
+ const req = { method: "POST", headers: {} } as IncomingMessage;
63
+ if (requestAuth) setRequestAuth(req, requestAuth);
64
+ const handled = await handleRealtimeTicket(
65
+ req,
66
+ res,
67
+ ["api", "realtime", "ticket"],
68
+ new URLSearchParams(),
69
+ );
70
+ expect(handled).toBe(true);
71
+ return status;
72
+ };
73
+ // Session presence uses the ticket too, so Comb being off no longer hides the route.
74
+ expect(await ticketStatus(auth)).toBe(200);
75
+ expect(await ticketStatus(null)).toBe(403);
69
76
  } finally {
70
77
  if (previous === undefined) delete process.env.COMB_ENABLED;
71
78
  else process.env.COMB_ENABLED = previous;
@@ -655,6 +655,97 @@ describe("realtime transport", () => {
655
655
  }
656
656
  });
657
657
 
658
+ test("session presence: the first joiner creates the room, and a viewer needs read access", async () => {
659
+ const owner = await createUserToken("Session Owner");
660
+ const viewer = await createUserToken("Session Viewer");
661
+ const created = await api("POST", "/api/tasks", {
662
+ body: { task: "Fix flaky deploy", requestedByUserId: owner.id, source: "ui" },
663
+ });
664
+ expect(created.status).toBe(201);
665
+ const rootId = ((await created.json()) as { id: string }).id;
666
+ const namespace = `presence:session:${rootId}`;
667
+ const ownerSocket = await openSocket(
668
+ {},
669
+ `${socketUrl}?ticket=${(await issueTicket(owner.token)).ticket}`,
670
+ );
671
+ const viewerSocket = await openSocket(
672
+ {},
673
+ `${socketUrl}?ticket=${(await issueTicket(viewer.token)).ticket}`,
674
+ );
675
+ try {
676
+ await ownerSocket.next((frame) => frame.type === "hello");
677
+ await viewerSocket.next((frame) => frame.type === "hello");
678
+ // Nobody is in the room yet: this join creates it.
679
+ const ownerJoined = await join(ownerSocket, 1, "default", namespace);
680
+ expect(ownerJoined.type).toBe("result");
681
+
682
+ viewerSocket.send({ id: 2, op: "join", name: "default", namespace, schemaVersion: 1 });
683
+ const denied = await viewerSocket.next((frame) => frame.type === "error" && frame.id === 2);
684
+ expect(denied.error).toContain("access");
685
+
686
+ const shared = await api("PUT", `/api/sessions/${rootId}/shares`, {
687
+ headers: { Authorization: `Bearer ${owner.token}` },
688
+ body: { everyone: "read", users: [] },
689
+ });
690
+ expect(shared.status).toBe(200);
691
+ expect((await join(viewerSocket, 3, "default", namespace)).type).toBe("result");
692
+ } finally {
693
+ await closeSocket(ownerSocket);
694
+ await closeSocket(viewerSocket);
695
+ }
696
+ });
697
+
698
+ test("session presence: revoking read access evicts the viewer from the room", async () => {
699
+ const owner = await createUserToken("Revoking Owner");
700
+ const viewer = await createUserToken("Revoked Viewer");
701
+ const created = await api("POST", "/api/tasks", {
702
+ body: { task: "Private incident", requestedByUserId: owner.id, source: "ui" },
703
+ });
704
+ expect(created.status).toBe(201);
705
+ const rootId = ((await created.json()) as { id: string }).id;
706
+ const namespace = `presence:session:${rootId}`;
707
+ const share = async (everyone: "read" | "none") => {
708
+ const response = await api("PUT", `/api/sessions/${rootId}/shares`, {
709
+ headers: { Authorization: `Bearer ${owner.token}` },
710
+ body: { everyone, users: [] },
711
+ });
712
+ expect(response.status).toBe(200);
713
+ };
714
+ const ownerSocket = await openSocket(
715
+ {},
716
+ `${socketUrl}?ticket=${(await issueTicket(owner.token)).ticket}`,
717
+ );
718
+ const viewerSocket = await openSocket(
719
+ {},
720
+ `${socketUrl}?ticket=${(await issueTicket(viewer.token)).ticket}`,
721
+ );
722
+ try {
723
+ await ownerSocket.next((frame) => frame.type === "hello");
724
+ await viewerSocket.next((frame) => frame.type === "hello");
725
+ await share("read");
726
+ expect((await join(ownerSocket, 1, "default", namespace)).type).toBe("result");
727
+ expect((await join(viewerSocket, 2, "default", namespace)).type).toBe("result");
728
+ viewerSocket.frames.splice(0);
729
+
730
+ await share("none");
731
+ const evicted = await viewerSocket.next(
732
+ (frame) => frame.type === "error" && frame.namespace === namespace,
733
+ );
734
+ expect(evicted.error).toContain("access");
735
+
736
+ ownerSocket.send({ id: 3, op: "presence", namespace, data: { typing: true } });
737
+ await ownerSocket.next((frame) => frame.type === "result" && frame.id === 3);
738
+ await Bun.sleep(100);
739
+ const leaked = viewerSocket.frames.filter(
740
+ (frame) => frame.type === "presence" && JSON.stringify(frame.peers).includes("typing"),
741
+ );
742
+ expect(leaked).toEqual([]);
743
+ } finally {
744
+ await closeSocket(ownerSocket);
745
+ await closeSocket(viewerSocket);
746
+ }
747
+ });
748
+
658
749
  test("refuses agent access to Comb presence namespaces", async () => {
659
750
  const agentId = await registerAgent();
660
751
  const client = await openSocket({
@@ -632,6 +632,8 @@ describe("script connections", () => {
632
632
  markMigrationApplied(database, "219_relabel_pi_context_compactions.sql");
633
633
  // 220 reads agents and swarm_config, which this migration-112-only fixture does not create.
634
634
  markMigrationApplied(database, "220_model_profiles.sql");
635
+ // 227 alters inbox_item_state, which this migration-112-only fixture does not create.
636
+ markMigrationApplied(database, "227_inbox_item_state_last_read_at.sql");
635
637
 
636
638
  const id = crypto.randomUUID();
637
639
  const now = new Date().toISOString();