@desplega.ai/agent-swarm 2.3.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 (247) hide show
  1. package/README.md +1 -0
  2. package/dist/{acp-adapter-v85a2bqy.js → acp-adapter-kedstxx6.js} +6 -6
  3. package/dist/{actions-zgcxfspj.js → actions-vget6r1k.js} +18 -16
  4. package/dist/{amp-adapter-jw9xfz42.js → amp-adapter-xeb5mktv.js} +2 -2
  5. package/dist/{app-4e1s4675.js → app-mqap8t8m.js} +3 -3
  6. package/dist/{assistant-v238n9dk.js → assistant-h0gv97mv.js} +20 -18
  7. package/dist/{boot-reembed-p666cw5n.js → boot-reembed-qr8a894z.js} +1 -1
  8. package/dist/{boot-reembed-w6pm64j4.js → boot-reembed-rj85jjhw.js} +1 -1
  9. package/dist/{boot-scrub-sweep-jcyjagn3.js → boot-scrub-sweep-eyfvn6v9.js} +3 -3
  10. package/dist/{boot-seeds-gbedmamp.js → boot-seeds-xs9a8dx1.js} +4 -4
  11. package/dist/{claude-adapter-q7wspzt8.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-wza784fb.js → claude-managed-setup-gkfgks4j.js} +1 -1
  14. package/dist/{claude-sdk-session-yf400ptr.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-30m5q8mc.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-fhvqt2fp.js → cli-2rwet3r7.js} +1 -1
  22. package/dist/{cli-azs9bhc5.js → cli-3wjs22x3.js} +2 -2
  23. package/dist/{cli-dxwh4j32.js → cli-4fhyq711.js} +45 -36
  24. package/dist/{cli-yv3n83x8.js → cli-5ff2sjyt.js} +1 -1
  25. package/dist/{cli-6vcyw6v7.js → cli-5s5hx57y.js} +5 -3
  26. package/dist/{cli-hz64bcnj.js → cli-6ftrrwm1.js} +9 -371
  27. package/dist/{cli-zes4tsbb.js → cli-8b9wvjps.js} +1 -1
  28. package/dist/cli-8cxkw1f5.js +382 -0
  29. package/dist/{cli-4s1kqrqg.js → cli-8rwvksbc.js} +2 -2
  30. package/dist/{cli-e09hj50q.js → cli-8wkb09q5.js} +11 -11
  31. package/dist/{cli-yv0vgscc.js → cli-a1esmmee.js} +13 -2
  32. package/dist/{cli-4hmaheb6.js → cli-avmqhatn.js} +7 -7
  33. package/dist/{cli-g6p7c2my.js → cli-b80a1100.js} +1 -1
  34. package/dist/{cli-tr24eeny.js → cli-bfc3k1x5.js} +3 -3
  35. package/dist/{cli-ka01m5rb.js → cli-bhwa8rzc.js} +13 -11
  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-a17tzmn0.js → cli-ecshf504.js} +2 -2
  41. package/dist/{cli-d43xqzbe.js → cli-eqgfs202.js} +1 -1
  42. package/dist/{cli-ppapy34w.js → cli-f3nx1h8z.js} +2 -2
  43. package/dist/{cli-ea7chd26.js → cli-ffbrkh1r.js} +74 -35
  44. package/dist/{cli-xgn4rwyv.js → cli-kk2r80n5.js} +4 -4
  45. package/dist/{cli-hn3pftj2.js → cli-kz116kvw.js} +3 -3
  46. package/dist/{cli-1v8w782f.js → cli-mvg2hf8p.js} +4 -4
  47. package/dist/{cli-5d1m1qca.js → cli-mz1ezzzh.js} +1 -1
  48. package/dist/{cli-jhmjwqcg.js → cli-nme7wery.js} +6 -6
  49. package/dist/{cli-65q70v2v.js → cli-nrrg0fvy.js} +18 -4
  50. package/dist/{cli-8jtrekpy.js → cli-p1m2b6gc.js} +7 -7
  51. package/dist/{cli-re4wkbzx.js → cli-rg7ntypm.js} +328 -47
  52. package/dist/{cli-fp4xbw07.js → cli-rsd7ga7v.js} +10 -8
  53. package/dist/{cli-etvn5ej2.js → cli-sppc0cmw.js} +1 -1
  54. package/dist/{cli-qrwp40st.js → cli-tg9ksbn8.js} +1 -1
  55. package/dist/{cli-6p7ddg72.js → cli-tgvpmnwz.js} +2 -2
  56. package/dist/{cli-q1812jbw.js → cli-tj4j2ts4.js} +6 -6
  57. package/dist/{cli-kqypty34.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-y3dvz6ge.js → cli-x1p5dhq2.js} +3 -3
  62. package/dist/{cli-8qsx817b.js → cli-x719j3vs.js} +219 -629
  63. package/dist/{cli-9e9kjqsj.js → cli-xfe63zx3.js} +1 -1
  64. package/dist/{cli-wtg6edvd.js → cli-ywyrffff.js} +1 -1
  65. package/dist/{cli-0rh20bp3.js → cli-zvze9rbm.js} +1 -1
  66. package/dist/cli.js +14 -14
  67. package/dist/{codex-adapter-nhjj2veb.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-6hggrjbx.js → codex-session-runner-0vzn09t8.js} +3 -3
  70. package/dist/{commands-mrw1ph2w.js → commands-psqhkttb.js} +2 -2
  71. package/dist/{controller-pt1zam5w.js → controller-t20vz2k8.js} +2 -2
  72. package/dist/{controllers-connect-ymw5541d.js → controllers-connect-gkxmqw9d.js} +24 -20
  73. package/dist/{cursor-adapter-cf4zh5x0.js → cursor-adapter-3v542qe5.js} +1 -1
  74. package/dist/{db-hy80219q.js → db-2xbyenjr.js} +2 -2
  75. package/dist/{db-hhpy6m4x.js → db-nc1rd869.js} +3 -2
  76. package/dist/{dispatcher-6pdjtkr1.js → dispatcher-ewceb8ts.js} +17 -15
  77. package/dist/{dsh-adapter-fbx06re2.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-j6vb6ya1.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-1193sprp.js → grok-adapter-vj88xr23.js} +7 -7
  84. package/dist/handlers-zbs90bny.js +39 -0
  85. package/dist/{heartbeat-h1w1ktce.js → heartbeat-t2m9r3wa.js} +20 -18
  86. package/dist/{hook-skrbja18.js → hook-8femx1bg.js} +4 -4
  87. package/dist/{hook-ejja4bpe.js → hook-xa677f8r.js} +5 -5
  88. package/dist/{http-mzh41mfj.js → http-xpmmq028.js} +868 -253
  89. package/dist/{index-vjn3wbzf.js → index-8pwc1jc8.js} +17 -15
  90. package/dist/{index-epr2gnbw.js → index-cdzyfrsp.js} +1 -1
  91. package/dist/{index-xmmkyx82.js → index-dg2tmj99.js} +17 -15
  92. package/dist/index-nxa6me0q.js +41 -0
  93. package/dist/{index-6rmnn89d.js → index-s7ckdajq.js} +9 -9
  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-4djvbm2j.js → keepalive-3nr4st3r.js} +3 -3
  98. package/dist/{lead-61kqp604.js → lead-td8h2sfn.js} +33 -31
  99. package/dist/{maintenance-f3xhnjsm.js → maintenance-verwpe2c.js} +1 -1
  100. package/dist/{oauth-refresh-sweep-fpd6x6rw.js → oauth-refresh-sweep-9q2t0y3q.js} +2 -2
  101. package/dist/{onboard-196tjgyn.js → onboard-aytqedmq.js} +2 -2
  102. package/dist/{opencode-adapter-eeesgfqk.js → opencode-adapter-f4hryjny.js} +4 -4
  103. package/dist/{otel-impl-g8fh4c21.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-sdcf5yaz.js → pricing-refresh-5nvme3sm.js} +3 -3
  106. package/dist/{rbac-roles-bsp5657v.js → rbac-roles-pd8c26m1.js} +5 -3
  107. package/dist/{rbac-roles-2x4g1wb5.js → rbac-roles-vbpy21mg.js} +2 -2
  108. package/dist/{render-v2-0pyb39bb.js → render-v2-7nqgvk2q.js} +3 -3
  109. package/dist/{resume-a86tkj6r.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-ypbzcapd.js → setup-n39szm6b.js} +1 -1
  113. package/dist/{virtual-modules-wzk6jf3a.js → virtual-modules-wyp2k6z3.js} +2 -2
  114. package/dist/{work-objects-yjp8de12.js → work-objects-jem2fre0.js} +20 -18
  115. package/dist/{worker-4ptnn1ge.js → worker-wr6xgfr2.js} +33 -31
  116. package/openapi.json +844 -110
  117. package/package.json +1 -1
  118. package/src/apps/action-caller.ts +6 -0
  119. package/src/apps/script-types.ts +16 -3
  120. package/src/be/db/active-sessions.ts +5 -0
  121. package/src/be/db/session-visibility.ts +29 -0
  122. package/src/be/db/tasks/read.ts +61 -3
  123. package/src/be/db/tasks/write.ts +15 -6
  124. package/src/be/db.ts +92 -33
  125. package/src/be/events.ts +11 -0
  126. package/src/be/migrations/230_session_shares.sql +23 -0
  127. package/src/be/rbac-roles.ts +2 -0
  128. package/src/be/session-shares.ts +244 -0
  129. package/src/be/swarm-config-guard.ts +23 -2
  130. package/src/commands/runner.ts +11 -0
  131. package/src/http/active-sessions.ts +13 -2
  132. package/src/http/agents.ts +10 -3
  133. package/src/http/all-routes.ts +1 -0
  134. package/src/http/api-keys.ts +3 -1
  135. package/src/http/approval-requests.ts +2 -1
  136. package/src/http/apps.ts +36 -11
  137. package/src/http/config.ts +3 -0
  138. package/src/http/context.ts +2 -0
  139. package/src/http/core.ts +39 -2
  140. package/src/http/events.ts +6 -2
  141. package/src/http/favorite-owner.ts +1 -1
  142. package/src/http/fs.ts +38 -3
  143. package/src/http/index.ts +3 -1
  144. package/src/http/memory.ts +3 -1
  145. package/src/http/ranking.ts +367 -0
  146. package/src/http/realtime.ts +26 -21
  147. package/src/http/route-def.ts +77 -11
  148. package/src/http/script-runs.ts +2 -1
  149. package/src/http/scripts.ts +8 -0
  150. package/src/http/session-access.ts +179 -0
  151. package/src/http/session-data.ts +10 -2
  152. package/src/http/sessions.ts +212 -12
  153. package/src/http/stats.ts +7 -3
  154. package/src/http/tasks.ts +83 -7
  155. package/src/http/used-by.ts +11 -3
  156. package/src/prompts/session-templates.ts +3 -1
  157. package/src/providers/amp-adapter.ts +1 -1
  158. package/src/ranking/telemetry-ranking.ts +147 -0
  159. package/src/ranking/usage.ts +57 -0
  160. package/src/rbac/admission.ts +22 -7
  161. package/src/rbac/legacy-policy.ts +2 -0
  162. package/src/rbac/permissions.ts +9 -0
  163. package/src/realtime/auth.ts +69 -0
  164. package/src/realtime/tickets.ts +4 -1
  165. package/src/realtime/transport.ts +77 -9
  166. package/src/slack/blocks.ts +18 -0
  167. package/src/slack/dm.ts +29 -0
  168. package/src/telemetry-contract/EVENTS.md +3 -0
  169. package/src/telemetry-contract/SOURCE.json +37 -4
  170. package/src/telemetry-contract/catalog.json +69 -0
  171. package/src/telemetry-contract/ranking/fixtures/expected.json +30 -0
  172. package/src/telemetry-contract/ranking/fixtures/invalid/bad-category.json +11 -0
  173. package/src/telemetry-contract/ranking/fixtures/invalid/bad-claim.json +11 -0
  174. package/src/telemetry-contract/ranking/fixtures/invalid/blank-description.json +11 -0
  175. package/src/telemetry-contract/ranking/fixtures/invalid/blank-name.json +11 -0
  176. package/src/telemetry-contract/ranking/fixtures/invalid/description-too-long.json +11 -0
  177. package/src/telemetry-contract/ranking/fixtures/invalid/domain-uppercase.json +11 -0
  178. package/src/telemetry-contract/ranking/fixtures/invalid/domain-with-scheme.json +11 -0
  179. package/src/telemetry-contract/ranking/fixtures/invalid/domain-with-www.json +11 -0
  180. package/src/telemetry-contract/ranking/fixtures/invalid/http-source-url.json +11 -0
  181. package/src/telemetry-contract/ranking/fixtures/invalid/missing-description.json +10 -0
  182. package/src/telemetry-contract/ranking/fixtures/invalid/missing-version.json +10 -0
  183. package/src/telemetry-contract/ranking/fixtures/invalid/name-not-string.json +11 -0
  184. package/src/telemetry-contract/ranking/fixtures/invalid/name-too-long.json +11 -0
  185. package/src/telemetry-contract/ranking/fixtures/invalid/not-object.json +1 -0
  186. package/src/telemetry-contract/ranking/fixtures/invalid/null.json +1 -0
  187. package/src/telemetry-contract/ranking/fixtures/invalid/other-with-subcategory.json +11 -0
  188. package/src/telemetry-contract/ranking/fixtures/invalid/oversize.json +1 -0
  189. package/src/telemetry-contract/ranking/fixtures/invalid/short-claim.json +11 -0
  190. package/src/telemetry-contract/ranking/fixtures/invalid/subcategory-other-category.json +11 -0
  191. package/src/telemetry-contract/ranking/fixtures/invalid/trailing-data.json +1 -0
  192. package/src/telemetry-contract/ranking/fixtures/invalid/unknown-field.json +12 -0
  193. package/src/telemetry-contract/ranking/fixtures/invalid/unsupported-version.json +11 -0
  194. package/src/telemetry-contract/ranking/fixtures/invalid/updated-at-not-date.json +11 -0
  195. package/src/telemetry-contract/ranking/fixtures/invalid/updated-at-offset.json +11 -0
  196. package/src/telemetry-contract/ranking/fixtures/invalid/wrong-case-field.json +11 -0
  197. package/src/telemetry-contract/ranking/fixtures/valid/category-without-subcategory.json +11 -0
  198. package/src/telemetry-contract/ranking/fixtures/valid/full.json +11 -0
  199. package/src/telemetry-contract/ranking/fixtures/valid/minimal.json +11 -0
  200. package/src/telemetry-contract/ranking/fixtures/valid/padded-text.json +11 -0
  201. package/src/telemetry-contract/ranking/fixtures/valid/unicode-name.json +11 -0
  202. package/src/telemetry-contract/ranking/listing.v1.schema.json +61 -0
  203. package/src/telemetry-contract/ranking/taxonomy.json +8 -0
  204. package/src/telemetry-contract/ranking-taxonomy.gen.ts +11 -0
  205. package/src/telemetry-contract/types.gen.ts +708 -1
  206. package/src/telemetry.ts +26 -2
  207. package/src/tests/apps-rbac.test.ts +189 -2
  208. package/src/tests/apps-script-types.test.ts +16 -3
  209. package/src/tests/check-pr-body.test.ts +23 -0
  210. package/src/tests/check-ranking-entry.test.ts +146 -0
  211. package/src/tests/db-extraction-contract.test.ts +1 -0
  212. package/src/tests/db-task-read-contract.test.ts +1 -0
  213. package/src/tests/docs-index-footprint.test.js +2 -2
  214. package/src/tests/org-identity.test.ts +44 -0
  215. package/src/tests/ranking-listing.test.ts +112 -0
  216. package/src/tests/ranking-route.test.ts +319 -0
  217. package/src/tests/ranking-usage.test.ts +50 -0
  218. package/src/tests/rbac-admission-e2e.test.ts +8 -0
  219. package/src/tests/rbac-admission.test.ts +38 -0
  220. package/src/tests/rbac-charact-http.test.ts +4 -3
  221. package/src/tests/rbac-engine.test.ts +2 -0
  222. package/src/tests/rbac-roles.test.ts +2 -0
  223. package/src/tests/realtime-room-surfaces.test.ts +3 -1
  224. package/src/tests/realtime-session-join-race.test.ts +142 -0
  225. package/src/tests/realtime-tickets.test.ts +24 -17
  226. package/src/tests/realtime-transport.test.ts +91 -0
  227. package/src/tests/session-route-posture.test.ts +639 -0
  228. package/src/tests/session-sharing-routes.test.ts +282 -0
  229. package/src/tests/session-sharing.test.ts +526 -0
  230. package/src/tests/sessions-detail-batch.test.ts +7 -1
  231. package/src/tests/slack-tool-blocks.test.ts +101 -0
  232. package/src/tests/slack-update.test.ts +55 -0
  233. package/src/tests/slack-user-dm.test.ts +177 -0
  234. package/src/tests/used-by-route.test.ts +10 -0
  235. package/src/tools/slack-post.ts +18 -12
  236. package/src/tools/slack-read.ts +13 -1
  237. package/src/tools/slack-reply.ts +7 -10
  238. package/src/tools/slack-start-thread.ts +18 -11
  239. package/src/tools/slack-update.ts +2 -9
  240. package/src/types.ts +35 -0
  241. package/src/utils/org-identity.ts +33 -0
  242. package/templates/skills/apps/SKILL.md +17 -0
  243. package/templates/skills/apps/content.md +17 -0
  244. package/dist/handlers-tb3xgjnm.js +0 -37
  245. package/dist/index-nyfcdpky.js +0 -39
  246. package/dist/{cli-n5mrsg0f.js → cli-60cgh1rv.js} +3 -3
  247. /package/dist/{cli-j0v75y13.js → cli-y83zk5sb.js} +0 -0
