@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/telemetry.ts CHANGED
@@ -630,6 +630,7 @@ function emit(
630
630
  metadata?: Record<string, unknown>,
631
631
  actor?: TelemetryActor,
632
632
  emitter: string = source,
633
+ identity?: { orgName: string; orgWebsite: string },
633
634
  ): void {
634
635
  if (!isTelemetryEnabled() || !installationId) return;
635
636
  maybeRefreshOrgIdentity();
@@ -639,7 +640,9 @@ function emit(
639
640
  const occurredAt = new Date().toISOString();
640
641
  const sentFor = installationId;
641
642
  const isCloud = cachedIsCloud || isCloudDeployment();
642
- const orgIdentity = getOrgIdentity();
643
+ const orgIdentity = identity
644
+ ? { ...getOrgIdentity(), organization_name: identity.orgName }
645
+ : getOrgIdentity();
643
646
  const userId = actor?.userId?.trim() || undefined;
644
647
  const payload = {
645
648
  schema_version: 2 as const,
@@ -653,7 +656,7 @@ function emit(
653
656
  orgId,
654
657
  orgName: orgIdentity.organization_name,
655
658
  orgDomain: cachedOrgDomain,
656
- orgWebsite: normalizeOrgWebsite(process.env.SWARM_ORG_WEBSITE),
659
+ orgWebsite: identity?.orgWebsite ?? normalizeOrgWebsite(process.env.SWARM_ORG_WEBSITE),
657
660
  installationId,
658
661
  actor: userId ? { userId, role: actor?.role ?? roleResolver?.(userId) ?? null } : undefined,
659
662
  isCloud,
@@ -820,6 +823,27 @@ export const telemetry = {
820
823
  );
821
824
  },
822
825
 
826
+ /**
827
+ * Public-ranking events. The API passes the org identity it just read from
828
+ * config, so a listing built right after an identity edit still matches the
829
+ * envelope's `org_name` / `org_website` (the proxy rejects a mismatch) before
830
+ * the debounced config reload updates `process.env`.
831
+ */
832
+ ranking<S extends EventSuffix<"ranking">>(
833
+ event: S,
834
+ props: FamilyProps<"ranking", S>,
835
+ identity?: { orgName: string; orgWebsite: string },
836
+ ): void {
837
+ emit(
838
+ `ranking.${event}`,
839
+ props as Record<string, unknown>,
840
+ undefined,
841
+ undefined,
842
+ source,
843
+ identity,
844
+ );
845
+ },
846
+
823
847
  org<S extends EventSuffix<"org">>(event: S, props: FamilyProps<"org", S>): void {
824
848
  emit(`org.${event}`, props as Record<string, unknown>, undefined);
825
849
  },
@@ -7,9 +7,20 @@ import {
7
7
  type ServerResponse,
8
8
  } from "node:http";
9
9
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10
- import { closeDb, createAgent, createUser, getDbClient, getTaskById, initDb } from "../be/db";
10
+ import type { AppActionCaller } from "../apps/action-caller";
11
+ import {
12
+ closeDb,
13
+ createAgent,
14
+ createPage,
15
+ createUser,
16
+ getDbClient,
17
+ getTaskById,
18
+ initDb,
19
+ } from "../be/db";
20
+ import { createRuntimeController } from "../be/runtime-pools/db";
21
+ import { mintControllerToken } from "../be/runtime-pools/tokens";
11
22
  import { upsertScriptByName } from "../be/scripts/db";
12
- import { type IdentityActor, mintToken } from "../be/users";
23
+ import { type IdentityActor, mintRuntimeToken, mintToken } from "../be/users";
13
24
  import { handleApps } from "../http/apps";
14
25
  import { resolveHttpRequestAuth } from "../http/auth";
15
26
  import { getPathSegments, parseQueryParams } from "../http/utils";
@@ -19,13 +30,18 @@ import { registerAppDiffTool } from "../tools/app-diff";
19
30
  import { registerAppGetTool, registerAppQueryTool } from "../tools/app-get";
20
31
  import { registerAppHistoryTool } from "../tools/app-history";
21
32
  import { registerAppPatchTool } from "../tools/app-patch";
33
+ import { signPageSession } from "../utils/page-session";
22
34
  import { setRequestAuth } from "../utils/request-auth-context";
35
+ import { SKIP_SANDBOX_SPAWN_TESTS } from "./sandbox-spawn-test-helpers";
36
+ import { CHILD_PROCESS_TEST_BUDGET_MS } from "./test-proc";
23
37
 
24
38
  const TEST_DB_PATH = `/private/tmp/test-apps-rbac-${process.pid}.sqlite`;
25
39
  const API_KEY = "example-apps-rbac-test-key";
26
40
  const AGENT_ID = crypto.randomUUID();
27
41
  const LEAD_ID = crypto.randomUUID();
28
42
  const OPERATOR_ACTOR: IdentityActor = { kind: "operator", id: "apps-rbac-test" };
43
+ const previousPageSecret = process.env.PAGE_SESSION_SECRET;
44
+ const previousSwarmKey = process.env.AGENT_SWARM_API_KEY;
29
45
 
30
46
  const definition = {
31
47
  models: { note: { columns: { title: { kind: "string" } } } },
@@ -45,6 +61,7 @@ let server: Server;
45
61
  let base = "";
46
62
  let userId = "";
47
63
  let userToken = "";
64
+ let agentToken = "";
48
65
  let appId = "";
49
66
  let tools: Record<string, RegisteredTool>;
50
67
 
@@ -144,12 +161,15 @@ async function callTool(name: string, args: unknown): Promise<unknown> {
144
161
  }
145
162
 
146
163
  beforeAll(async () => {
164
+ process.env.PAGE_SESSION_SECRET = "example-apps-rbac-page-session-secret";
165
+ process.env.AGENT_SWARM_API_KEY = API_KEY;
147
166
  for (const suffix of ["", "-wal", "-shm"]) {
148
167
  await unlink(`${TEST_DB_PATH}${suffix}`).catch(() => undefined);
149
168
  }
150
169
  initDb(TEST_DB_PATH);
151
170
  await createAgent({ id: AGENT_ID, name: "apps-rbac-worker", isLead: false, status: "idle" });
152
171
  await createAgent({ id: LEAD_ID, name: "apps-rbac-lead", isLead: true, status: "idle" });
172
+ agentToken = (await mintRuntimeToken(AGENT_ID, crypto.randomUUID())).plaintext;
153
173
  const user = await createUser({ name: "Apps RBAC User" });
154
174
  userId = user.id;
155
175
  userToken = (await mintToken(user.id, "apps-rbac", OPERATOR_ACTOR)).plaintext;
@@ -177,6 +197,10 @@ afterEach(() => {
177
197
  afterAll(async () => {
178
198
  await new Promise<void>((resolve) => server.close(() => resolve()));
179
199
  closeDb();
200
+ if (previousPageSecret === undefined) delete process.env.PAGE_SESSION_SECRET;
201
+ else process.env.PAGE_SESSION_SECRET = previousPageSecret;
202
+ if (previousSwarmKey === undefined) delete process.env.AGENT_SWARM_API_KEY;
203
+ else process.env.AGENT_SWARM_API_KEY = previousSwarmKey;
180
204
  for (const suffix of ["", "-wal", "-shm"]) {
181
205
  await unlink(`${TEST_DB_PATH}${suffix}`).catch(() => undefined);
182
206
  }
@@ -323,6 +347,9 @@ describe("app viewer identity", () => {
323
347
  });
324
348
  expect(action.status).toBe(200);
325
349
  expect((await getTaskById(action.body.taskId))?.requestedByUserId).toBe(userId);
350
+ expect((await getTaskById(action.body.taskId))?.task).toContain(
351
+ `Caller (server-verified): ${JSON.stringify({ kind: "user", id: userId, name: "Apps RBAC User" })}`,
352
+ );
326
353
 
327
354
  const operatorAction = await request<{ taskId: string }>(
328
355
  `/api/apps/${appId}/actions/triage`,
@@ -342,6 +369,166 @@ describe("app viewer identity", () => {
342
369
  });
343
370
  });
344
371
 
372
+ describe("verified app action caller", () => {
373
+ beforeEach(async () => {
374
+ const saved = await upsertScriptByName({
375
+ name: `apps_caller_${crypto.randomUUID().replaceAll("-", "")}`,
376
+ scope: "agent",
377
+ scopeId: AGENT_ID,
378
+ agentId: AGENT_ID,
379
+ source: "export default function run(args: Record<string, unknown>) { return args; }",
380
+ description: "Action caller fixture",
381
+ intent: "Return the arguments received by a script action",
382
+ signatureJson: JSON.stringify({ args: { type: "object" }, result: { type: "object" } }),
383
+ typeChecked: true,
384
+ embeddingMode: "skip",
385
+ });
386
+ const patched = await request(`/api/apps/${appId}`, "operator", {
387
+ method: "PATCH",
388
+ body: JSON.stringify({
389
+ definition: {
390
+ actions: {
391
+ echo: {
392
+ kind: "script",
393
+ scriptId: saved.script.id,
394
+ args: { caller: { kind: "guest", id: "default-forgery" }, base: 2 },
395
+ },
396
+ },
397
+ },
398
+ }),
399
+ });
400
+ expect(patched.status).toBe(200);
401
+ });
402
+
403
+ const scriptTest = test.skipIf(SKIP_SANDBOX_SPAWN_TESTS);
404
+ scriptTest.each([
405
+ ["user", undefined],
406
+ ["operator", AGENT_ID],
407
+ ["operator", crypto.randomUUID()],
408
+ ["operator", undefined],
409
+ ["agent", AGENT_ID],
410
+ ] as const)(
411
+ "%s caller with agent header %s preserves identity and overwrites forged input",
412
+ async (principal, agentId) => {
413
+ const caller: AppActionCaller =
414
+ principal === "user"
415
+ ? { kind: "user", id: userId, name: "Apps RBAC User" }
416
+ : agentId === AGENT_ID
417
+ ? { kind: "agent", id: AGENT_ID, name: "apps-rbac-worker" }
418
+ : { kind: "operator", id: null };
419
+ const requestHeaders = {
420
+ ...(agentId ? { "X-Agent-ID": agentId } : {}),
421
+ ...(principal === "agent" ? { Authorization: `Bearer ${agentToken}` } : {}),
422
+ };
423
+ const checks: RbacCheck[] = [];
424
+ setAuditSink((check) => checks.push(check));
425
+ const row = await request<{ row: { createdBy: string } }>(
426
+ `/api/apps/${appId}/models/note/rows`,
427
+ principal,
428
+ {
429
+ method: "POST",
430
+ headers: requestHeaders,
431
+ body: JSON.stringify({ values: { title: "caller row" } }),
432
+ },
433
+ );
434
+ expect(row.status).toBe(201);
435
+ expect(row.body.row.createdBy).toBe(caller.id ? `${caller.kind}:${caller.id}` : "operator");
436
+
437
+ const action = await request<{
438
+ ok: boolean;
439
+ result: { caller: AppActionCaller; app: { id: string }; base: number; add: number };
440
+ }>(`/api/apps/${appId}/actions/echo`, principal, {
441
+ method: "POST",
442
+ headers: requestHeaders,
443
+ body: JSON.stringify({
444
+ input: { caller: { kind: "operator", id: "input-forgery" }, app: { id: "fake" }, add: 3 },
445
+ }),
446
+ });
447
+ expect(action.status).toBe(200);
448
+ expect(action.body.ok).toBe(true);
449
+ expect(action.body.result).toEqual({ caller, app: { id: appId }, base: 2, add: 3 });
450
+
451
+ const taskAction = await request<{ taskId: string }>(
452
+ `/api/apps/${appId}/actions/triage`,
453
+ principal,
454
+ { method: "POST", headers: requestHeaders, body: JSON.stringify({ input: {} }) },
455
+ );
456
+ expect(taskAction.status).toBe(200);
457
+ expect((await getTaskById(taskAction.body.taskId))?.task).toContain(
458
+ `Caller (server-verified): ${JSON.stringify(caller)}`,
459
+ );
460
+ expect(checks).toHaveLength(3);
461
+ expect(checks.every((check) => check.principal.kind === principal)).toBe(true);
462
+ },
463
+ CHILD_PROCESS_TEST_BUDGET_MS,
464
+ );
465
+
466
+ scriptTest(
467
+ "a signed page session includes the verified page id and viewer identity",
468
+ async () => {
469
+ const page = await createPage({
470
+ agentId: AGENT_ID,
471
+ slug: `caller-${crypto.randomUUID()}`,
472
+ title: "Caller page",
473
+ contentType: "text/html",
474
+ body: "<p>Caller page</p>",
475
+ });
476
+ const session = await signPageSession({
477
+ pageId: page.id,
478
+ exp: Math.floor(Date.now() / 1000) + 3600,
479
+ uid: userId,
480
+ });
481
+ const action = await request<{ ok: boolean; result: { caller: AppActionCaller } }>(
482
+ `/api/apps/${appId}/actions/echo`,
483
+ "operator",
484
+ {
485
+ method: "POST",
486
+ headers: {
487
+ "X-Agent-ID": AGENT_ID,
488
+ "X-Page-ID": page.id,
489
+ "X-Page-Session": session,
490
+ },
491
+ body: JSON.stringify({ input: { caller: { pageId: "fake" } } }),
492
+ },
493
+ );
494
+ expect(action.status).toBe(200);
495
+ expect(action.body.ok).toBe(true);
496
+ expect(action.body.result.caller).toEqual({
497
+ kind: "user",
498
+ id: userId,
499
+ name: "Apps RBAC User",
500
+ pageId: page.id,
501
+ });
502
+ },
503
+ CHILD_PROCESS_TEST_BUDGET_MS,
504
+ );
505
+
506
+ test("controller tokens use the guest principal even with a registered agent header", async () => {
507
+ const controller = await createRuntimeController({
508
+ name: "apps-caller-controller",
509
+ kind: "docker",
510
+ placement: "remote",
511
+ });
512
+ const token = await mintControllerToken(controller.id);
513
+ const checks: RbacCheck[] = [];
514
+ setAuditSink((check) => checks.push(check));
515
+ const action = await request(`/api/apps/${appId}/actions/echo`, "operator", {
516
+ method: "POST",
517
+ headers: { Authorization: `Bearer ${token.plaintext}`, "X-Agent-ID": AGENT_ID },
518
+ body: JSON.stringify({ input: {} }),
519
+ });
520
+ expect(action.status).toBe(403);
521
+ expect(checks).toEqual([
522
+ {
523
+ principal: { kind: "guest" },
524
+ verb: "app.use",
525
+ resource: { kind: "app", appId },
526
+ source: "http",
527
+ },
528
+ ]);
529
+ });
530
+ });
531
+
345
532
  describe("user writers and the script ownership gate", () => {
346
533
  async function saveScript(scope: "agent" | "global", ownerId: string | null): Promise<string> {
347
534
  const result = await upsertScriptByName({
@@ -127,9 +127,16 @@ describe("renderAppTypes", () => {
127
127
  expect(rendered).toContain("params: { value: never };");
128
128
  });
129
129
 
130
- test("renders a documentation-grade action union", () => {
130
+ test("renders action names and arguments with server-stamped caller context", () => {
131
131
  const rendered = renderAppTypes([app("PM Inbox", issueDefinition())]);
132
132
  expect(rendered).toContain('export type ActionName = "closeIssue" | "notifyOwner";');
133
+ expect(rendered).toContain("export interface ActionArgs");
134
+ expect(rendered).toContain('app: { id: "PM Inbox-id" };');
135
+ expect(rendered).toContain("caller: AppActionCaller;");
136
+ expect(rendered).toContain('kind: "user" | "agent" | "operator" | "guest";');
137
+ expect(rendered).toContain("id: string | null;");
138
+ expect(rendered).toContain("name?: string;");
139
+ expect(rendered).toContain("pageId?: string;");
133
140
  });
134
141
 
135
142
  test("derives and dedupes namespaces and model interface names", () => {
@@ -262,14 +269,20 @@ describe("renderAppTypes", () => {
262
269
  expect(rendered).not.toMatch(/^export const pwned2/m);
263
270
  });
264
271
 
265
- test("a model named actionName does not collide with the ActionName alias", () => {
272
+ test("model names do not collide with generated action types", () => {
266
273
  const rendered = renderAppTypes([
267
274
  app("Collision", {
268
- models: { actionName: { columns: { title: { kind: "string" } } } },
275
+ models: {
276
+ actionName: { columns: { title: { kind: "string" } } },
277
+ actionArgs: { columns: { title: { kind: "string" } } },
278
+ appActionCaller: { columns: { title: { kind: "string" } } },
279
+ },
269
280
  actions: { close: { kind: "task", prompt: "close" } },
270
281
  }),
271
282
  ]);
272
283
  expect(rendered).toContain("export interface ActionName_2");
284
+ expect(rendered).toContain("export interface ActionArgs_2");
285
+ expect(rendered).toContain("export interface AppActionCaller_2");
273
286
  expect(rendered).toContain('export type ActionName = "close";');
274
287
  });
275
288
 
@@ -11,8 +11,11 @@ import {
11
11
  createAgent,
12
12
  createTaskExtended,
13
13
  createUser,
14
+ getAssetKeyMappingByProvider,
14
15
  getDbClient,
16
+ getTaskById,
15
17
  initDb,
18
+ insertTaskAttachment,
16
19
  } from "../be/db";
17
20
  import { insertScript } from "../be/scripts/db";
18
21
  import { handleAssets } from "../http/assets";
@@ -343,4 +346,129 @@ describe("asset namespace REST contract", () => {
343
346
  expect(audit.body.structuralValid).toBe(true);
344
347
  expect(audit.body.warningCount).toBe(0);
345
348
  });
349
+
350
+ test("cascade moves descendants and sourced mappings without stamping task activity", async () => {
351
+ const root = await createTaskExtended("cascade root", { agentId, key: "shared/cascade/" });
352
+ const child = await createTaskExtended("cascade child", { agentId, parentTaskId: root.id });
353
+ const leaf = await createTaskExtended("cascade leaf", { agentId, parentTaskId: child.id });
354
+ const unchanged = await createTaskExtended("already moved", {
355
+ agentId,
356
+ parentTaskId: root.id,
357
+ key: "shared/cascade-target/",
358
+ });
359
+ const provider = { providerId: "agent-fs", providerKey: "cascade-file.md" };
360
+ await insertTaskAttachment({
361
+ taskId: leaf.id,
362
+ name: "cascade-file.md",
363
+ kind: "agent-fs",
364
+ ...provider,
365
+ });
366
+ const mapping = (await getAssetKeyMappingByProvider(provider))!;
367
+ const lastUpdatedAt = "2026-01-01T00:00:00.000Z";
368
+ for (const task of [root, child, leaf, unchanged]) {
369
+ await getDbClient().run("UPDATE agent_tasks SET lastUpdatedAt = ? WHERE id = ?", [
370
+ lastUpdatedAt,
371
+ task.id,
372
+ ]);
373
+ }
374
+ const move = await api(
375
+ "PATCH",
376
+ `/api/assets/task/${root.id}/key`,
377
+ {
378
+ key: "shared/cascade-target/",
379
+ cascade: true,
380
+ },
381
+ { agentId },
382
+ );
383
+ expect(move.status).toBe(200);
384
+ expect(move.body.movedTaskIds).toEqual([root.id, child.id, leaf.id]);
385
+ for (const task of [root, child, leaf, unchanged]) {
386
+ const moved = (await getTaskById(task.id))!;
387
+ expect(moved.key).toBe("shared/cascade-target/");
388
+ expect(moved.lastUpdatedAt).toBe(lastUpdatedAt);
389
+ }
390
+ expect((await getAssetKeyMappingByProvider(provider))?.key).toBe("shared/cascade-target/");
391
+ const history = await getDbClient().query<{ entity_id: string }>(
392
+ "SELECT entity_id FROM asset_key_history WHERE entity_id IN (?, ?, ?, ?, ?) AND new_key = ?",
393
+ [root.id, child.id, leaf.id, unchanged.id, mapping.id, "shared/cascade-target/"],
394
+ );
395
+ expect(history.map((row) => row.entity_id).sort()).toEqual(
396
+ [root.id, child.id, leaf.id, mapping.id].sort(),
397
+ );
398
+ expect(
399
+ (
400
+ await api(
401
+ "PATCH",
402
+ `/api/assets/task/${root.id}/key`,
403
+ {
404
+ key: "shared/cascade-target/",
405
+ cascade: true,
406
+ },
407
+ { agentId },
408
+ )
409
+ ).body.movedTaskIds,
410
+ ).toEqual([]);
411
+
412
+ // The root can already have the destination key while descendants still need a move.
413
+ await getDbClient().run('UPDATE agent_tasks SET "key" = ? WHERE id = ?', [
414
+ "shared/other-child/",
415
+ child.id,
416
+ ]);
417
+ expect(
418
+ (
419
+ await api(
420
+ "PATCH",
421
+ `/api/assets/task/${root.id}/key`,
422
+ {
423
+ key: "shared/cascade-target/",
424
+ cascade: true,
425
+ },
426
+ { agentId },
427
+ )
428
+ ).body.movedTaskIds,
429
+ ).toEqual([child.id]);
430
+ });
431
+
432
+ test("cascade denies the whole move when a descendant is not authorized", async () => {
433
+ const root = await createTaskExtended("allowed root", { agentId });
434
+ const child = await createTaskExtended("denied child", {
435
+ agentId: secondAgentId,
436
+ creatorAgentId: secondAgentId,
437
+ parentTaskId: root.id,
438
+ });
439
+ const move = await api(
440
+ "PATCH",
441
+ `/api/assets/task/${root.id}/key`,
442
+ {
443
+ key: "shared/must-not-move/",
444
+ cascade: true,
445
+ },
446
+ { agentId },
447
+ );
448
+ expect(move.status).toBe(403);
449
+ expect((await getTaskById(root.id))?.key).toBe(root.key);
450
+ expect((await getTaskById(child.id))?.key).toBe(child.key);
451
+ expect(
452
+ await getDbClient().query("SELECT id FROM asset_key_history WHERE entity_id IN (?, ?)", [
453
+ root.id,
454
+ child.id,
455
+ ]),
456
+ ).toEqual([]);
457
+ });
458
+
459
+ test("cascade is rejected for non-task entities", async () => {
460
+ for (const entityType of ["workflow", "schedule", "page", "file", "app", "script"]) {
461
+ const move = await api(
462
+ "PATCH",
463
+ `/api/assets/${entityType}/${crypto.randomUUID()}/key`,
464
+ {
465
+ key: "shared/invalid-cascade/",
466
+ cascade: true,
467
+ },
468
+ { operator: true },
469
+ );
470
+ expect(move.status).toBe(400);
471
+ expect(move.body.error).toContain("only supported for task");
472
+ }
473
+ });
346
474
  });