@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
@@ -0,0 +1,244 @@
1
+ import { getDbClient } from "./db";
2
+
3
+ /**
4
+ * Session sharing store. A session is a root task (`parentTaskId IS NULL`) and
5
+ * its descendants; its owner is the root's `requestedByUserId`.
6
+ *
7
+ * Rows in `session_shares` are never deleted. Revoking sets `revokedAt`, and an
8
+ * access change revokes the active row and inserts a new one in the same
9
+ * transaction, so the rows for a root are its full share history.
10
+ */
11
+
12
+ export type SessionShareAccess = "read" | "write";
13
+ export type SessionAccess = "owner" | SessionShareAccess | "none";
14
+ export type SessionEveryoneAccess = SessionShareAccess | "none";
15
+
16
+ export type SessionShareRow = {
17
+ id: string;
18
+ rootTaskId: string;
19
+ /** NULL = everyone. */
20
+ userId: string | null;
21
+ access: SessionShareAccess;
22
+ createdAt: string;
23
+ revokedAt: string | null;
24
+ revokedBy: string | null;
25
+ };
26
+
27
+ export type SessionRoot = { id: string; requestedByUserId: string | null };
28
+
29
+ export type SessionSharesState = {
30
+ everyone: SessionEveryoneAccess;
31
+ users: { userId: string; access: SessionShareAccess; createdAt: string }[];
32
+ };
33
+
34
+ export type DesiredSessionShares = {
35
+ everyone: SessionEveryoneAccess;
36
+ users: { userId: string; access: SessionShareAccess }[];
37
+ };
38
+
39
+ const RANK: Record<SessionAccess, number> = { none: 0, read: 1, write: 2, owner: 3 };
40
+
41
+ /** True when `access` grants at least `needed`. */
42
+ export function hasSessionAccess(access: SessionAccess, needed: SessionAccess): boolean {
43
+ return RANK[access] >= RANK[needed];
44
+ }
45
+
46
+ /**
47
+ * The session root of any task: walk `parentTaskId` up to the topmost task
48
+ * that still exists. No depth cap, so a chain of any length resolves to its
49
+ * actual root; a corrupt parent cycle has no top and resolves to null.
50
+ */
51
+ export async function getSessionRoot(taskId: string): Promise<SessionRoot | null> {
52
+ return (await getSessionRoots([taskId])).get(taskId) ?? null;
53
+ }
54
+
55
+ const ROOT_LOOKUP_CHUNK = 500;
56
+
57
+ type AncestorRow = { id: string; parentTaskId: string | null; requestedByUserId: string | null };
58
+
59
+ /** `getSessionRoot` for many tasks; unknown tasks and tasks in a cycle are absent. */
60
+ export async function getSessionRoots(taskIds: string[]): Promise<Map<string, SessionRoot>> {
61
+ const ids = [...new Set(taskIds)];
62
+ // Every ancestor once (`UNION` drops revisits, which also ends a cycle),
63
+ // then resolve each chain in memory: linear in the tasks involved.
64
+ const tasks = new Map<string, AncestorRow>();
65
+ for (let start = 0; start < ids.length; start += ROOT_LOOKUP_CHUNK) {
66
+ const chunk = ids.slice(start, start + ROOT_LOOKUP_CHUNK).filter((id) => !tasks.has(id));
67
+ if (chunk.length === 0) continue;
68
+ const rows = await getDbClient().query<AncestorRow>(
69
+ `WITH RECURSIVE up(id) AS (
70
+ SELECT id FROM agent_tasks WHERE id IN (${chunk.map(() => "?").join(", ")})
71
+ UNION
72
+ SELECT t.parentTaskId FROM agent_tasks t JOIN up ON t.id = up.id
73
+ WHERE t.parentTaskId IS NOT NULL
74
+ )
75
+ SELECT t.id, t.parentTaskId, t.requestedByUserId
76
+ FROM up CROSS JOIN agent_tasks t ON t.id = up.id`,
77
+ chunk,
78
+ );
79
+ for (const row of rows) tasks.set(row.id, row);
80
+ }
81
+
82
+ const resolved = new Map<string, SessionRoot | null>();
83
+ const rootOf = (taskId: string): SessionRoot | null => {
84
+ const path: string[] = [];
85
+ const onPath = new Set<string>();
86
+ let current = tasks.get(taskId);
87
+ let root: SessionRoot | null = null;
88
+ while (current) {
89
+ const known = resolved.get(current.id);
90
+ if (known !== undefined) {
91
+ root = known;
92
+ break;
93
+ }
94
+ if (onPath.has(current.id)) break;
95
+ path.push(current.id);
96
+ onPath.add(current.id);
97
+ const parent = current.parentTaskId ? tasks.get(current.parentTaskId) : undefined;
98
+ if (!parent) {
99
+ root = { id: current.id, requestedByUserId: current.requestedByUserId };
100
+ break;
101
+ }
102
+ current = parent;
103
+ }
104
+ for (const id of path) resolved.set(id, root);
105
+ return root;
106
+ };
107
+
108
+ const roots = new Map<string, SessionRoot>();
109
+ for (const id of ids) {
110
+ const root = rootOf(id);
111
+ if (root) roots.set(id, root);
112
+ }
113
+ return roots;
114
+ }
115
+
116
+ /**
117
+ * What `actingUserId` may do in the session rooted at `root`.
118
+ *
119
+ * - `owner` for the root's requester, and for every user when the root has no
120
+ * owner (Slack and schedule sessions stay open, as before sharing existed).
121
+ * - Otherwise the best active share naming the user or everyone; `none` when
122
+ * there is none. A root with no rows is private to its owner.
123
+ *
124
+ * A null `actingUserId` is a viewer with no user (a guest page session): it
125
+ * gets ownerless roots and everyone shares only. Callers that act for no user
126
+ * (agents, operator-key calls with no picked dashboard user) skip this check
127
+ * entirely; see `resolveSessionActor`.
128
+ */
129
+ export async function sessionAccessForRoot(
130
+ actingUserId: string | null,
131
+ root: SessionRoot,
132
+ ): Promise<SessionAccess> {
133
+ if (!root.requestedByUserId) return "owner";
134
+ if (actingUserId && root.requestedByUserId === actingUserId) return "owner";
135
+ const rows = await getDbClient().query<{ access: SessionShareAccess }>(
136
+ `SELECT access FROM session_shares
137
+ WHERE rootTaskId = ? AND revokedAt IS NULL AND (userId IS NULL OR userId = ?)`,
138
+ [root.id, actingUserId],
139
+ );
140
+ if (rows.some((row) => row.access === "write")) return "write";
141
+ return rows.length > 0 ? "read" : "none";
142
+ }
143
+
144
+ /** `sessionAccessForRoot` for any task in the session; `none` for an unknown task. */
145
+ export async function sessionAccess(
146
+ actingUserId: string | null,
147
+ taskId: string,
148
+ ): Promise<SessionAccess> {
149
+ const root = await getSessionRoot(taskId);
150
+ return root ? sessionAccessForRoot(actingUserId, root) : "none";
151
+ }
152
+
153
+ export async function listActiveSessionShares(rootTaskId: string): Promise<SessionShareRow[]> {
154
+ return getDbClient().query<SessionShareRow>(
155
+ `SELECT id, rootTaskId, userId, access, createdAt, revokedAt, revokedBy
156
+ FROM session_shares
157
+ WHERE rootTaskId = ? AND revokedAt IS NULL
158
+ ORDER BY createdAt, rowid`,
159
+ [rootTaskId],
160
+ );
161
+ }
162
+
163
+ /** Every row for a root, revoked ones included, oldest first. */
164
+ export async function listSessionShareHistory(rootTaskId: string): Promise<SessionShareRow[]> {
165
+ return getDbClient().query<SessionShareRow>(
166
+ `SELECT id, rootTaskId, userId, access, createdAt, revokedAt, revokedBy
167
+ FROM session_shares
168
+ WHERE rootTaskId = ?
169
+ ORDER BY createdAt, rowid`,
170
+ [rootTaskId],
171
+ );
172
+ }
173
+
174
+ export function toSessionSharesState(rows: SessionShareRow[]): SessionSharesState {
175
+ const everyone = rows.find((row) => row.userId === null);
176
+ return {
177
+ everyone: everyone?.access ?? "none",
178
+ users: rows
179
+ .filter((row) => row.userId !== null)
180
+ .map((row) => ({ userId: row.userId!, access: row.access, createdAt: row.createdAt })),
181
+ };
182
+ }
183
+
184
+ /**
185
+ * Make the active shares of `root` equal `desired`, in one write transaction.
186
+ * A target whose access changes has its active row revoked and a new row
187
+ * inserted; a target no longer listed is revoked; nothing is deleted or
188
+ * updated in place apart from revocation. The owner is never stored.
189
+ */
190
+ export async function setSessionShares(
191
+ root: SessionRoot,
192
+ desired: DesiredSessionShares,
193
+ actorUserId: string | null,
194
+ ): Promise<SessionSharesState> {
195
+ const want = new Map<string, SessionShareAccess>();
196
+ if (desired.everyone !== "none") want.set("*", desired.everyone);
197
+ for (const user of desired.users) {
198
+ if (user.userId !== root.requestedByUserId) want.set(user.userId, user.access);
199
+ }
200
+
201
+ return getDbClient().transaction(async (tx) => {
202
+ const now = new Date().toISOString();
203
+ const active = await tx.query<SessionShareRow>(
204
+ `SELECT id, rootTaskId, userId, access, createdAt, revokedAt, revokedBy
205
+ FROM session_shares WHERE rootTaskId = ? AND revokedAt IS NULL`,
206
+ [root.id],
207
+ );
208
+ const kept = new Set<string>();
209
+ for (const row of active) {
210
+ const target = row.userId ?? "*";
211
+ if (want.get(target) === row.access) {
212
+ kept.add(target);
213
+ continue;
214
+ }
215
+ await tx.run(
216
+ "UPDATE session_shares SET revokedAt = ?, revokedBy = ?, updated_by = ? WHERE id = ?",
217
+ [now, actorUserId, actorUserId, row.id],
218
+ );
219
+ }
220
+ for (const [target, access] of want) {
221
+ if (kept.has(target)) continue;
222
+ await tx.run(
223
+ `INSERT INTO session_shares (id, rootTaskId, userId, access, createdAt, created_by, updated_by)
224
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
225
+ [
226
+ crypto.randomUUID(),
227
+ root.id,
228
+ target === "*" ? null : target,
229
+ access,
230
+ now,
231
+ actorUserId,
232
+ actorUserId,
233
+ ],
234
+ );
235
+ }
236
+ const rows = await tx.query<SessionShareRow>(
237
+ `SELECT id, rootTaskId, userId, access, createdAt, revokedAt, revokedBy
238
+ FROM session_shares WHERE rootTaskId = ? AND revokedAt IS NULL
239
+ ORDER BY createdAt, rowid`,
240
+ [root.id],
241
+ );
242
+ return toSessionSharesState(rows);
243
+ });
244
+ }
@@ -1,4 +1,5 @@
1
1
  import { validateChatSdkAdaptersValue } from "../integrations/chat-sdk/schema";
2
+ import { parseTelemetryRanking, TELEMETRY_RANKING_KEY } from "../ranking/telemetry-ranking";
2
3
  import { validateQuickJSPoolSize } from "../scripts-runtime/executors/quickjs-pool-config";
3
4
  import { normalizeSlackReactionShortcode } from "../slack/reaction-shortcode";
4
5
  import { ProviderNameSchema } from "../types";
@@ -69,15 +70,24 @@ export function isApiOnlyConfigKey(key: string): boolean {
69
70
  }
70
71
 
71
72
  /** Config rows owned by dedicated API surfaces, not the generic config API. */
72
- export const INTERNAL_CONFIG_KEYS = new Set(["onboarding_state", "runtime_owner_id"]);
73
+ export const INTERNAL_CONFIG_KEYS = new Set([
74
+ "onboarding_state",
75
+ "runtime_owner_id",
76
+ TELEMETRY_RANKING_KEY,
77
+ ]);
73
78
 
74
79
  export function isInternalConfigKey(key: string): boolean {
75
80
  return INTERNAL_CONFIG_KEYS.has(key.toLowerCase());
76
81
  }
77
82
 
78
83
  export function internalConfigKeyError(key: string): Error {
84
+ const lower = key.toLowerCase();
79
85
  const owner =
80
- key.toLowerCase() === "runtime_owner_id" ? "the runtime supervisor" : "/api/onboarding";
86
+ lower === "runtime_owner_id"
87
+ ? "the runtime supervisor"
88
+ : lower === TELEMETRY_RANKING_KEY
89
+ ? "/api/ranking"
90
+ : "/api/onboarding";
81
91
  return new Error(`Key '${key}' is managed by ${owner}`);
82
92
  }
83
93
 
@@ -301,6 +311,16 @@ const VALIDATED_KEYS: Record<string, ConfigValidator> = {
301
311
  return "Invalid TASK_TOOL_MANIFESTS (expected JSON with taskTypes and/or schedules maps, each selecting at most 16 known swarm tools)";
302
312
  }
303
313
  },
314
+ // Owned by /api/ranking (an internal key); parsed here as defense in depth.
315
+ [TELEMETRY_RANKING_KEY.toUpperCase()]: (value) => {
316
+ try {
317
+ if (typeof value !== "string") throw new Error("Expected JSON string");
318
+ parseTelemetryRanking(value);
319
+ return null;
320
+ } catch {
321
+ return "Invalid telemetry_ranking (expected the v1 ranking listing JSON)";
322
+ }
323
+ },
304
324
  // MCP tool allowlist: comma-separated names or a JSON array of strings.
305
325
  // Unknown names are accepted here and warned about at session init.
306
326
  SWARM_ENABLED_TOOLS: (value) => {
@@ -405,6 +425,7 @@ const VALIDATED_KEYS: Record<string, ConfigValidator> = {
405
425
  },
406
426
  ...booleanValidators([
407
427
  "SWARM_USED_BY_CONSENT",
428
+ "PUBLIC_RANKING_ENABLED",
408
429
  "SWARM_DEV_MODE",
409
430
  "MULTI_RUNTIME_ENABLED",
410
431
  "STEERING_ENABLED",
@@ -55,6 +55,7 @@ import {
55
55
  skillFromToolArgs,
56
56
  } from "../providers/skill-invoke.ts";
57
57
  import type { SteerDeliveryResult } from "../providers/types.ts";
58
+ import { rankingUsageProps } from "../ranking/usage.ts";
58
59
  import { WorkerRealtimeClient } from "../realtime/worker-client";
59
60
  import { resumedTaskModelSelection, type TaskModelPinFields } from "../tasks/model-pin.ts";
60
61
  import { initTelemetry, telemetry } from "../telemetry.ts";
@@ -1228,6 +1229,10 @@ export const RELOADABLE_ENV_KEYS: ReadonlySet<string> = new Set([
1228
1229
  "TEMPLATE_REGISTRY_URL",
1229
1230
  "SLACK_DISABLE",
1230
1231
  "SWARM_ORG_NAME",
1232
+ // Gate the public-ranking `ranking.usage` emit, so clearing the website or
1233
+ // turning the setting off stops it on running workers.
1234
+ "SWARM_ORG_WEBSITE",
1235
+ "PUBLIC_RANKING_ENABLED",
1231
1236
  // pi reads these from process.env for its traits and its session.
1232
1237
  "PI_TOOL_DEFERRAL",
1233
1238
  "PI_CODEMODE",
@@ -4923,6 +4928,12 @@ async function spawnProviderProcess(
4923
4928
  },
4924
4929
  { userId: opts.requestedByUserId ?? null },
4925
4930
  );
4931
+ // Public ranking: token counts only, while the install is opted in.
4932
+ const rankingUsage = rankingUsageProps(result.cost, {
4933
+ model: result.cost.model,
4934
+ provider: result.cost.provider ?? opts.harnessProvider,
4935
+ });
4936
+ if (rankingUsage) telemetry.ranking("usage", rankingUsage);
4926
4937
  }
4927
4938
 
4928
4939
  // Post completion context usage snapshot
@@ -13,7 +13,8 @@ import {
13
13
  } from "../be/db";
14
14
  import { ActiveSessionSchema, AgentTaskSchema } from "../types";
15
15
  import { isMultiRuntimeEnabled } from "../utils/multi-runtime";
16
- import { route } from "./route-def";
16
+ import { bodyTask, route } from "./route-def";
17
+ import { readableByFor, resolveSessionActor } from "./session-access";
17
18
  import { jsonError } from "./utils";
18
19
 
19
20
  // ─── Route Definitions ───────────────────────────────────────────────────────
@@ -39,6 +40,7 @@ const createActiveSession = route({
39
40
  method: "post",
40
41
  path: "/api/active-sessions",
41
42
  pattern: ["api", "active-sessions"],
43
+ session: bodyTask("taskId"),
42
44
  summary: "Create a new active session",
43
45
  tags: ["Active Sessions"],
44
46
  body: z.object({
@@ -60,6 +62,7 @@ const deleteSessionByTask = route({
60
62
  method: "delete",
61
63
  path: "/api/active-sessions/by-task/{taskId}",
62
64
  pattern: ["api", "active-sessions", "by-task", null],
65
+ session: { taskParam: "taskId" },
63
66
  summary: "Delete active session by task ID",
64
67
  tags: ["Active Sessions"],
65
68
  params: z.object({ taskId: z.string() }),
@@ -72,6 +75,7 @@ const deleteSessionById = route({
72
75
  method: "delete",
73
76
  path: "/api/active-sessions/{id}",
74
77
  pattern: ["api", "active-sessions", null],
78
+ session: { activeSessionParam: "id" },
75
79
  summary: "Delete active session by ID",
76
80
  tags: ["Active Sessions"],
77
81
  params: z.object({ id: z.string() }),
@@ -84,6 +88,7 @@ const heartbeatSession = route({
84
88
  method: "put",
85
89
  path: "/api/active-sessions/heartbeat/{taskId}",
86
90
  pattern: ["api", "active-sessions", "heartbeat", null],
91
+ session: { taskParam: "taskId" },
87
92
  summary: "Update heartbeat for an active session",
88
93
  tags: ["Active Sessions"],
89
94
  params: z.object({ taskId: z.string() }),
@@ -96,6 +101,7 @@ const updateProviderSession = route({
96
101
  method: "put",
97
102
  path: "/api/active-sessions/provider-session/{taskId}",
98
103
  pattern: ["api", "active-sessions", "provider-session", null],
104
+ session: { taskParam: "taskId" },
99
105
  summary: "Update provider session ID on an active session",
100
106
  tags: ["Active Sessions"],
101
107
  params: z.object({ taskId: z.string() }),
@@ -109,6 +115,7 @@ const cleanupSessions = route({
109
115
  method: "post",
110
116
  path: "/api/active-sessions/cleanup",
111
117
  pattern: ["api", "active-sessions", "cleanup"],
118
+ session: { agentWide: "Deletes every session of an agent, or every stale session." },
112
119
  summary: "Clean up stale sessions",
113
120
  tags: ["Active Sessions"],
114
121
  body: z
@@ -126,6 +133,7 @@ const recoverOrphanedTasks = route({
126
133
  method: "post",
127
134
  path: "/api/active-sessions/recover-orphaned-tasks",
128
135
  pattern: ["api", "active-sessions", "recover-orphaned-tasks"],
136
+ session: { none: "Its handler admits only the calling agent, never a user." },
129
137
  summary: "Recover orphaned in-progress tasks for an agent",
130
138
  tags: ["Active Sessions"],
131
139
  body: z.object({
@@ -153,7 +161,10 @@ export async function handleActiveSessions(
153
161
  if (listActiveSessions.match(req.method, pathSegments)) {
154
162
  const parsed = await listActiveSessions.parse(req, res, pathSegments, queryParams);
155
163
  if (!parsed) return true;
156
- const sessions = await getActiveSessions(parsed.query.agentId || undefined);
164
+ const sessions = await getActiveSessions(
165
+ parsed.query.agentId || undefined,
166
+ readableByFor(await resolveSessionActor(req, myAgentId)),
167
+ );
157
168
  listActiveSessions.respond(res, 200, { sessions });
158
169
  return true;
159
170
  }
@@ -90,6 +90,7 @@ import { getRequestAuth } from "../utils/request-auth-context";
90
90
  import { isRuntimePoolsEnabled } from "../utils/runtime-pools-flag";
91
91
  import { scrubSecrets } from "../utils/secret-scrubber";
92
92
  import { route, runtimeInstanceHeader } from "./route-def";
93
+ import { readableByFor, resolveSessionActor } from "./session-access";
93
94
  import { agentWithCapacity, json, jsonError } from "./utils";
94
95
 
95
96
  function singleHeaderValue(req: IncomingMessage, name: string): string | undefined {
@@ -844,7 +845,7 @@ export async function handleAgentsRest(
844
845
  res: ServerResponse,
845
846
  pathSegments: string[],
846
847
  queryParams: URLSearchParams,
847
- _myAgentId: string | undefined,
848
+ myAgentId: string | undefined,
848
849
  ): Promise<boolean> {
849
850
  if (listAgents.match(req.method, pathSegments)) {
850
851
  const parsed = await listAgents.parse(req, res, pathSegments, queryParams);
@@ -853,7 +854,10 @@ export async function handleAgentsRest(
853
854
  // List responses default to slim (no identity markdown); `?fields=full` restores it.
854
855
  const slim = parsed.query.fields !== "full";
855
856
  const agents = includeTasks
856
- ? await getAllAgentsWithTasks({ slim })
857
+ ? await getAllAgentsWithTasks({
858
+ slim,
859
+ readableBy: readableByFor(await resolveSessionActor(req, myAgentId)),
860
+ })
857
861
  : await getAllAgents({ slim });
858
862
  const [agentsWithCapacity, transports] = await Promise.all([
859
863
  Promise.all(agents.map(agentWithCapacity)),
@@ -1537,7 +1541,10 @@ export async function handleAgentsRest(
1537
1541
  if (!parsed) return true;
1538
1542
  const includeTasks = parsed.query.include === "tasks";
1539
1543
  const agent = includeTasks
1540
- ? await getAgentWithTasks(parsed.params.id)
1544
+ ? await getAgentWithTasks(
1545
+ parsed.params.id,
1546
+ readableByFor(await resolveSessionActor(req, myAgentId)),
1547
+ )
1541
1548
  : await getAgentById(parsed.params.id);
1542
1549
 
1543
1550
  if (!agent) {
@@ -67,6 +67,7 @@ import "./task-templates";
67
67
  import "./telemetry-ui";
68
68
  import "./trackers/jira";
69
69
  import "./trackers/linear";
70
+ import "./ranking";
70
71
  import "./used-by";
71
72
  import "./fleet-benchmark";
72
73
  import "./users";
@@ -20,7 +20,7 @@ import {
20
20
  SUBSCRIPTION_PLANS,
21
21
  SUBSCRIPTION_PLANS_CHECKED_AT,
22
22
  } from "../utils/subscription-plans";
23
- import { route } from "./route-def";
23
+ import { bodyTask, route } from "./route-def";
24
24
  import { clearUsageCache } from "./usage-cache";
25
25
  import { jsonError } from "./utils";
26
26
 
@@ -36,6 +36,7 @@ const reportUsage = route({
36
36
  method: "post",
37
37
  path: "/api/keys/report-usage",
38
38
  pattern: ["api", "keys", "report-usage"],
39
+ session: bodyTask("taskId"),
39
40
  summary: "Record which API key was used for a task",
40
41
  tags: ["API Keys"],
41
42
  body: z.object({
@@ -82,6 +83,7 @@ const reportAuthFailure = route({
82
83
  method: "post",
83
84
  path: "/api/keys/report-auth-failure",
84
85
  pattern: ["api", "keys", "report-auth-failure"],
86
+ session: bodyTask("taskId"),
85
87
  summary: "Record an auth failure for a pooled key; bench it after 2 in a row",
86
88
  tags: ["API Keys"],
87
89
  body: z.object({
@@ -23,7 +23,7 @@ import { getRequestAuth } from "../utils/request-auth-context";
23
23
  import { cancelApprovalRequest } from "../workflows/approval-cancel";
24
24
  import { createApprovalFollowUpTask } from "../workflows/approval-notifications";
25
25
  import { workflowEventBus } from "../workflows/event-bus";
26
- import { route } from "./route-def";
26
+ import { bodyTask, route } from "./route-def";
27
27
  import { jsonError } from "./utils";
28
28
 
29
29
  // ─── Route Definitions ───────────────────────────────────────────────────────
@@ -255,6 +255,7 @@ const createRoute = route({
255
255
  method: "post",
256
256
  path: "/api/approval-requests",
257
257
  pattern: ["api", "approval-requests"],
258
+ session: bodyTask("sourceTaskId"),
258
259
  summary: "Create a new approval request",
259
260
  tags: ["ApprovalRequests"],
260
261
  body: z.object({
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,
@@ -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() }),