@@ -1,10 +1,11 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import { z } from "zod";
3
- import { getCombConfig } from "../comb/config";
3
+ import { findUserById } from "../be/users";
4
4
  import { can } from "../rbac";
5
5
  import bundle from "../realtime/browser.generated.txt" with { type: "text" };
6
6
  import { issueRealtimeTicket } from "../realtime/tickets";
7
7
  import { getRequestAuth } from "../utils/request-auth-context";
8
+ import { dashboardUserIdHeader } from "./favorite-owner";
8
9
  import { route } from "./route-def";
9
10
  import { jsonError } from "./utils";
10
11
 
@@ -14,14 +15,16 @@ const realtimeTicketRoute = route({
14
15
  pattern: ["api", "realtime", "ticket"],
15
16
  summary: "Issue a one-shot dashboard realtime ticket",
16
17
  tags: ["Rooms"],
17
- rbac: { permission: "comb.presence" },
18
+ description:
19
+ "The ticket authorizes one dashboard socket for presence rooms: Comb drives (`comb.presence`) and sessions (`session.presence`). An operator-key tab's picked user (`X-Swarm-User-Id`) rides on the ticket so session presence can check that user's access.",
20
+ // Comb presence came first; a role granting only `comb.presence` keeps its ticket.
21
+ rbac: { permission: "session.presence", alsoAdmits: ["comb.presence"] },
18
22
  responses: {
19
23
  200: {
20
24
  description: "One-shot realtime ticket",
21
25
  schema: z.object({ ticket: z.string(), expiresAt: z.number().int() }),
22
26
  },
23
- 403: { description: "Comb presence is not allowed" },
24
- 404: { description: "Comb is not enabled" },
27
+ 403: { description: "Presence is not allowed" },
25
28
  },
26
29
  });
