@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
package/src/http/apps.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import * as z from "zod";
3
+ import type { AppActionCaller } from "../apps/action-caller";
3
4
  import {
4
5
  type AppDefinition,
5
6
  AppNameSchema,
@@ -660,45 +661,64 @@ const syncAppRoute = route({
660
661
  });
661
662
 
662
663
  /**
663
- * RBAC-gate an app operation and resolve the acting principal's stable actor id
664
- * (`user:<id>`, `agent:<id>`, or `operator`) for row provenance. Returns null
665
- * (after writing the 403) when the write is denied.
664
+ * Check app permissions and resolve server-verified identity for action input
665
+ * and row provenance. Return null after a 403 response when access is denied.
666
666
  */
667
- async function authorizeApp(
667
+ async function authorizeAppCaller(
668
668
  req: IncomingMessage,
669
669
  res: ServerResponse,
670
670
  myAgentId: string | undefined,
671
671
  verb: "app.manage" | "app.use",
672
672
  resource: RbacResource,
673
- ): Promise<string | null> {
673
+ ): Promise<{ actor: string; caller: AppActionCaller } | null> {
674
674
  const auth = getRequestAuth(req);
675
675
  let principal: RbacPrincipal;
676
676
  let actor: string;
677
+ let caller: AppActionCaller;
677
678
  if (auth?.kind === "operator") {
678
679
  principal = { kind: "operator" };
679
- actor = "operator";
680
+ // Page sessions never act as an agent, even if a header names one.
681
+ const agent = myAgentId && !auth.page ? await getAgentById(myAgentId) : null;
682
+ actor = agent ? `agent:${agent.id}` : "operator";
683
+ caller = agent
684
+ ? { kind: "agent", id: agent.id, name: agent.name }
685
+ : { kind: "operator", id: null };
680
686
  } else if (auth?.kind === "user") {
681
687
  principal = { kind: "user", userId: auth.userId };
682
688
  actor = `user:${auth.userId}`;
683
- } else if (auth?.kind === "guest") {
689
+ caller = { kind: "user", id: auth.userId, name: auth.user.name };
690
+ } else if (auth?.kind === "guest" || auth?.kind === "controller") {
684
691
  principal = { kind: "guest" };
685
692
  actor = "guest";
693
+ caller = { kind: "guest", id: null };
686
694
  } else {
687
695
  const agent = myAgentId ? await getAgentById(myAgentId) : null;
688
696
  principal = { kind: "agent", agentId: myAgentId ?? "", isLead: agent?.isLead ?? false };
689
697
  actor = myAgentId ? `agent:${myAgentId}` : "agent";
698
+ caller = { kind: "agent", id: myAgentId ?? null, name: agent?.name };
690
699
  }
700
+ if (auth?.page) caller.pageId = auth.page.id;
691
701
  const decision = can({
692
702
  principal,
693
703
  verb,
694
704
  resource,
695
705
  source: "http",
696
706
  });
697
- if (decision.allow) return actor;
707
+ if (decision.allow) return { actor, caller };
698
708
  jsonError(res, decision.reason, 403);
699
709
  return null;
700
710
  }
701
711
 
712
+ async function authorizeApp(
713
+ req: IncomingMessage,
714
+ res: ServerResponse,
715
+ myAgentId: string | undefined,
716
+ verb: "app.manage" | "app.use",
717
+ resource: RbacResource,
718
+ ): Promise<string | null> {
719
+ return (await authorizeAppCaller(req, res, myAgentId, verb, resource))?.actor ?? null;
720
+ }
721
+
702
722
  async function authorizeAppManage(
703
723
  req: IncomingMessage,
704
724
  res: ServerResponse,
@@ -1326,8 +1346,12 @@ export async function handleApps(
1326
1346
  if (enforceContentLengthCap(req, res, MAX_APP_ROW_BODY_BYTES) === BODY_TOO_LARGE) return true;
1327
1347
  const parsed = await runActionRoute.parse(req, res, pathSegments, queryParams);
1328
1348
  if (!parsed) return true;
1329
- const actor = await authorizeAppUse(req, res, myAgentId, parsed.params.id);
1330
- if (!actor) return true;
1349
+ const identity = await authorizeAppCaller(req, res, myAgentId, "app.use", {
1350
+ kind: "app",
1351
+ appId: parsed.params.id,
1352
+ });
1353
+ if (!identity) return true;
1354
+ const { actor, caller } = identity;
1331
1355
 
1332
1356
  const app = await getApp(parsed.params.id);
1333
1357
  if (!app) {
@@ -1375,7 +1399,7 @@ export async function handleApps(
1375
1399
 
1376
1400
  const output = await runSavedScriptAsAgent({
1377
1401
  script,
1378
- input: { ...action.args, ...input, app: { id: app.id } },
1402
+ input: { ...action.args, ...input, app: { id: app.id }, caller },
1379
1403
  agentId: runAsAgentId,
1380
1404
  });
1381
1405
  const ok = output.exitCode === 0 && !output.error && !output.runtimeError;
@@ -1434,6 +1458,7 @@ export async function handleApps(
1434
1458
  app_id: app.id,
1435
1459
  action_name: parsed.params.name,
1436
1460
  input_json: JSON.stringify(input),
1461
+ caller_json: JSON.stringify(caller),
1437
1462
  });
1438
1463
  const task = await createTaskWithSiblingAwareness(
1439
1464
  taskPrompt.text,
@@ -7,6 +7,7 @@ import { resolveHttpAuditUserId } from "../be/audit-user";
7
7
  import {
8
8
  getDb,
9
9
  getDbClient,
10
+ getRootTaskChain,
10
11
  getTaskById,
11
12
  listAssetSummaries,
12
13
  moveAssetKey,
@@ -120,10 +121,17 @@ function ensureOperator(req: IncomingMessage, res: ServerResponse): boolean {
120
121
  }
121
122
 
122
123
  const moveAssetBodySchema = z.object({ key: AssetKeySchema });
124
+ const moveGenericAssetBodySchema = moveAssetBodySchema.extend({
125
+ cascade: z
126
+ .boolean()
127
+ .optional()
128
+ .describe("For tasks only, also move every descendant task recursively through parentTaskId."),
129
+ });
123
130
  const moveAssetSuccessSchema = z.object({
124
131
  entityType: AssetEntityTypeSchema,
125
132
  id: z.string(),
126
133
  key: AssetKeySchema,
134
+ movedTaskIds: z.array(z.string()).optional(),
127
135
  });
128
136
  const moveAssetResponses = {
129
137
  200: { description: "Asset namespace updated", schema: moveAssetSuccessSchema },
@@ -168,7 +176,7 @@ const moveAssetRoute = route({
168
176
  "Updates namespace metadata only. Provider-backed files keep the same provider key, org, and drive; no remote move occurs. Personal keys are labels, not a privacy guarantee.",
169
177
  tags: ["Assets"],
170
178
  params: z.object({ entityType: AssetEntityTypeSchema, id: z.string().min(1) }),
171
- body: moveAssetBodySchema,
179
+ body: moveGenericAssetBodySchema,
172
180
  responses: moveAssetResponses,
173
181
  rbac: {
174
182
  ungated:
@@ -302,17 +310,29 @@ export async function handleAssets(
302
310
  ) {
303
311
  const parsed = await moveAssetRoute.parse(req, res, pathSegments, queryParams);
304
312
  if (!parsed) return true;
313
+ if (parsed.body.cascade && parsed.params.entityType !== "task") {
314
+ jsonError(res, "Cascade is only supported for task namespace moves", 400);
315
+ return true;
316
+ }
305
317
  if (parsed.params.entityType === "file" && !ensureOperator(req, res)) return true;
318
+ let descendantTaskIds: string[] | undefined;
306
319
  if (parsed.params.entityType === "task") {
307
320
  const task = await getTaskById(parsed.params.id);
308
321
  if (!task) {
309
322
  jsonError(res, "Asset not found", 404);
310
323
  return true;
311
324
  }
312
- if (!(await canMutateTaskNamespace(task, myAgentId, req))) {
313
- jsonError(res, "Not authorized to move this task namespace", 403);
314
- return true;
325
+ // Only the tasks checked here move: a subtask created after this read stays put.
326
+ const tasks = parsed.body.cascade ? await getRootTaskChain(task.id) : [task];
327
+ for (const target of tasks) {
328
+ if (!(await canMutateTaskNamespace(target, myAgentId, req))) {
329
+ jsonError(res, "Not authorized to move this task namespace", 403);
330
+ return true;
331
+ }
315
332
  }
333
+ descendantTaskIds = tasks
334
+ .filter((target) => target.id !== task.id)
335
+ .map((target) => target.id);
316
336
  }
317
337
  if (parsed.params.entityType === "app") {
318
338
  if (!(await getApp(parsed.params.id))) {
@@ -344,6 +364,7 @@ export async function handleAssets(
344
364
  id: parsed.params.id,
345
365
  key,
346
366
  changedBy: actor ?? undefined,
367
+ descendantTaskIds,
347
368
  });
348
369
  if (!moved) {
349
370
  jsonError(res, "Asset not found", 404);
@@ -353,6 +374,7 @@ export async function handleAssets(
353
374
  entityType: parsed.params.entityType,
354
375
  id: parsed.params.id,
355
376
  key,
377
+ ...moved,
356
378
  });
357
379
  } catch (error) {
358
380
  if (error instanceof AssetKeyAuthorizationError) {
@@ -31,6 +31,7 @@ import { SwarmConfigSchema } from "../types";
31
31
  import { getRequestAuth, type HttpRequestAuth } from "../utils/request-auth-context";
32
32
  import { registerVolatileSecret } from "../utils/secret-scrubber";
33
33
  import { reloadGlobalConfigsAndIntegrations, scheduleIntegrationsReload } from "./core";
34
+ import { syncRankingAfterConfigChange } from "./ranking";
34
35
  import { route } from "./route-def";
35
36
  import { duplicateGuardedQueryKey, jsonError } from "./utils";
36
37
 
@@ -527,6 +528,7 @@ export async function handleConfig(
527
528
  if (scope === "global" && key === "SWARM_ORG_WEBSITE") {
528
529
  await fillOrgLogoFromWebsite(String(value));
529
530
  }
531
+ if (scope === "global") await syncRankingAfterConfigChange(key, String(value));
530
532
  if (scope === "global") {
531
533
  scheduleIntegrationsReload();
532
534
  }
@@ -561,6 +563,7 @@ export async function handleConfig(
561
563
  return true;
562
564
  }
563
565
  if (existing.scope === "global") {
566
+ await syncRankingAfterConfigChange(existing.key, null);
564
567
  scheduleIntegrationsReload();
565
568
  }
566
569
  if (
@@ -43,6 +43,7 @@ const postContext = route({
43
43
  method: "post",
44
44
  path: "/api/tasks/{id}/context",
45
45
  pattern: ["api", "tasks", null, "context"],
46
+ session: { taskParam: "id" },
46
47
  summary: "Record a context usage snapshot for a task",
47
48
  tags: ["Tasks"],
48
49
  params: z.object({ id: z.string() }),
@@ -72,6 +73,7 @@ const getContext = route({
72
73
  method: "get",
73
74
  path: "/api/tasks/{id}/context",
74
75
  pattern: ["api", "tasks", null, "context"],
76
+ session: { taskParam: "id" },
75
77
  summary: "Get context usage history for a task",
76
78
  tags: ["Tasks"],
77
79
  params: z.object({ id: z.string() }),
package/src/http/core.ts CHANGED
@@ -47,6 +47,14 @@ import { refreshSecretScrubberCache, registerSensitiveKeyName } from "../utils/s
47
47
  import { resolveHttpRequestAuth } from "./auth";
48
48
  import { generateOpenApiSpec, SCALAR_HTML } from "./openapi";
49
49
  import { findRoute, isPublicRoute, route, runtimeInstanceHeader } from "./route-def";
50
+ import {
51
+ canReadSession,
52
+ pathSessionDenial,
53
+ readableByFor,
54
+ resolveSessionActor,
55
+ SESSION_READ_DENIED,
56
+ userAgentIdentityDenial,
57
+ } from "./session-access";
50
58
  import {
51
59
  agentWithCapacity,
52
60
  duplicateGuardedQueryKey,
@@ -503,6 +511,15 @@ export async function handleCore(
503
511
  setRequestAuth(req, auth);
504
512
  }
505
513
 
514
+ // A user token never gains agent identity from X-Agent-ID, on /mcp, /api/poll
515
+ // or any worker-header route.
516
+ const routeDef = findRoute(req.method, pathSegments);
517
+ const agentIdentityDenial = await userAgentIdentityDenial(req, routeDef, pathSegments);
518
+ if (agentIdentityDenial) {
519
+ jsonError(res, agentIdentityDenial, 403);
520
+ return true;
521
+ }
522
+
506
523
  if (auth?.kind === "agent") {
507
524
  const query = parseQueryParams(req.url || "");
508
525
  const duplicate = duplicateGuardedQueryKey(query);
@@ -565,7 +582,7 @@ export async function handleCore(
565
582
  if (auth?.kind === "user" && isRbacEnabled()) {
566
583
  const grant = await getUserGrant(auth.userId);
567
584
  if (!grant.grantsAll) {
568
- const def = findRoute(req.method, pathSegments);
585
+ const def = routeDef;
569
586
  const decision = decideAdmission({
570
587
  method: req.method ?? "",
571
588
  rbac: def?.rbac,
@@ -585,6 +602,16 @@ export async function handleCore(
585
602
  }
586
603
  }
587
604
 
605
+ // Session access for every route keyed by a task or steering message, before
606
+ // its handler reads or changes anything (the route's `session` posture).
607
+ if (routeDef?.session) {
608
+ const sessionDenial = await pathSessionDenial(req, myAgentId, routeDef, pathSegments);
609
+ if (sessionDenial) {
610
+ jsonError(res, sessionDenial, 403);
611
+ return true;
612
+ }
613
+ }
614
+
588
615
  // POST /internal/reload-config — re-read swarm_config into process.env and re-init integrations
589
616
  if (req.method === "POST" && req.url === "/internal/reload-config") {
590
617
  try {
@@ -662,7 +689,14 @@ export async function handleCore(
662
689
  const queryParams = parseQueryParams(req.url || "");
663
690
  const taskId = queryParams.get("taskId");
664
691
 
692
+ // A user token naming an agent in X-Agent-ID still reads only its sessions.
693
+ const sessionActor = await resolveSessionActor(req, myAgentId);
665
694
  if (taskId) {
695
+ if (!(await canReadSession(req, myAgentId, taskId))) {
696
+ res.writeHead(403, { "Content-Type": "application/json" });
697
+ res.end(JSON.stringify({ error: SESSION_READ_DENIED }));
698
+ return true;
699
+ }
666
700
  // Check if specific task is cancelled
667
701
  const task = await getTaskById(taskId);
668
702
  if (task && task.status === "cancelled") {
@@ -687,7 +721,10 @@ export async function handleCore(
687
721
  }
688
722
 
689
723
  // No taskId - return all recently cancelled tasks for this agent
690
- const cancelledTasks = await getRecentlyCancelledTasksForAgent(myAgentId);
724
+ const cancelledTasks = await getRecentlyCancelledTasksForAgent(
725
+ myAgentId,
726
+ readableByFor(sessionActor),
727
+ );
691
728
  res.writeHead(200, { "Content-Type": "application/json" });
692
729
  res.end(JSON.stringify({ cancelled: cancelledTasks }));
693
730
  return true;
@@ -13,7 +13,8 @@ import {
13
13
  EventStatusSchema,
14
14
  SwarmEventSchema,
15
15
  } from "../types";
16
- import { route } from "./route-def";
16
+ import { bodyListTask, bodyTask, route } from "./route-def";
17
+ import { readableByFor, resolveSessionActor } from "./session-access";
17
18
  import { jsonError } from "./utils";
18
19
 
19
20
  // ─── Route Definitions ───────────────────────────────────────────────────────
@@ -36,6 +37,7 @@ const createEventRoute = route({
36
37
  method: "post",
37
38
  path: "/api/events",
38
39
  pattern: ["api", "events"],
40
+ session: bodyTask("taskId"),
39
41
  summary: "Store a single event",
40
42
  tags: ["Events"],
41
43
  body: eventBodySchema,
@@ -53,6 +55,7 @@ const createEventsBatchRoute = route({
53
55
  method: "post",
54
56
  path: "/api/events/batch",
55
57
  pattern: ["api", "events", "batch"],
58
+ session: bodyListTask("events", "taskId"),
56
59
  summary: "Store multiple events in a batch",
57
60
  tags: ["Events"],
58
61
  body: z.object({
@@ -153,7 +156,7 @@ export async function handleEvents(
153
156
  res: ServerResponse,
154
157
  pathSegments: string[],
155
158
  queryParams: URLSearchParams,
156
- _myAgentId: string | undefined,
159
+ myAgentId: string | undefined,
157
160
  ): Promise<boolean> {
158
161
  // Match batch BEFORE generic /api/events (POST)
159
162
  if (createEventsBatchRoute.match(req.method, pathSegments)) {
@@ -252,6 +255,7 @@ export async function handleEvents(
252
255
  limit: parsed.query.limit ?? 100,
253
256
  events: eventFilter,
254
257
  latestPerDataField: latestPerDataField ? { events: latestEvents, dataFields } : undefined,
258
+ readableBy: readableByFor(await resolveSessionActor(req, myAgentId)),
255
259
  });
256
260
  getEventsRoute.respond(
257
261
  res,
@@ -16,7 +16,7 @@ export type FavoriteOwner = {
16
16
  */
17
17
  export const DASHBOARD_USER_HEADER = "x-swarm-user-id";
18
18
 
19
- function dashboardUserIdHeader(req: IncomingMessage): string | null {
19
+ export function dashboardUserIdHeader(req: IncomingMessage): string | null {
20
20
  const raw = req.headers[DASHBOARD_USER_HEADER];
21
21
  const value = (Array.isArray(raw) ? raw[0] : raw)?.trim();
22
22
  return value ? value : null;
package/src/http/fs.ts CHANGED
@@ -15,6 +15,7 @@ import { getCurrentRequestAuth } from "../utils/request-auth-context";
15
15
  import { scrubSecrets } from "../utils/secret-scrubber";
16
16
  import { requestPrincipal } from "./request-principal";
17
17
  import { route } from "./route-def";
18
+ import { canReadSession, SESSION_READ_DENIED } from "./session-access";
18
19
  import { BODY_TOO_LARGE, enforceContentLengthCap, jsonError } from "./utils";
19
20
 
20
21
  // Upload wall-clock past which the provider round-trip is worth a log line.
@@ -126,6 +127,7 @@ const listTaskFilesRoute = route({
126
127
  method: "get",
127
128
  path: "/api/fs/tasks/{taskId}/files",
128
129
  pattern: ["api", "fs", "tasks", null, "files"],
130
+ session: { taskParam: "taskId" },
129
131
  summary: "List task file attachments",
130
132
  tags: ["FS"],
131
133
  params: taskParams,
@@ -142,6 +144,7 @@ const uploadTaskFileRoute = route({
142
144
  method: "post",
143
145
  path: "/api/fs/tasks/{taskId}/files",
144
146
  pattern: ["api", "fs", "tasks", null, "files"],
147
+ session: { taskParam: "taskId" },
145
148
  summary: "Upload a binary task file attachment",
146
149
  description:
147
150
  "Accepts a raw binary request body. Pass the display/path name as the `name` query parameter.",
@@ -163,6 +166,7 @@ const getTaskFileRoute = route({
163
166
  method: "get",
164
167
  path: "/api/fs/tasks/{taskId}/files/{attachmentId}",
165
168
  pattern: ["api", "fs", "tasks", null, "files", null],
169
+ session: { taskParam: "taskId" },
166
170
  summary: "Get task file attachment metadata",
167
171
  tags: ["FS"],
168
172
  params: attachmentParams,
@@ -176,6 +180,7 @@ const downloadTaskFileRoute = route({
176
180
  method: "get",
177
181
  path: "/api/fs/tasks/{taskId}/files/{attachmentId}/raw",
178
182
  pattern: ["api", "fs", "tasks", null, "files", null, "raw"],
183
+ session: { taskParam: "taskId" },
179
184
  summary: "Download raw task file bytes",
180
185
  description: "Streams raw bytes. File content is not secret-scrubbed.",
181
186
  tags: ["FS"],
@@ -193,6 +198,7 @@ const signedUrlTaskFileRoute = route({
193
198
  method: "get",
194
199
  path: "/api/fs/tasks/{taskId}/files/{attachmentId}/signed-url",
195
200
  pattern: ["api", "fs", "tasks", null, "files", null, "signed-url"],
201
+ session: { taskParam: "taskId" },
196
202
  summary: "Create a provider signed GET URL for a task file",
197
203
  tags: ["FS"],
198
204
  params: attachmentParams,
@@ -208,6 +214,7 @@ const deleteTaskFileRoute = route({
208
214
  method: "delete",
209
215
  path: "/api/fs/tasks/{taskId}/files/{attachmentId}",
210
216
  pattern: ["api", "fs", "tasks", null, "files", null],
217
+ session: { taskParam: "taskId" },
211
218
  summary: "Delete a task file attachment",
212
219
  tags: ["FS"],
213
220
  params: attachmentParams,
@@ -263,7 +270,13 @@ export async function handleFs(
263
270
  if (downloadTaskFileRoute.match(req.method, pathSegments)) {
264
271
  const parsed = await downloadTaskFileRoute.parse(req, res, pathSegments, queryParams);
265
272
  if (!parsed) return true;
266
- const attachment = await findAttachment(parsed.params.taskId, parsed.params.attachmentId, res);
273
+ const attachment = await findAttachment(
274
+ req,
275
+ myAgentId,
276
+ parsed.params.taskId,
277
+ parsed.params.attachmentId,
278
+ res,
279
+ );
267
280
  if (!attachment) return true;
268
281
  return sendDownload(res, attachment);
269
282
  }
@@ -271,7 +284,13 @@ export async function handleFs(
271
284
  if (signedUrlTaskFileRoute.match(req.method, pathSegments)) {
272
285
  const parsed = await signedUrlTaskFileRoute.parse(req, res, pathSegments, queryParams);
273
286
  if (!parsed) return true;
274
- const attachment = await findAttachment(parsed.params.taskId, parsed.params.attachmentId, res);
287
+ const attachment = await findAttachment(
288
+ req,
289
+ myAgentId,
290
+ parsed.params.taskId,
291
+ parsed.params.attachmentId,
292
+ res,
293
+ );
275
294
  if (!attachment) return true;
276
295
  return sendSignedUrl(res, attachment, parsed.query.expiresIn);
277
296
  }
@@ -279,7 +298,13 @@ export async function handleFs(
279
298
  if (getTaskFileRoute.match(req.method, pathSegments)) {
280
299
  const parsed = await getTaskFileRoute.parse(req, res, pathSegments, queryParams);
281
300
  if (!parsed) return true;
282
- const attachment = await findAttachment(parsed.params.taskId, parsed.params.attachmentId, res);
301
+ const attachment = await findAttachment(
302
+ req,
303
+ myAgentId,
304
+ parsed.params.taskId,
305
+ parsed.params.attachmentId,
306
+ res,
307
+ );
283
308
  if (!attachment) return true;
284
309
  getTaskFileRoute.respond(res, 200, attachment);
285
310
  return true;
@@ -313,6 +338,10 @@ export async function handleFs(
313
338
  const parsed = await listTaskFilesRoute.parse(req, res, pathSegments, queryParams);
314
339
  if (!parsed) return true;
315
340
  if (!(await getTaskById(parsed.params.taskId))) return notFound(res, "Task not found");
341
+ if (!(await canReadSession(req, myAgentId, parsed.params.taskId))) {
342
+ jsonError(res, SESSION_READ_DENIED, 403);
343
+ return true;
344
+ }
316
345
  listTaskFilesRoute.respond(res, 200, {
317
346
  attachments: await getTaskAttachments(parsed.params.taskId),
318
347
  });
@@ -493,6 +522,8 @@ function backingProviderId(attachment: TaskAttachment): string | null {
493
522
  }
494
523
 
495
524
  async function findAttachment(
525
+ req: IncomingMessage,
526
+ myAgentId: string | undefined,
496
527
  taskId: string,
497
528
  attachmentId: string,
498
529
  res: ServerResponse,
@@ -501,6 +532,10 @@ async function findAttachment(
501
532
  notFound(res, "Task not found");
502
533
  return null;
503
534
  }
535
+ if (!(await canReadSession(req, myAgentId, taskId))) {
536
+ jsonError(res, SESSION_READ_DENIED, 403);
537
+ return null;
538
+ }
504
539
  const attachment = (await getTaskAttachments(taskId)).find((item) => item.id === attachmentId);
505
540
  if (!attachment) {
506
541
  notFound(res, "Attachment not found");
@@ -1,6 +1,6 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import { z } from "zod";
3
- import { listInboxState, upsertInboxState } from "../be/db";
3
+ import { getDbClient, listInboxState, upsertInboxState } from "../be/db";
4
4
  import { InboxItemStateSchema, InboxItemStatusSchema, InboxItemTypeSchema } from "../types";
5
5
  import { route } from "./route-def";
6
6
  import { jsonError } from "./utils";
@@ -29,29 +29,60 @@ const listState = route({
29
29
  auth: { apiKey: true },
30
30
  });
31
31
 
32
+ const inboxStateItemSchema = z.object({
33
+ itemType: InboxItemTypeSchema,
34
+ itemId: z.string().min(1),
35
+ status: InboxItemStatusSchema,
36
+ snoozeUntil: z.string().datetime().optional(),
37
+ readAt: z.string().datetime().optional(),
38
+ lastReadAt: z
39
+ .string()
40
+ .datetime()
41
+ .nullable()
42
+ .optional()
43
+ .describe(
44
+ "Omit to preserve the read cursor, send null to clear it, or send a timestamp to set it, including an earlier time.",
45
+ ),
46
+ });
47
+
32
48
  const upsertState = route({
33
49
  method: "patch",
34
50
  path: "/api/inbox-state",
35
51
  pattern: ["api", "inbox-state"],
36
52
  summary: "Upsert per-user dismiss/snooze/done state for an inbox item",
37
53
  tags: ["Inbox State"],
54
+ body: inboxStateItemSchema.extend({ userId: z.string() }),
55
+ responses: {
56
+ 200: {
57
+ description: "Upserted inbox state row",
58
+ schema: z.object({ item: InboxItemStateSchema }),
59
+ },
60
+ 400: { description: "Validation error" },
61
+ 401: { description: "Unauthorized" },
62
+ },
63
+ auth: { apiKey: true },
64
+ });
65
+
66
+ const bulkUpsertState = route({
67
+ method: "patch",
68
+ path: "/api/inbox-state/bulk",
69
+ pattern: ["api", "inbox-state", "bulk"],
70
+ summary: "Upsert inbox-item state in one transaction",
71
+ tags: ["Inbox State"],
38
72
  body: z.object({
39
73
  userId: z.string(),
40
- itemType: InboxItemTypeSchema,
41
- itemId: z.string().min(1),
42
- status: InboxItemStatusSchema,
43
- snoozeUntil: z.string().datetime().optional(),
44
- readAt: z.string().datetime().optional(),
74
+ items: z.array(inboxStateItemSchema).min(1).max(200),
45
75
  }),
46
76
  responses: {
47
77
  200: {
48
- description: "Upserted inbox state row",
49
- schema: z.object({ item: InboxItemStateSchema }),
78
+ description: "Upserted inbox state rows in request order",
79
+ schema: z.object({ items: z.array(InboxItemStateSchema) }),
50
80
  },
51
81
  400: { description: "Validation error" },
52
82
  401: { description: "Unauthorized" },
53
83
  },
54
84
  auth: { apiKey: true },
85
+ rbac: { ungated: "per-user inbox UI state, same posture as PATCH /api/inbox-state" },
55
86
  });
56
87
 
57
88
  // ─── Handler ─────────────────────────────────────────────────────────────────
@@ -74,6 +105,24 @@ export async function handleInboxState(
74
105
  return true;
75
106
  }
76
107
 
108
+ if (bulkUpsertState.match(req.method, pathSegments)) {
109
+ const parsed = await bulkUpsertState.parse(req, res, pathSegments, queryParams);
110
+ if (!parsed) return true;
111
+ try {
112
+ const items = await getDbClient().transaction(async () => {
113
+ const rows = [];
114
+ for (const item of parsed.body.items) {
115
+ rows.push(await upsertInboxState({ ...item, userId: parsed.body.userId }));
116
+ }
117
+ return rows;
118
+ });
119
+ bulkUpsertState.respond(res, 200, { items });
120
+ } catch (err) {
121
+ jsonError(res, err instanceof Error ? err.message : "Failed to upsert inbox state", 500);
122
+ }
123
+ return true;
124
+ }
125
+
77
126
  if (upsertState.match(req.method, pathSegments)) {
78
127
  const parsed = await upsertState.parse(req, res, pathSegments, queryParams);
79
128
  if (!parsed) return true;
@@ -85,6 +134,7 @@ export async function handleInboxState(
85
134
  status: parsed.body.status,
86
135
  snoozeUntil: parsed.body.snoozeUntil,
87
136
  readAt: parsed.body.readAt,
137
+ lastReadAt: parsed.body.lastReadAt,
88
138
  });
89
139
  upsertState.respond(res, 200, { item });
90
140
  } catch (err) {
package/src/http/index.ts CHANGED
@@ -116,6 +116,7 @@ import { handlePagesPublic } from "./pages-public";
116
116
  import { handlePoll } from "./poll";
117
117
  import { handlePricing } from "./pricing";
118
118
  import { handlePromptTemplates } from "./prompt-templates";
119
+ import { handleRanking } from "./ranking";
119
120
  import { handleRealtimeAsset, handleRealtimeTicket } from "./realtime";
120
121
  import { handleRepos } from "./repos";
121
122
  import { handleRooms } from "./rooms";
@@ -403,10 +404,11 @@ const httpServer = createHttpServer(async (req, res) => {
403
404
  () => handleExtensions(req, res, pathSegments, queryParams, myAgentId),
404
405
  () => handleFavorites(req, res, pathSegments, queryParams, myAgentId),
405
406
  () => handleUsers(req, res, pathSegments, queryParams),
406
- () => handleSessions(req, res, pathSegments, queryParams),
407
+ () => handleSessions(req, res, pathSegments, queryParams, myAgentId),
407
408
  () => handleInboxState(req, res, pathSegments, queryParams),
408
409
  () => handleTelemetryUi(req, res, pathSegments, queryParams, myAgentId),
409
410
  () => handleUsedBy(req, res, pathSegments, queryParams, myAgentId),
411
+ () => handleRanking(req, res, pathSegments, queryParams),
410
412
  () => handleFleetBenchmark(req, res, pathSegments, queryParams),
411
413
  () => handleTaskTemplates(req, res, pathSegments, queryParams),
412
414
  () => handleMcp(req, res, transports, transportActivity, mcpSessionAgents),
@@ -44,7 +44,7 @@ import { AgentMemorySchema, AgentMemoryScopeSchema, AgentMemorySourceSchema } fr
44
44
  import { getRequestAuth } from "../utils/request-auth-context";
45
45
  import { scrubSecrets } from "../utils/secret-scrubber";
46
46
  import { rejectGuest } from "./request-principal";
47
- import { route } from "./route-def";
47
+ import { bodyTask, route } from "./route-def";
48
48
  import { jsonError, parseQueryParams } from "./utils";
49
49
 
50
50
  // ─── Response Schemas ────────────────────────────────────────────────────────
@@ -68,6 +68,7 @@ const indexMemory = route({
68
68
  method: "post",
69
69
  path: "/api/memory/index",
70
70
  pattern: ["api", "memory", "index"],
71
+ session: bodyTask("sourceTaskId"),
71
72
  summary: "Ingest content into memory system (async embedding)",
72
73
  tags: ["Memory"],
73
74
  // The gate only bites when the key that lands on the rows (here, `sourcePath`) sits under a
@@ -687,6 +688,7 @@ const rateMemory = route({
687
688
  method: "post",
688
689
  path: "/api/memory/rate",
689
690
  pattern: ["api", "memory", "rate"],
691
+ session: { none: "worker-only: it needs X-Agent-ID, which a user token cannot send" },
690
692
  summary: "Submit RatingEvents to update memory usefulness posteriors",
691
693
  tags: ["Memory"],
692
694
  auth: { apiKey: true, agentId: true },