@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,179 @@
1
+ import type { IncomingMessage } from "node:http";
2
+ import { getSteeringMessageById } from "../be/db";
3
+ import { getActiveSessionById } from "../be/db/active-sessions";
4
+ import { getUserGrant } from "../be/rbac-roles";
5
+ import {
6
+ getSessionRoot,
7
+ hasSessionAccess,
8
+ type SessionAccess,
9
+ sessionAccess,
10
+ sessionAccessForRoot,
11
+ } from "../be/session-shares";
12
+ import { findUserById } from "../be/users";
13
+ import { isRbacEnabled } from "../rbac";
14
+ import { getRequestAuth } from "../utils/request-auth-context";
15
+ import { dashboardUserIdHeader } from "./favorite-owner";
16
+ import { type RouteDef, routePathParams } from "./route-def";
17
+
18
+ /**
19
+ * Who a request acts for when it touches a session.
20
+ *
21
+ * - `bypass`: no session check. Agents (an `aseph_` token, or the operator key
22
+ * with `X-Agent-ID`) continue chains through `POST /api/tasks` all the time,
23
+ * and an operator-key call with no picked dashboard user is a script or the
24
+ * CLI. Both keep the access they had before sharing existed.
25
+ * - `user`: an `aswt_` user token, or an operator-key dashboard tab naming an
26
+ * active user in `X-Swarm-User-Id`. On the shared key that identity is
27
+ * honor-system, the same trust the dashboard already gives favorites.
28
+ * `userId: null` is a guest page session, which has no user at all.
29
+ */
30
+ export type SessionActor = { kind: "bypass" } | { kind: "user"; userId: string | null };
31
+
32
+ const BYPASS: SessionActor = { kind: "bypass" };
33
+
34
+ export async function resolveSessionActor(
35
+ req: IncomingMessage,
36
+ myAgentId: string | undefined,
37
+ ): Promise<SessionActor> {
38
+ const auth = getRequestAuth(req);
39
+ if (auth?.kind === "user") return { kind: "user", userId: auth.userId };
40
+ if (auth?.kind === "guest") return { kind: "user", userId: null };
41
+ if (auth?.kind === "operator" && !myAgentId && !auth.page) {
42
+ const picked = dashboardUserIdHeader(req);
43
+ if (!picked) return BYPASS;
44
+ const user = await findUserById(picked);
45
+ return user?.status === "active" ? { kind: "user", userId: user.id } : BYPASS;
46
+ }
47
+ return BYPASS;
48
+ }
49
+
50
+ /** The actor's access to the session holding `taskId`; `owner` for a bypass actor. */
51
+ export async function sessionAccessFor(
52
+ actor: SessionActor,
53
+ taskId: string,
54
+ ): Promise<SessionAccess> {
55
+ return actor.kind === "bypass" ? "owner" : sessionAccess(actor.userId, taskId);
56
+ }
57
+
58
+ export const SESSION_READ_DENIED =
59
+ "You don't have access to this session. Ask the owner to share it.";
60
+
61
+ /** True unless the request acts for a user who lacks read access to the task's session. */
62
+ export async function canReadSession(
63
+ req: IncomingMessage,
64
+ myAgentId: string | undefined,
65
+ taskId: string,
66
+ ): Promise<boolean> {
67
+ const access = await sessionAccessFor(await resolveSessionActor(req, myAgentId), taskId);
68
+ return hasSessionAccess(access, "read");
69
+ }
70
+
71
+ /** The SQL list filter (`readableSessionCondition`) for an actor; none for a bypass actor. */
72
+ export function readableByFor(actor: SessionActor): { userId: string | null } | undefined {
73
+ return actor.kind === "user" ? { userId: actor.userId } : undefined;
74
+ }
75
+
76
+ export const SESSION_WRITE_DENIED =
77
+ "You can't write in this session. Ask the owner to share it with edit access.";
78
+
79
+ /**
80
+ * The denial for an actor lacking `needed` access to the session holding
81
+ * `taskId`, or null. An unknown task passes, so the handler still answers 404.
82
+ */
83
+ async function denialFor(
84
+ actor: SessionActor,
85
+ taskId: string,
86
+ needed: "read" | "write",
87
+ ): Promise<string | null> {
88
+ if (actor.kind === "bypass") return null;
89
+ const root = await getSessionRoot(taskId);
90
+ if (!root) return null;
91
+ if (hasSessionAccess(await sessionAccessForRoot(actor.userId, root), needed)) return null;
92
+ return needed === "read" ? SESSION_READ_DENIED : SESSION_WRITE_DENIED;
93
+ }
94
+
95
+ export const SESSION_AGENT_WIDE_DENIED =
96
+ "This changes sessions across a whole agent. Only the agent or the operator can call it.";
97
+
98
+ /**
99
+ * Enforces a route's path session posture (`taskParam`, `steeringParam`,
100
+ * `activeSessionParam`, `agentWide`) before any handler runs: a GET needs read
101
+ * access, every other method write access.
102
+ */
103
+ export async function pathSessionDenial(
104
+ req: IncomingMessage,
105
+ myAgentId: string | undefined,
106
+ def: RouteDef,
107
+ pathSegments: string[],
108
+ ): Promise<string | null> {
109
+ const posture = def.session;
110
+ if (!posture || "bodyTaskIds" in posture || "none" in posture) return null;
111
+ const actor = await resolveSessionActor(req, myAgentId);
112
+ if (actor.kind === "bypass") return null;
113
+ if ("agentWide" in posture) return SESSION_AGENT_WIDE_DENIED;
114
+ const needed = def.method === "get" ? "read" : "write";
115
+ const params = routePathParams(def, pathSegments);
116
+ let taskId: string | undefined;
117
+ if ("taskParam" in posture) {
118
+ taskId = params[posture.taskParam];
119
+ } else if ("steeringParam" in posture) {
120
+ const message = await getSteeringMessageById(params[posture.steeringParam] ?? "");
121
+ taskId = message?.taskId;
122
+ } else {
123
+ const row = await getActiveSessionById(params[posture.activeSessionParam] ?? "");
124
+ // An unknown row passes so the handler answers; a row with no task is the agent's own.
125
+ if (!row) return null;
126
+ if (!row.taskId) return needed === "read" ? SESSION_READ_DENIED : SESSION_WRITE_DENIED;
127
+ taskId = row.taskId;
128
+ }
129
+ if (!taskId) return null;
130
+ return denialFor(actor, taskId, needed);
131
+ }
132
+
133
+ /** Enforces a `bodyTaskIds` posture: every task the body names needs write access. */
134
+ export async function bodySessionDenial(
135
+ req: IncomingMessage,
136
+ taskIds: readonly unknown[],
137
+ ): Promise<string | null> {
138
+ const ids = [...new Set(taskIds.filter((id): id is string => typeof id === "string" && !!id))];
139
+ if (ids.length === 0) return null;
140
+ const actor = await resolveSessionActor(req, headerAgentId(req));
141
+ for (const id of ids) {
142
+ const denial = await denialFor(actor, id, "write");
143
+ if (denial) return denial;
144
+ }
145
+ return null;
146
+ }
147
+
148
+ function headerAgentId(req: IncomingMessage): string | undefined {
149
+ const header = req.headers["x-agent-id"];
150
+ return Array.isArray(header) ? header[0] : header;
151
+ }
152
+
153
+ export const USER_AGENT_HEADER_DENIED =
154
+ "A user token cannot act as an agent. Drop X-Agent-ID, or call with the agent's own credential.";
155
+
156
+ /**
157
+ * Refuses a user or guest token that claims agent identity: any X-Agent-ID,
158
+ * and the agent MCP transport at all (`/mcp-user` is the user transport).
159
+ * Runs once in `handleCore`, before any handler reads or changes state. A
160
+ * route that sets `auth.userNamesAgent` (the dashboard script playground)
161
+ * admits the pair for a user holding `agent.act.any`, an authorization of the
162
+ * user, never of the named agent.
163
+ */
164
+ export async function userAgentIdentityDenial(
165
+ req: IncomingMessage,
166
+ def: RouteDef | undefined,
167
+ pathSegments: string[],
168
+ ): Promise<string | null> {
169
+ const auth = getRequestAuth(req);
170
+ if (auth?.kind !== "user" && auth?.kind !== "guest") return null;
171
+ if (pathSegments.length === 1 && pathSegments[0] === "mcp") return USER_AGENT_HEADER_DENIED;
172
+ if (req.headers["x-agent-id"] === undefined) return null;
173
+ if (auth.kind === "user" && def?.auth?.userNamesAgent) {
174
+ if (!isRbacEnabled()) return null;
175
+ const grant = await getUserGrant(auth.userId);
176
+ if (grant.grantsAll || grant.verbs.has("agent.act.any")) return null;
177
+ }
178
+ return USER_AGENT_HEADER_DENIED;
179
+ }
@@ -24,7 +24,8 @@ import {
24
24
  SessionCostSchema,
25
25
  SessionLogSchema,
26
26
  } from "../types";