27
30
 
@@ -37,7 +40,7 @@ route({
37
40
  },
38
41
  });
39
42
 
40
- /** `POST /api/realtime/ticket`: a one-shot ticket for a dashboard realtime socket (Comb presence). */
43
+ /** `POST /api/realtime/ticket`: a one-shot ticket for a dashboard realtime socket (presence rooms). */
41
44
  export async function handleRealtimeTicket(
42
45
  req: IncomingMessage,
43
46
  res: ServerResponse,
@@ -45,34 +48,36 @@ export async function handleRealtimeTicket(
45
48
  queryParams: URLSearchParams,
46
49
  ): Promise<boolean> {
47
50
  if (!realtimeTicketRoute.match(req.method, pathSegments)) return false;
48
- // Comb presence is the only ticket consumer, so the route is off with Comb.
49
- if (!getCombConfig().enabled) {
50
- jsonError(res, "Comb is not enabled", 404);
51
- return true;
52
- }
51
+ // Session presence needs the ticket with Comb off too; each room still
52
+ // checks its own verb and access when the socket joins it.
53
53
  const parsed = await realtimeTicketRoute.parse(req, res, pathSegments, queryParams);
54
54
  if (!parsed) return true;
55
55
  const auth = getRequestAuth(req);
56
56
  if (!auth || auth.kind === "agent" || auth.kind === "guest" || auth.kind === "controller") {
57
- jsonError(res, "Comb presence requires dashboard authentication", 403);
57
+ jsonError(res, "Presence requires dashboard authentication", 403);
58
58
  return true;
59
59
  }
60
60
  const principal =
61
61
  auth.kind === "operator"
62
62
  ? ({ kind: "operator" } as const)
63
63
  : ({ kind: "user", userId: auth.userId } as const);
64
- if (
65
- !can({
66
- principal,
67
- verb: "comb.presence",
68
- resource: { kind: "none" },
69
- source: "http",
70
- }).allow
71
- ) {
72
- jsonError(res, "Comb presence requires the comb.presence permission", 403);
64
+ const allowed = (verb: "comb.presence" | "session.presence") =>
65
+ can({ principal, verb, resource: { kind: "none" }, source: "http" }).allow;
66
+ if (!allowed("comb.presence") && !allowed("session.presence")) {
67
+ jsonError(res, "Presence requires the comb.presence or session.presence permission", 403);
73
68
  return true;
74
69
  }
75
- realtimeTicketRoute.respond(res, 200, issueRealtimeTicket(auth));
70
+ let dashboardUserId: string | undefined;
71
+ if (auth.kind === "operator" && !auth.page) {
72
+ const picked = dashboardUserIdHeader(req);
73
+ const user = picked ? await findUserById(picked) : null;
74
+ if (user?.status === "active") dashboardUserId = user.id;
75
+ }
76
+ realtimeTicketRoute.respond(
77
+ res,
78
+ 200,
79
+ issueRealtimeTicket(dashboardUserId ? { ...auth, dashboardUserId } : auth),
80
+ );
76
81
  return true;
77
82
  }
78
83
 
@@ -76,7 +76,20 @@ export interface RouteDef<
76
76
  auth?: {
77
77
  apiKey?: boolean; // default true
78
78
  agentId?: boolean; // requires X-Agent-ID
79
+ /**
80
+ * Why a user token may name an agent in X-Agent-ID here. Everywhere else
81
+ * `handleCore` refuses that pair. The user still needs `agent.act.any`.
82
+ */
83
+ userNamesAgent?: string;
79
84
  };
85
+ /**
86
+ * Session posture: how a user caller's session access gates this route.
87
+ * Required on every route keyed by a task, steering message or attachment,
88
+ * on every write whose body names a task, and on every active-session write
89
+ * (`session-route-posture.test.ts`).
90
+ * `handleCore` enforces the path forms; `parse()` enforces `bodyTaskIds`.
91
+ */
92
+ session?: SessionPosture;
80
93
  /**
81
94
  * RBAC posture (DES-445). Required on every non-GET route — enforced by
82
95
  * `bun run check:rbac-coverage` (CI). Either name the `PermissionVerb` the
@@ -84,7 +97,56 @@ export interface RouteDef<
84
97
  * Declarative in slice 1: the handler still calls `can()` itself; the role
85
98
  * engine (increment 3) may enforce the declared verb centrally.
86
99
  */
87
- rbac?: { permission: PermissionVerb } | { ungated: string };
100
+ rbac?:
101
+ | { permission: PermissionVerb; alsoAdmits?: readonly PermissionVerb[] }
102
+ | { ungated: string };
103
+ }
104
+
105
+ /**
106
+ * - `taskParam` / `steeringParam`: the path param naming a task, or a steering
107
+ * message whose task counts. A GET needs read access, any other method write.
108
+ * - `activeSessionParam`: the path param naming an active-session row; its task
109
+ * counts the same way. A row with no task admits no user.
110
+ * - `bodyTaskIds`: the task ids a write's body names; each needs write access.
111
+ * - `agentWide`: why the route changes sessions across a whole agent. It
112
+ * refuses every user actor; agents and the operator key keep access.
113
+ * - `none`: why the session does not gate this route.
114
+ */
115
+ export type SessionPosture =
116
+ | { taskParam: string }
117
+ | { steeringParam: string }
118
+ | { activeSessionParam: string }
119
+ | { agentWide: string }
120
+ | { bodyTaskIds: (body: unknown) => readonly unknown[] }
121
+ | { none: string };
122
+
123
+ /** A `bodyTaskIds` posture reading one top-level body key. */
124
+ export function bodyTask(key: string): SessionPosture {
125
+ return { bodyTaskIds: (body) => [(body as Record<string, unknown> | null | undefined)?.[key]] };
126
+ }
127
+
128
+ /** A `bodyTaskIds` posture reading `key` off every item of the body's `listKey` array. */
129
+ export function bodyListTask(listKey: string, key: string): SessionPosture {
130
+ return {
131
+ bodyTaskIds: (body) => {
132
+ const items = (body as Record<string, unknown> | null | undefined)?.[listKey];
133
+ return Array.isArray(items)
134
+ ? items.map((item) => (item as Record<string, unknown> | null)?.[key])
135
+ : [];
136
+ },
137
+ };
138
+ }
139
+
140
+ /** The raw `{name}` path params of `def` read off the request's segments. */
141
+ export function routePathParams(def: RouteDef, pathSegments: string[]): Record<string, string> {
142
+ const params: Record<string, string> = {};
143
+ const paramNames = def.path.match(/\{(\w+)\}/g)?.map((p) => p.slice(1, -1)) ?? [];
144
+ for (let i = 0; i < def.pattern.length; i++) {
145
+ if (def.pattern[i] === null && paramNames.length > 0) {
146
+ params[paramNames.shift()!] = pathSegments[i] ?? "";
147
+ }
148
+ }
149
+ return params;
88
150
  }
89
151
 
90
152
  interface ParsedRequest<TParams, TQuery, TBody> {
@@ -367,21 +429,25 @@ export function route<
367
429
 
368
430
  async parse(req, res, pathSegments, queryParams) {
369
431
  try {
370
- // Extract path params from dynamic segments
371
- const rawParams: Record<string, string> = {};
372
- if (def.params) {
373
- const paramNames = def.path.match(/\{(\w+)\}/g)?.map((p) => p.slice(1, -1)) ?? [];
374
- for (let i = 0; i < def.pattern.length; i++) {
375
- if (def.pattern[i] === null && paramNames.length > 0) {
376
- rawParams[paramNames.shift()!] = pathSegments[i] ?? "";
377
- }
432
+ const rawBody = def.body ? await parseBody(req, def.maxBodyBytes) : undefined;
433
+ // Session access first, so a malformed body cannot tell a reader
434
+ // anything about a session they cannot write in.
435
+ if (def.session && "bodyTaskIds" in def.session) {
436
+ // Lazy: session-access reaches the DB layer, which imports route files.
437
+ const { bodySessionDenial } = await import("./session-access");
438
+ const denial = await bodySessionDenial(req, def.session.bodyTaskIds(rawBody));
439
+ if (denial) {
440
+ jsonError(res, denial, 403);
441
+ return null;
378
442
  }
379
443
  }
380
444
 
381
445
  // Parse + validate each part
382
- const params = def.params ? def.params.parse(rawParams) : undefined;
446
+ const params = def.params
447
+ ? def.params.parse(routePathParams(def, pathSegments))
448
+ : undefined;
383
449
  const query = def.query ? def.query.parse(Object.fromEntries(queryParams)) : undefined;
384
- const body = def.body ? def.body.parse(await parseBody(req, def.maxBodyBytes)) : undefined;
450
+ const body = def.body ? def.body.parse(rawBody) : undefined;
385
451
 
386
452
  return { params, query, body } as ParsedRequest<
387
453
  z.infer<TParams>,
@@ -47,7 +47,7 @@ import {
47
47
  RawLlmConfigSchema,
48
48
  RawLlmOutputSchema,
49
49
  } from "../workflows/executors/raw-llm";
50
- import { route } from "./route-def";
50
+ import { bodyTask, route } from "./route-def";
51
51
  import { json, jsonError } from "./utils";
52
52
 
53
53
  const DEFAULT_SCRIPT_RUN_CONCURRENCY_CAP = 10;
@@ -282,6 +282,7 @@ const agentTaskRoute = route({
282
282
  method: "post",
283
283
  path: "/api/internal/script-runs/{runId}/agent-task",
284
284
  pattern: ["api", "internal", "script-runs", null, "agent-task"],
285
+ session: bodyTask("parentTaskId"),
285
286
  operationId: "script_runs_internal_agent_task",
286
287
  summary: "Create or wait for a script workflow agent task step",
287
288
  tags: ["Script Runs"],
@@ -225,6 +225,10 @@ const upsertRoute = route({
225
225
  method: "post",
226
226
  path: "/api/scripts/upsert",
227
227
  pattern: ["api", "scripts", "upsert"],
228
+ auth: {
229
+ userNamesAgent:
230
+ "dashboard script playground: a user runs or saves a script in a chosen agent's scope",
231
+ },
228
232
  operationId: "scripts_upsert",
229
233
  summary: "Create or update a reusable script",
230
234
  description: "Explicit script upserts run a TypeScript typecheck before writing.",
@@ -253,6 +257,10 @@ const runRoute = route({
253
257
  method: "post",
254
258
  path: "/api/scripts/run",
255
259
  pattern: ["api", "scripts", "run"],
260
+ auth: {
261
+ userNamesAgent:
262
+ "dashboard script playground: a user runs or saves a script in a chosen agent's scope",
263
+ },
256
264
  operationId: "scripts_run",
257
265
  summary: "Run a reusable or inline script",
258
266
  description:
@@ -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;