27
- import { route } from "./route-def";
27
+ import { bodyTask, route } from "./route-def";
28
+ import { canReadSession, SESSION_READ_DENIED } from "./session-access";
28
29
  import { recomputeSessionCost } from "./session-cost-recompute";
29
30
  import { cachedUsageReport } from "./usage-cache";
30
31
  import { jsonError } from "./utils";
@@ -127,6 +128,7 @@ const createSessionLogsRoute = route({
127
128
  method: "post",
128
129
  path: "/api/session-logs",
129
130
  pattern: ["api", "session-logs"],
131
+ session: bodyTask("taskId"),
130
132
  summary: "Store session logs",
131
133
  tags: ["Session Data"],
132
134
  body: z.object({
@@ -149,6 +151,7 @@ const getSessionLogsByTask = route({
149
151
  method: "get",
150
152
  path: "/api/tasks/{taskId}/session-logs",
151
153
  pattern: ["api", "tasks", null, "session-logs"],
154
+ session: { taskParam: "taskId" },
152
155
  summary: "Get session logs for a task",
153
156
  tags: ["Session Data"],
154
157
  params: z.object({ taskId: z.string() }),
@@ -172,6 +175,7 @@ const createSessionCostRoute = route({
172
175
  method: "post",
173
176
  path: "/api/session-costs",
174
177
  pattern: ["api", "session-costs"],
178
+ session: bodyTask("taskId"),
175
179
  summary: "Store session cost record",
176
180
  tags: ["Session Data"],
177
181
  body: z.object({
@@ -310,7 +314,7 @@ export async function handleSessionData(
310
314
  res: ServerResponse,
311
315
  pathSegments: string[],
312
316
  queryParams: URLSearchParams,
313
- _myAgentId: string | undefined,
317
+ myAgentId: string | undefined,
314
318
  ): Promise<boolean> {
315
319
  if (createSessionLogsRoute.match(req.method, pathSegments)) {
316
320
  const parsed = await createSessionLogsRoute.parse(req, res, pathSegments, queryParams);
@@ -353,6 +357,10 @@ export async function handleSessionData(
353
357
  jsonError(res, "Task not found", 404);
354
358
  return true;
355
359
  }
360
+ if (!(await canReadSession(req, myAgentId, task.id))) {
361
+ jsonError(res, SESSION_READ_DENIED, 403);
362
+ return true;
363
+ }
356
364
  const logs = await getSessionLogsByTaskId(parsed.params.taskId, parsed.query?.limit);
357
365
  getSessionLogsByTask.respond(res, 200, { logs });
358
366
  return true;
@@ -7,18 +7,40 @@ import {
7
7
  getTaskById,
8
8
  listRecentSessions,
9
9
  } from "../be/db";
10
+ import {
11
+ getSessionRoot,
12
+ listActiveSessionShares,
13
+ type SessionAccess,
14
+ sessionAccessForRoot,
15
+ setSessionShares,
16
+ toSessionSharesState,
17
+ } from "../be/session-shares";
10
18
  import { getTaskSteeringFieldsForTasks } from "../be/steering";
11
19
  import { getTaskCitationsForTasks, TaskCitationSchema } from "../be/task-citations";
12
20
  import {
21
+ findUserById,
13
22
  InvalidRuntimeTokenError,
14
23
  mintRuntimeToken,
15
24
  mintSessionToken,
16
25
  revokeSessionToken,
17
26
  } from "../be/users";
18
- import { type AgentTask, AgentTaskSchema, AgentTaskStatusSchema, SteerModeSchema } from "../types";
27
+ import { can } from "../rbac";
28
+ import { sessionAccessTopic } from "../realtime/auth";
29
+ import { realtimeBus } from "../realtime/bus";
30
+ import {
31
+ type AgentTask,
32
+ AgentTaskSchema,
33
+ AgentTaskStatusSchema,
34
+ SessionAccessSchema,
35
+ SessionOwnerSchema,
36
+ SessionSharesSchema,
37
+ SessionSharesUpdateSchema,
38
+ SteerModeSchema,
39
+ } from "../types";
19
40
  import { getRequestAuth } from "../utils/request-auth-context";
20
41
  import { isRuntimePoolsEnabled } from "../utils/runtime-pools-flag";
21
42
  import { route } from "./route-def";
43
+ import { resolveSessionActor, type SessionActor } from "./session-access";
22
44
  import { jsonError } from "./utils";
23
45
 
24
46
  // ─── Response Schemas ────────────────────────────────────────────────────────
@@ -91,6 +113,9 @@ const mintSessionTokenRoute = route({
91
113
  method: "post",
92
114
  path: "/api/sessions/tokens",
93
115
  pattern: ["api", "sessions", "tokens"],
116
+ session: {
117
+ none: "user tokens cannot mint: the handler admits only the operator key or the task's own runtime",
118
+ },
94
119
  summary: "Mint an ephemeral task or runtime token",
95
120
  description:
96
121
  "Operators can mint task or runtime tokens. Runtime tokens can mint task tokens for their own assigned tasks. Child tokens expire with their parent runtime.",
@@ -175,6 +200,14 @@ const listSessions = route({
175
200
  contextKeyPrefix: z.string().min(1).optional(),
176
201
  /** `full` restores the legacy shape (full root `AgentTask`); default is slim. */
177
202
  fields: z.enum(["full", "slim"]).optional(),
203
+ /**
204
+ * `mine`: sessions the caller owns. `shared`: sessions someone else owns
205
+ * with an active share to the caller or to everyone. Omit for every
206
+ * session the caller can open. Callers that act for no user (agents, the
207
+ * operator key with no picked dashboard user) see every session, and
208
+ * an empty list for either scope.
209
+ */
210
+ scope: z.enum(["mine", "shared"]).optional(),
178
211
  }),
179
212
  responses: {
180
213
  200: {
@@ -195,23 +228,78 @@ const getSession = route({
195
228
  method: "get",
196
229
  path: "/api/sessions/{rootTaskId}",
197
230
  pattern: ["api", "sessions", null],
231
+ session: { taskParam: "rootTaskId" },
198
232
  summary: "Get a session — root task + the entire descendant chain",
199
233
  tags: ["Sessions"],
200
234
  params: z.object({ rootTaskId: z.string() }),
201
235
  responses: {
202
236
  200: {
203
- description: "Root task + chain (ordered by createdAt)",
237
+ description:
238
+ "Root task + chain (ordered by createdAt), plus the caller's `access` and the session `owner`",
204
239
  schema: z.object({
205
240
  root: TaskWithSteeringSchema,
206
241
  chain: z.array(TaskWithSteeringSchema),
242
+ access: SessionAccessSchema,
243
+ owner: SessionOwnerSchema.nullable(),
207
244
  }),
208
245
  },
209
246
  401: { description: "Unauthorized" },
247
+ 403: { description: "The caller has no access to this session" },
248
+ 404: { description: "Root task not found" },
249
+ },
250
+ auth: { apiKey: true },
251
+ });
252
+
253
+ const getSessionShares = route({
254
+ method: "get",
255
+ path: "/api/sessions/{rootTaskId}/shares",
256
+ pattern: ["api", "sessions", null, "shares"],
257
+ session: { taskParam: "rootTaskId" },
258
+ summary: "List a session's active shares",
259
+ description:
260
+ "Active shares only. `everyone` is the access every user has; `users` are per-person shares. A session with no shares is private to its owner.",
261
+ tags: ["Sessions"],
262
+ params: z.object({ rootTaskId: z.string() }),
263
+ responses: {
264
+ 200: { description: "Active shares", schema: SessionSharesSchema },
265
+ 401: { description: "Unauthorized" },
266
+ 403: { description: "The caller has no access to this session" },
267
+ 404: { description: "Root task not found" },
268
+ },
269
+ auth: { apiKey: true },
270
+ });
271
+
272
+ const putSessionShares = route({
273
+ method: "put",
274
+ path: "/api/sessions/{rootTaskId}/shares",
275
+ pattern: ["api", "sessions", null, "shares"],
276
+ session: { taskParam: "rootTaskId" },
277
+ summary: "Set who can view or edit a session",
278
+ description:
279
+ "Owner only. The body is the full desired state: users left out lose access. Changed targets have their active share revoked and a new one inserted in one transaction; shares are never deleted.",
280
+ tags: ["Sessions"],
281
+ params: z.object({ rootTaskId: z.string() }),
282
+ body: SessionSharesUpdateSchema,
283
+ responses: {
284
+ 200: { description: "Active shares after the change", schema: SessionSharesSchema },
285
+ 400: { description: "Validation error or unknown user" },
286
+ 401: { description: "Unauthorized" },
287
+ 403: { description: "Only the session owner can share it" },
210
288
  404: { description: "Root task not found" },
289
+ 409: { description: "The session has no owner, so it is already open to everyone" },
211
290
  },
212
291
  auth: { apiKey: true },
292
+ rbac: { permission: "session.share" },
213
293
  });
214
294
 
295
+ /** The actor's access to the session rooted at `root`; `owner` when the actor bypasses checks. */
296
+ async function accessForRoot(
297
+ actor: SessionActor,
298
+ root: { id: string; requestedByUserId: string | null },
299
+ ): Promise<SessionAccess> {
300
+ return actor.kind === "bypass" ? "owner" : sessionAccessForRoot(actor.userId, root);
301
+ }
302
+
215
303
  // ─── Handler ─────────────────────────────────────────────────────────────────
216
304
 
217
305
  export async function handleSessions(
@@ -219,6 +307,7 @@ export async function handleSessions(
219
307
  res: ServerResponse,
220
308
  pathSegments: string[],
221
309
  queryParams: URLSearchParams,
310
+ myAgentId?: string,
222
311
  ): Promise<boolean> {
223
312
  if (mintSessionTokenRoute.match(req.method, pathSegments)) {
224
313
  const auth = getRequestAuth(req);
@@ -312,13 +401,30 @@ export async function handleSessions(
312
401
  .map((s) => s.trim())
313
402
  .filter(Boolean)
314
403
  : undefined;
315
- const baseOpts = {
316
- limit: parsed.query.limit,
317
- offset: parsed.query.offset,
404
+ const actor = await resolveSessionActor(req, myAgentId);
405
+ if (actor.kind === "bypass" && parsed.query.scope) {
406
+ // No user to own or receive a share.
407
+ listSessions.respond(res, 200, {
408
+ sessions: [],
409
+ total: 0,
410
+ limit: parsed.query.limit ?? 25,
411
+ offset: parsed.query.offset ?? 0,
412
+ });
413
+ return true;
414
+ }
415
+ const filterOpts = {
318
416
  source: sources,
319
417
  q: parsed.query.q,
320
418
  requestedByUserId: parsed.query.requestedByUserId,
321
419
  contextKeyPrefix: parsed.query.contextKeyPrefix,
420
+ ...(actor.kind === "user"
421
+ ? { viewer: { userId: actor.userId, scope: parsed.query.scope ?? "all" } as const }
422
+ : {}),
423
+ };
424
+ const baseOpts = {
425
+ ...filterOpts,
426
+ limit: parsed.query.limit,
427
+ offset: parsed.query.offset,
322
428
  };
323
429
  // List responses default to slim (root is a task summary); `?fields=full` restores it.
324
430
  const sessions =
@@ -327,12 +433,7 @@ export async function handleSessions(
327
433
  : await listRecentSessions({ ...baseOpts, slim: true });
328
434
  // Filter-aware total: same WHERE as the list query, so the UI pager
329
435
  // reflects the filtered result set.
330
- const total = await countSessions({
331
- source: sources,
332
- q: parsed.query.q,
333
- requestedByUserId: parsed.query.requestedByUserId,
334
- contextKeyPrefix: parsed.query.contextKeyPrefix,
335
- });
436
+ const total = await countSessions(filterOpts);
336
437
  listSessions.respond(res, 200, {
337
438
  sessions,
338
439
  total,
@@ -350,6 +451,25 @@ export async function handleSessions(
350
451
  jsonError(res, "Root task not found", 404);
351
452
  return true;
352
453
  }
454
+ // The route accepts any task id; access follows the topmost root.
455
+ const sessionRoot = (await getSessionRoot(root.id)) ?? {
456
+ id: root.id,
457
+ requestedByUserId: root.requestedByUserId ?? null,
458
+ };
459
+ const access = await accessForRoot(await resolveSessionActor(req, myAgentId), sessionRoot);
460
+ if (access === "none") {
461
+ jsonError(res, "You don't have access to this session. Ask the owner to share it.", 403);
462
+ return true;
463
+ }
464
+ const ownerUser = sessionRoot.requestedByUserId
465
+ ? await findUserById(sessionRoot.requestedByUserId)
466
+ : null;
467
+ const owner = sessionRoot.requestedByUserId
468
+ ? {
469
+ id: sessionRoot.requestedByUserId,
470
+ name: ownerUser?.name ?? sessionRoot.requestedByUserId,
471
+ }
472
+ : null;
353
473
  const chain = await getRootTaskChain(parsed.params.rootTaskId);
354
474
  // Fixed query count regardless of chain length: one agents lookup and one
355
475
  // citations lookup for the whole chain (chunked past 500 ids).
@@ -363,7 +483,87 @@ export async function handleSessions(
363
483
  ...steering.get(task.id)!,
364
484
  citations: citations.get(task.id)!,
365
485
  });
366
- getSession.respond(res, 200, { root: decorate(root), chain: chain.map(decorate) });
486
+ getSession.respond(res, 200, {
487
+ root: decorate(root),
488
+ chain: chain.map(decorate),
489
+ access,
490
+ owner,
491
+ });
492
+ return true;
493
+ }
494
+
495
+ if (getSessionShares.match(req.method, pathSegments)) {
496
+ const parsed = await getSessionShares.parse(req, res, pathSegments, queryParams);
497
+ if (!parsed) return true;
498
+ const root = await getSessionRoot(parsed.params.rootTaskId);
499
+ if (!root) {
500
+ jsonError(res, "Root task not found", 404);
501
+ return true;
502
+ }
503
+ const access = await accessForRoot(await resolveSessionActor(req, myAgentId), root);
504
+ if (access === "none") {
505
+ jsonError(res, "You don't have access to this session. Ask the owner to share it.", 403);
506
+ return true;
507
+ }
508
+ getSessionShares.respond(
509
+ res,
510
+ 200,
511
+ toSessionSharesState(await listActiveSessionShares(root.id)),
512
+ );
513
+ return true;
514
+ }
515
+
516
+ if (putSessionShares.match(req.method, pathSegments)) {
517
+ const parsed = await putSessionShares.parse(req, res, pathSegments, queryParams);
518
+ if (!parsed) return true;
519
+ const auth = getRequestAuth(req);
520
+ const actor = await resolveSessionActor(req, myAgentId);
521
+ // Sharing is a dashboard action: a person, or the operator key itself.
522
+ const principal =
523
+ auth?.kind === "user"
524
+ ? ({ kind: "user", userId: auth.userId } as const)
525
+ : auth?.kind === "operator" && !myAgentId
526
+ ? ({ kind: "operator" } as const)
527
+ : null;
528
+ if (
529
+ !principal ||
530
+ !can({ principal, verb: "session.share", resource: { kind: "none" }, source: "http" }).allow
531
+ ) {
532
+ jsonError(res, "Only the session owner can share it", 403);
533
+ return true;
534
+ }
535
+ const root = await getSessionRoot(parsed.params.rootTaskId);
536
+ if (!root) {
537
+ jsonError(res, "Root task not found", 404);
538
+ return true;
539
+ }
540
+ if (!root.requestedByUserId) {
541
+ jsonError(res, "This session has no owner, so it is already open to everyone", 409);
542
+ return true;
543
+ }
544
+ if (actor.kind === "user" && actor.userId !== root.requestedByUserId) {
545
+ jsonError(res, "Only the session owner can share it", 403);
546
+ return true;
547
+ }
548
+ const userIds = [...new Set(parsed.body.users.map((user) => user.userId))];
549
+ if (userIds.length !== parsed.body.users.length) {
550
+ jsonError(res, "Each user may appear only once", 400);
551
+ return true;
552
+ }
553
+ for (const userId of userIds) {
554
+ if (!(await findUserById(userId))) {
555
+ jsonError(res, `Unknown user ${userId}`, 400);
556
+ return true;
557
+ }
558
+ }
559
+ const state = await setSessionShares(
560
+ root,
561
+ parsed.body,
562
+ actor.kind === "user" ? actor.userId : null,
563
+ );
564
+ // Committed: presence sockets re-check access, so a revoked viewer leaves.
565
+ realtimeBus.publish(sessionAccessTopic(root.id), null);
566
+ putSessionShares.respond(res, 200, state);
367
567
  return true;
368
568
  }
369
569
 
package/src/http/stats.ts CHANGED
@@ -19,6 +19,7 @@ import { isMultiRuntimeEnabled } from "../utils/multi-runtime";
19
19
  import { resolveHttpFavoriteOwner } from "./favorite-owner";
20
20
  import { route } from "./route-def";
21
21
  import { scheduleSummaryWithFavoriteSchema } from "./schedules";
22
+ import { readableByFor, resolveSessionActor } from "./session-access";
22
23
 
23
24
  // ─── Response schemas ────────────────────────────────────────────────────────
24
25
 
@@ -222,11 +223,12 @@ export async function handleStats(
222
223
  if (!parsed) return true;
223
224
  const limit = parsed.query.limit ?? 100;
224
225
  const agentId = parsed.query.agentId;
226
+ const readableBy = readableByFor(await resolveSessionActor(req, myAgentId));
225
227
  let logs: AgentLog[] = [];
226
228
  if (agentId) {
227
- logs = (await getLogsByAgentId(agentId)).slice(0, limit);
229
+ logs = (await getLogsByAgentId(agentId, readableBy)).slice(0, limit);
228
230
  } else {
229
- logs = await getAllLogs(limit);
231
+ logs = await getAllLogs(limit, readableBy);
230
232
  }
231
233
  listLogs.respond(res, 200, { logs });
232
234
  return true;
@@ -306,7 +308,9 @@ export async function handleStats(
306
308
  }
307
309
 
308
310
  if (getConcurrentContextRoute.match(req.method, pathSegments)) {
309
- const context = await getConcurrentContext();
311
+ const context = await getConcurrentContext(
312
+ readableByFor(await resolveSessionActor(req, myAgentId)),
313
+ );
310
314
  getConcurrentContextRoute.respond(res, 200, context);
311
315
  return true;
312
316
  }