@desplega.ai/agent-swarm 2.3.0 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (332) hide show
  1. package/README.md +1 -0
  2. package/dist/{acp-adapter-v85a2bqy.js → acp-adapter-zvzr538d.js} +9 -9
  3. package/dist/{actions-zgcxfspj.js → actions-jhtqdw69.js} +21 -19
  4. package/dist/{amp-adapter-jw9xfz42.js → amp-adapter-bd84rhs2.js} +4 -4
  5. package/dist/{app-4e1s4675.js → app-kreq1gwg.js} +3 -3
  6. package/dist/{assistant-v238n9dk.js → assistant-3k22sbsq.js} +23 -21
  7. package/dist/{boot-reembed-p666cw5n.js → boot-reembed-e53x6b9v.js} +1 -1
  8. package/dist/{boot-reembed-w6pm64j4.js → boot-reembed-f6z923ny.js} +1 -1
  9. package/dist/{boot-scrub-sweep-jcyjagn3.js → boot-scrub-sweep-0538vsb7.js} +4 -4
  10. package/dist/{boot-seeds-gbedmamp.js → boot-seeds-6wxqkfk2.js} +4 -4
  11. package/dist/{claude-adapter-q7wspzt8.js → claude-adapter-gfts5044.js} +9 -9
  12. package/dist/{claude-managed-adapter-n7ahjyx6.js → claude-managed-adapter-h2y889ge.js} +3 -3
  13. package/dist/{claude-managed-setup-wza784fb.js → claude-managed-setup-h00hsgtz.js} +2 -2
  14. package/dist/{claude-sdk-session-yf400ptr.js → claude-sdk-session-56besmfz.js} +13 -13
  15. package/dist/{cli-65q70v2v.js → cli-0bh477zn.js} +33 -6
  16. package/dist/{cli-4ync7m60.js → cli-0ebey9hg.js} +1 -1
  17. package/dist/{cli-665j3feb.js → cli-0xk505e8.js} +1 -1
  18. package/dist/{cli-7gh7020n.js → cli-1dmcq2eh.js} +1 -1
  19. package/dist/{cli-gzvb3d4a.js → cli-1jx0srjt.js} +3 -3
  20. package/dist/{cli-zj4d6afs.js → cli-1pa3rz69.js} +21 -6
  21. package/dist/{cli-7ayprxp7.js → cli-2dfbv8qr.js} +8 -0
  22. package/dist/{cli-30m5q8mc.js → cli-3kh4kpqy.js} +6 -6
  23. package/dist/{cli-qrwp40st.js → cli-3p8fqcnh.js} +2 -2
  24. package/dist/{cli-y3dvz6ge.js → cli-3r448dqn.js} +3 -3
  25. package/dist/{cli-a17tzmn0.js → cli-3tnpj2bh.js} +7 -7
  26. package/dist/{cli-ea7chd26.js → cli-471qk6s2.js} +93 -46
  27. package/dist/{cli-a49vpbf3.js → cli-5kqnemen.js} +1 -1
  28. package/dist/{cli-0rh20bp3.js → cli-6ncjvx55.js} +3 -3
  29. package/dist/{cli-fb6t92v8.js → cli-6x9ma5w5.js} +1 -1
  30. package/dist/{cli-k28p6xh7.js → cli-7225gwj7.js} +2 -2
  31. package/dist/{cli-8cvvytad.js → cli-8nyhf7za.js} +3 -3
  32. package/dist/{cli-e09hj50q.js → cli-8wkb09q5.js} +11 -11
  33. package/dist/{cli-j0v75y13.js → cli-a0kcty51.js} +4 -4
  34. package/dist/{cli-zes4tsbb.js → cli-a4gvwttf.js} +4 -4
  35. package/dist/{cli-jhp7w1fy.js → cli-a5ebc95n.js} +2 -2
  36. package/dist/{cli-5d1m1qca.js → cli-ash6xxk7.js} +2 -2
  37. package/dist/{cli-fp4xbw07.js → cli-axh29tyr.js} +14 -12
  38. package/dist/{cli-p22zdvnt.js → cli-bn0rewsy.js} +3 -3
  39. package/dist/{cli-hz64bcnj.js → cli-bn5kevnf.js} +11 -373
  40. package/dist/{cli-8jtrekpy.js → cli-d2mb5s8a.js} +7 -7
  41. package/dist/{cli-c5sbmw1v.js → cli-d82btycs.js} +94 -15
  42. package/dist/{cli-vjfpshn3.js → cli-ejs3m1sx.js} +1 -1
  43. package/dist/{cli-yv0vgscc.js → cli-fpmsqq59.js} +18 -2
  44. package/dist/cli-fz9kewbh.js +382 -0
  45. package/dist/{cli-4s1kqrqg.js → cli-g0hgtz4p.js} +6 -6
  46. package/dist/{cli-hn3pftj2.js → cli-gf0kfg81.js} +7 -7
  47. package/dist/{cli-d43xqzbe.js → cli-hqhzg1qq.js} +2 -2
  48. package/dist/{cli-azs9bhc5.js → cli-j4rqdf2n.js} +2 -2
  49. package/dist/{cli-wtg6edvd.js → cli-jh0qcrga.js} +1 -1
  50. package/dist/{cli-kqypty34.js → cli-k9z4zd4p.js} +6 -5
  51. package/dist/{cli-jhmjwqcg.js → cli-kmqgmcnb.js} +47 -14
  52. package/dist/{cli-8qsx817b.js → cli-kr2pmfqx.js} +583 -769
  53. package/dist/{cli-4g629sgr.js → cli-m2qgh5sv.js} +20 -1
  54. package/dist/{cli-9e9kjqsj.js → cli-mfe7f7b5.js} +2 -2
  55. package/dist/{cli-x3en6ct3.js → cli-nwyw3zhg.js} +1 -1
  56. package/dist/{cli-6vcyw6v7.js → cli-nx462e4v.js} +5 -3
  57. package/dist/{cli-tr24eeny.js → cli-p0rakbrk.js} +9 -9
  58. package/dist/{cli-4hmaheb6.js → cli-p6argxh9.js} +9 -9
  59. package/dist/{cli-3avfh5az.js → cli-pemywhfq.js} +2 -2
  60. package/dist/{cli-etvn5ej2.js → cli-pnp2j5zg.js} +2 -2
  61. package/dist/{cli-wk8ccrz6.js → cli-pw7v69vp.js} +2 -2
  62. package/dist/{cli-ka01m5rb.js → cli-rdq7tzwm.js} +17 -14
  63. package/dist/{cli-re4wkbzx.js → cli-rqqsfz30.js} +677 -104
  64. package/dist/{cli-xgn4rwyv.js → cli-s48yvzb8.js} +4 -4
  65. package/dist/{cli-zv4vy2b0.js → cli-s95dknxh.js} +1 -1
  66. package/dist/{cli-g6p7c2my.js → cli-sjyetj21.js} +2 -2
  67. package/dist/{cli-rdr166ht.js → cli-smvcw9z5.js} +1 -1
  68. package/dist/{cli-fhvqt2fp.js → cli-sv5gb0yb.js} +1 -1
  69. package/dist/{cli-yv3n83x8.js → cli-syhq378v.js} +2 -2
  70. package/dist/{cli-q1812jbw.js → cli-tbhwrxpt.js} +9 -9
  71. package/dist/{cli-w7z94e6c.js → cli-tx00c0h6.js} +4 -2
  72. package/dist/{cli-dxwh4j32.js → cli-vy0fay6k.js} +46 -37
  73. package/dist/{cli-ptkpaqfy.js → cli-wt645v7q.js} +1 -1
  74. package/dist/{cli-ppapy34w.js → cli-wxbn3yfx.js} +2 -2
  75. package/dist/{cli-cby329ny.js → cli-x5mq1fyq.js} +3 -3
  76. package/dist/{cli-rc3r0bnh.js → cli-xbzvet96.js} +6 -6
  77. package/dist/{cli-4mkvwgzr.js → cli-xf234ws8.js} +1 -1
  78. package/dist/{cli-6p7ddg72.js → cli-xftrvxag.js} +5 -5
  79. package/dist/cli-z071fb95.js +848 -0
  80. package/dist/{cli-1v8w782f.js → cli-z1ew8vbq.js} +14 -7
  81. package/dist/{cli-9td5ss65.js → cli-zfg9enh9.js} +3 -3
  82. package/dist/{cli-yxqag76b.js → cli-zw88zjba.js} +1 -1
  83. package/dist/cli.js +19 -19
  84. package/dist/{codex-adapter-nhjj2veb.js → codex-adapter-p6rmm7vv.js} +6 -6
  85. package/dist/{codex-hook-x6tm6y5m.js → codex-hook-x7nejyrr.js} +8 -5
  86. package/dist/{codex-login-35p4sk85.js → codex-login-m819c41r.js} +1 -1
  87. package/dist/{codex-session-runner-6hggrjbx.js → codex-session-runner-8vd5t9t0.js} +7 -7
  88. package/dist/{commands-mrw1ph2w.js → commands-7ems2p13.js} +2 -2
  89. package/dist/{controller-pt1zam5w.js → controller-m263fg43.js} +4 -4
  90. package/dist/{controllers-connect-ymw5541d.js → controllers-connect-bg33ae53.js} +28 -24
  91. package/dist/{cursor-adapter-cf4zh5x0.js → cursor-adapter-cz6nm6mj.js} +3 -3
  92. package/dist/{db-hy80219q.js → db-w505dwa8.js} +2 -2
  93. package/dist/{db-hhpy6m4x.js → db-ys2rbdft.js} +3 -2
  94. package/dist/{devin-adapter-7egzmt0k.js → devin-adapter-zgqadvnm.js} +1 -1
  95. package/dist/{dispatcher-6pdjtkr1.js → dispatcher-0s0mckqr.js} +20 -18
  96. package/dist/{docker-njavd3fb.js → docker-fkgtr75t.js} +2 -2
  97. package/dist/{dsh-adapter-fbx06re2.js → dsh-adapter-3n3net33.js} +3 -3
  98. package/dist/{e2b-5r6a5jeb.js → e2b-7am6tmq6.js} +2 -2
  99. package/dist/{e2b-pw1nvtmh.js → e2b-thdcxtjd.js} +2 -2
  100. package/dist/{easter-egg-3d-6hg2r942.js → easter-egg-3d-5cgynaja.js} +1 -1
  101. package/dist/{event-bus-101fenkh.js → event-bus-9vpk8gmq.js} +1 -1
  102. package/dist/{execute-bcqc69a5.js → execute-t4e6m3rw.js} +1 -1
  103. package/dist/{external-channel-j6vb6ya1.js → external-channel-5b23fv8z.js} +2 -2
  104. package/dist/{google-generative-ai-phpmwxvg.js → google-generative-ai-bmq8gkf3.js} +1 -1
  105. package/dist/{google-vertex-es9ybgdy.js → google-vertex-b1rvp52s.js} +1 -1
  106. package/dist/{grok-adapter-1193sprp.js → grok-adapter-q9fghe5q.js} +10 -10
  107. package/dist/handlers-mqrym347.js +39 -0
  108. package/dist/{heartbeat-h1w1ktce.js → heartbeat-tww6yx1w.js} +23 -21
  109. package/dist/{hook-ejja4bpe.js → hook-dx0pkvar.js} +16 -9
  110. package/dist/{hook-skrbja18.js → hook-pvmh9b6m.js} +6 -6
  111. package/dist/{http-mzh41mfj.js → http-k2ejbt05.js} +975 -292
  112. package/dist/{index-6rmnn89d.js → index-2z710v4x.js} +10 -10
  113. package/dist/{index-xmmkyx82.js → index-dgz61nff.js} +20 -18
  114. package/dist/{index-wgsw9xcz.js → index-g6tbb6ve.js} +1 -1
  115. package/dist/index-h2ek08d6.js +40 -0
  116. package/dist/index-pqfwh9nn.js +41 -0
  117. package/dist/{index-epr2gnbw.js → index-x5aktmp8.js} +1 -1
  118. package/dist/{index-x0q2a4f9.js → index-ysd45kgc.js} +1 -1
  119. package/dist/{index-g909y089.js → index-zgvqzjsw.js} +8 -8
  120. package/dist/{keepalive-4djvbm2j.js → keepalive-nhvpw10z.js} +4 -4
  121. package/dist/lead-jb6sf69z.js +78 -0
  122. package/dist/{maintenance-f3xhnjsm.js → maintenance-agakc519.js} +1 -1
  123. package/dist/{oauth-refresh-sweep-fpd6x6rw.js → oauth-refresh-sweep-4dcbhtn0.js} +3 -3
  124. package/dist/{onboard-196tjgyn.js → onboard-4n2kwa4v.js} +3 -3
  125. package/dist/{opencode-adapter-eeesgfqk.js → opencode-adapter-3s7ax02e.js} +7 -7
  126. package/dist/{otel-impl-g8fh4c21.js → otel-impl-tfa12080.js} +2 -2
  127. package/dist/{pi-mono-adapter-zasp3ze3.js → pi-mono-adapter-ytp7gz8b.js} +24 -13
  128. package/dist/{pricing-refresh-sdcf5yaz.js → pricing-refresh-3cqx3f3x.js} +3 -3
  129. package/dist/{rbac-roles-bsp5657v.js → rbac-roles-9fae5rnv.js} +5 -3
  130. package/dist/{rbac-roles-2x4g1wb5.js → rbac-roles-y6zbfzve.js} +2 -2
  131. package/dist/{render-v2-0pyb39bb.js → render-v2-emawjvc4.js} +3 -3
  132. package/dist/{resume-a86tkj6r.js → resume-zhgts7h7.js} +10 -8
  133. package/dist/{runtime-ar8310ba.js → runtime-xe3aqy4x.js} +1 -1
  134. package/dist/session-access-cm823e06.js +25 -0
  135. package/dist/{setup-ypbzcapd.js → setup-tkp99w1e.js} +1 -1
  136. package/dist/{virtual-modules-wzk6jf3a.js → virtual-modules-4r3zkc20.js} +2 -2
  137. package/dist/{work-objects-yjp8de12.js → work-objects-f722h263.js} +23 -21
  138. package/dist/worker-p7f336rq.js +78 -0
  139. package/dist/{x-8ewt8gxn.js → x-y45mthwn.js} +2 -2
  140. package/openapi.json +1086 -305
  141. package/package.json +2 -2
  142. package/src/apps/action-caller.ts +6 -0
  143. package/src/apps/action-runner.ts +146 -0
  144. package/src/apps/row-store.ts +4 -2
  145. package/src/apps/script-types.ts +17 -4
  146. package/src/be/db/active-sessions.ts +5 -0
  147. package/src/be/db/session-visibility.ts +29 -0
  148. package/src/be/db/tasks/read.ts +61 -3
  149. package/src/be/db/tasks/write.ts +15 -6
  150. package/src/be/db.ts +106 -37
  151. package/src/be/events.ts +11 -0
  152. package/src/be/fleet-benchmark-stats.ts +10 -5
  153. package/src/be/migrations/230_session_shares.sql +23 -0
  154. package/src/be/models-catalog.ts +4 -0
  155. package/src/be/onboarding.ts +6 -0
  156. package/src/be/rbac-roles.ts +2 -0
  157. package/src/be/scripts/typecheck.ts +13 -0
  158. package/src/be/session-shares.ts +244 -0
  159. package/src/be/swarm-config-guard.ts +33 -3
  160. package/src/be/telemetry-enable.ts +1 -1
  161. package/src/commands/provider-credentials.ts +12 -2
  162. package/src/commands/runner.ts +14 -0
  163. package/src/heartbeat/heartbeat.ts +2 -2
  164. package/src/hooks/codex-hook.ts +6 -2
  165. package/src/hooks/hook.ts +7 -0
  166. package/src/hooks/pr-body-guard.ts +38 -11
  167. package/src/hooks/pr-room-guard.ts +88 -0
  168. package/src/http/active-sessions.ts +13 -2
  169. package/src/http/agents.ts +11 -3
  170. package/src/http/all-routes.ts +1 -0
  171. package/src/http/api-keys.ts +3 -1
  172. package/src/http/approval-requests.ts +2 -1
  173. package/src/http/apps.ts +118 -190
  174. package/src/http/config.ts +3 -0
  175. package/src/http/context.ts +2 -0
  176. package/src/http/core.ts +39 -2
  177. package/src/http/events.ts +6 -2
  178. package/src/http/favorite-owner.ts +1 -1
  179. package/src/http/fleet-benchmark.ts +79 -30
  180. package/src/http/fs.ts +38 -3
  181. package/src/http/global-config.ts +7 -0
  182. package/src/http/index.ts +3 -1
  183. package/src/http/memory.ts +3 -1
  184. package/src/http/ranking.ts +351 -0
  185. package/src/http/realtime.ts +26 -21
  186. package/src/http/route-def.ts +77 -11
  187. package/src/http/script-runs.ts +2 -1
  188. package/src/http/scripts.ts +8 -0
  189. package/src/http/session-access.ts +179 -0
  190. package/src/http/session-data.ts +10 -2
  191. package/src/http/sessions.ts +212 -12
  192. package/src/http/stats.ts +7 -3
  193. package/src/http/tasks.ts +85 -8
  194. package/src/http/telemetry-ui.ts +1 -1
  195. package/src/http/used-by.ts +64 -20
  196. package/src/http/users.ts +2 -2
  197. package/src/prompts/session-templates.ts +3 -1
  198. package/src/providers/amp-adapter.ts +1 -1
  199. package/src/providers/pi-mono-adapter.ts +15 -0
  200. package/src/providers/reasoning-effort.ts +2 -1
  201. package/src/providers/types.ts +2 -0
  202. package/src/ranking/telemetry-ranking.ts +119 -0
  203. package/src/ranking/usage.ts +57 -0
  204. package/src/rbac/admission.ts +22 -7
  205. package/src/rbac/legacy-policy.ts +2 -0
  206. package/src/rbac/permissions.ts +9 -0
  207. package/src/realtime/auth.ts +69 -0
  208. package/src/realtime/tickets.ts +4 -1
  209. package/src/realtime/transport.ts +77 -9
  210. package/src/scripts-runtime/sdk-allowlist.ts +7 -0
  211. package/src/scripts-runtime/types/stdlib.d.ts +31 -0
  212. package/src/scripts-runtime/types/swarm-sdk.d.ts +31 -0
  213. package/src/server.ts +4 -0
  214. package/src/slack/blocks.ts +18 -0
  215. package/src/slack/dm.ts +29 -0
  216. package/src/telemetry-context.ts +19 -1
  217. package/src/telemetry-contract/EVENTS.md +45 -8
  218. package/src/telemetry-contract/SOURCE.json +38 -4
  219. package/src/telemetry-contract/catalog.json +294 -5
  220. package/src/telemetry-contract/ranking/fixtures/expected.json +30 -0
  221. package/src/telemetry-contract/ranking/fixtures/invalid/bad-category.json +11 -0
  222. package/src/telemetry-contract/ranking/fixtures/invalid/bad-claim.json +11 -0
  223. package/src/telemetry-contract/ranking/fixtures/invalid/blank-description.json +11 -0
  224. package/src/telemetry-contract/ranking/fixtures/invalid/blank-name.json +11 -0
  225. package/src/telemetry-contract/ranking/fixtures/invalid/description-too-long.json +11 -0
  226. package/src/telemetry-contract/ranking/fixtures/invalid/domain-uppercase.json +11 -0
  227. package/src/telemetry-contract/ranking/fixtures/invalid/domain-with-scheme.json +11 -0
  228. package/src/telemetry-contract/ranking/fixtures/invalid/domain-with-www.json +11 -0
  229. package/src/telemetry-contract/ranking/fixtures/invalid/http-source-url.json +11 -0
  230. package/src/telemetry-contract/ranking/fixtures/invalid/missing-description.json +10 -0
  231. package/src/telemetry-contract/ranking/fixtures/invalid/missing-version.json +10 -0
  232. package/src/telemetry-contract/ranking/fixtures/invalid/name-not-string.json +11 -0
  233. package/src/telemetry-contract/ranking/fixtures/invalid/name-too-long.json +11 -0
  234. package/src/telemetry-contract/ranking/fixtures/invalid/not-object.json +1 -0
  235. package/src/telemetry-contract/ranking/fixtures/invalid/null.json +1 -0
  236. package/src/telemetry-contract/ranking/fixtures/invalid/other-with-subcategory.json +11 -0
  237. package/src/telemetry-contract/ranking/fixtures/invalid/oversize.json +1 -0
  238. package/src/telemetry-contract/ranking/fixtures/invalid/short-claim.json +11 -0
  239. package/src/telemetry-contract/ranking/fixtures/invalid/subcategory-other-category.json +11 -0
  240. package/src/telemetry-contract/ranking/fixtures/invalid/trailing-data.json +1 -0
  241. package/src/telemetry-contract/ranking/fixtures/invalid/unknown-field.json +12 -0
  242. package/src/telemetry-contract/ranking/fixtures/invalid/unsupported-version.json +11 -0
  243. package/src/telemetry-contract/ranking/fixtures/invalid/updated-at-not-date.json +11 -0
  244. package/src/telemetry-contract/ranking/fixtures/invalid/updated-at-offset.json +11 -0
  245. package/src/telemetry-contract/ranking/fixtures/invalid/wrong-case-field.json +11 -0
  246. package/src/telemetry-contract/ranking/fixtures/valid/category-without-subcategory.json +11 -0
  247. package/src/telemetry-contract/ranking/fixtures/valid/full.json +11 -0
  248. package/src/telemetry-contract/ranking/fixtures/valid/minimal.json +11 -0
  249. package/src/telemetry-contract/ranking/fixtures/valid/no-claim.json +10 -0
  250. package/src/telemetry-contract/ranking/fixtures/valid/padded-text.json +11 -0
  251. package/src/telemetry-contract/ranking/fixtures/valid/unicode-name.json +11 -0
  252. package/src/telemetry-contract/ranking/listing.v1.schema.json +61 -0
  253. package/src/telemetry-contract/ranking/taxonomy.json +8 -0
  254. package/src/telemetry-contract/ranking-taxonomy.gen.ts +11 -0
  255. package/src/telemetry-contract/types.gen.ts +963 -58
  256. package/src/telemetry-identity.ts +47 -9
  257. package/src/telemetry-snapshot.ts +9 -1
  258. package/src/telemetry-ui-events.ts +9 -2
  259. package/src/telemetry.ts +40 -5
  260. package/src/tests/app-action-run-tool.test.ts +178 -0
  261. package/src/tests/app-row-tools.test.ts +360 -0
  262. package/src/tests/apps-rbac.test.ts +189 -2
  263. package/src/tests/apps-script-types.test.ts +16 -3
  264. package/src/tests/credential-check.test.ts +38 -0
  265. package/src/tests/db-extraction-contract.test.ts +1 -0
  266. package/src/tests/db-task-read-contract.test.ts +1 -0
  267. package/src/tests/docs-index-footprint.test.js +2 -2
  268. package/src/tests/fleet-benchmark.test.ts +122 -2
  269. package/src/tests/http-users.test.ts +62 -1
  270. package/src/tests/model-catalog-table.test.ts +63 -0
  271. package/src/tests/model-profiles-api.test.ts +19 -7
  272. package/src/tests/models-catalog.test.ts +50 -0
  273. package/src/tests/onboarding.test.ts +21 -0
  274. package/src/tests/org-identity.test.ts +44 -0
  275. package/src/tests/org-website.test.ts +62 -0
  276. package/src/tests/pi-mono-adapter.test.ts +23 -0
  277. package/src/tests/pr-body-leaks.test.ts +4 -0
  278. package/src/tests/pr-room-guard.test.ts +242 -0
  279. package/src/tests/ranking-listing.test.ts +97 -0
  280. package/src/tests/ranking-route.test.ts +321 -0
  281. package/src/tests/ranking-usage.test.ts +50 -0
  282. package/src/tests/rbac-admission-e2e.test.ts +8 -0
  283. package/src/tests/rbac-admission.test.ts +38 -0
  284. package/src/tests/rbac-charact-http.test.ts +4 -3
  285. package/src/tests/rbac-engine.test.ts +2 -0
  286. package/src/tests/rbac-roles.test.ts +2 -0
  287. package/src/tests/realtime-room-surfaces.test.ts +3 -1
  288. package/src/tests/realtime-session-join-race.test.ts +142 -0
  289. package/src/tests/realtime-tickets.test.ts +24 -17
  290. package/src/tests/realtime-transport.test.ts +91 -0
  291. package/src/tests/reasoning-effort.test.ts +39 -3
  292. package/src/tests/runner-harness-telemetry-env.test.ts +56 -1
  293. package/src/tests/session-route-posture.test.ts +639 -0
  294. package/src/tests/session-sharing-routes.test.ts +282 -0
  295. package/src/tests/session-sharing.test.ts +526 -0
  296. package/src/tests/sessions-detail-batch.test.ts +7 -1
  297. package/src/tests/slack-tool-blocks.test.ts +101 -0
  298. package/src/tests/slack-update.test.ts +55 -0
  299. package/src/tests/slack-user-dm.test.ts +177 -0
  300. package/src/tests/swarm-tool-result-gate.test.ts +2 -4
  301. package/src/tests/telemetry-context.test.ts +128 -2
  302. package/src/tests/telemetry-ui-route.test.ts +123 -0
  303. package/src/tests/tool-annotations.test.ts +3 -2
  304. package/src/tests/used-by-route.test.ts +153 -9
  305. package/src/tests/workflow-agent-task-retry.test.ts +130 -4
  306. package/src/tests/workflow-wait-recovery.test.ts +116 -4
  307. package/src/tools/app-action-run.ts +63 -0
  308. package/src/tools/app-rows.ts +234 -0
  309. package/src/tools/model-catalog/index.ts +1 -1
  310. package/src/tools/slack-post.ts +18 -12
  311. package/src/tools/slack-read.ts +13 -1
  312. package/src/tools/slack-reply.ts +7 -10
  313. package/src/tools/slack-start-thread.ts +18 -11
  314. package/src/tools/slack-update.ts +2 -9
  315. package/src/tools/tool-config.ts +8 -0
  316. package/src/tools/utils.ts +1 -1
  317. package/src/tools/workflows/retry-workflow-run.ts +1 -1
  318. package/src/types.ts +52 -0
  319. package/src/utils/company-profile.ts +24 -0
  320. package/src/utils/org-identity.ts +33 -0
  321. package/src/utils/org-website.ts +58 -21
  322. package/src/workflows/recovery.ts +10 -22
  323. package/src/workflows/resume.ts +16 -0
  324. package/src/workflows/task-step-routing.ts +47 -7
  325. package/templates/skills/apps/SKILL.md +58 -2
  326. package/templates/skills/apps/content.md +58 -2
  327. package/dist/handlers-tb3xgjnm.js +0 -37
  328. package/dist/index-nyfcdpky.js +0 -39
  329. package/dist/index-vjn3wbzf.js +0 -38
  330. package/dist/lead-61kqp604.js +0 -76
  331. package/dist/worker-4ptnn1ge.js +0 -76
  332. package/dist/{cli-n5mrsg0f.js → cli-60cgh1rv.js} +3 -3
@@ -0,0 +1,360 @@
1
+ import { afterAll, beforeAll, expect, spyOn, test } from "bun:test";
2
+ import { unlink } from "node:fs/promises";
3
+ import { createServer as createHttpServer } from "node:http";
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import {
6
+ type AnySchema,
7
+ normalizeObjectSchema,
8
+ safeParseAsync,
9
+ } from "@modelcontextprotocol/sdk/server/zod-compat.js";
10
+ import * as rowStore from "../apps/row-store";
11
+ import { type AppRecord, createApp } from "../apps/store";
12
+ import { closeDb, createAgent, initDb } from "../be/db";
13
+ import { typecheckScript } from "../be/scripts/typecheck";
14
+ import { handleCore } from "../http/core";
15
+ import { handleMcpBridge } from "../http/mcp-bridge";
16
+ import { getPathSegments, parseQueryParams } from "../http/utils";
17
+ import { LEGACY_POLICY, type LegacyRule } from "../rbac";
18
+ import { runScript } from "../scripts-runtime/loader";
19
+ import { registerAppRowTools } from "../tools/app-rows";
20
+ import { markScriptSdkRequestOrigin } from "../tools/utils";
21
+ import { SKIP_SANDBOX_SPAWN_TESTS } from "./sandbox-spawn-test-helpers";
22
+ import { listenOnFreePort } from "./test-net";
23
+ import { CHILD_PROCESS_TEST_BUDGET_MS } from "./test-proc";
24
+
25
+ const dbPath = `/private/tmp/test-app-row-tools-${process.pid}.sqlite`;
26
+ const agentId = crypto.randomUUID();
27
+ const apiKey = "example-app-row-tools-key";
28
+ const previousKey = process.env.AGENT_SWARM_API_KEY;
29
+ const definition: AppRecord["definition"] = {
30
+ schemaVersion: 1,
31
+ models: {
32
+ note: {
33
+ columns: {
34
+ title: { kind: "string" },
35
+ rank: { kind: "number" },
36
+ active: { kind: "boolean" },
37
+ state: { kind: "enum", enum: ["open", "closed"] },
38
+ externalId: { kind: "string" },
39
+ projected: { kind: "string", source: { of: "external", field: "title" } },
40
+ },
41
+ sources: { external: { connector: "swarm-tasks", joinKey: "externalId", config: {} } },
42
+ },
43
+ },
44
+ pages: { main: { root: "root", elements: { root: { type: "Container", props: {} } } } },
45
+ defaultPage: "main",
46
+ };
47
+
48
+ type ToolResult = {
49
+ isError?: boolean;
50
+ structuredContent: {
51
+ message: string;
52
+ row: import("../apps/row-store").AppRow;
53
+ rows: import("../apps/row-store").AppRow[];
54
+ total: number;
55
+ deleted?: number;
56
+ ok?: boolean;
57
+ };
58
+ };
59
+ type RegisteredTool = {
60
+ inputSchema: AnySchema;
61
+ handler: (args: unknown, extra: unknown) => Promise<ToolResult>;
62
+ };
63
+ let tools: Record<string, RegisteredTool>;
64
+ let appId: string;
65
+
66
+ async function removeDb() {
67
+ for (const suffix of ["", "-wal", "-shm"]) await unlink(dbPath + suffix).catch(() => undefined);
68
+ }
69
+
70
+ beforeAll(async () => {
71
+ process.env.AGENT_SWARM_API_KEY = apiKey;
72
+ await removeDb();
73
+ initDb(dbPath);
74
+ await createAgent({ id: agentId, name: "row-tool-agent", isLead: false, status: "idle" });
75
+ appId = (await createApp({ name: "Row tools", definition })).id;
76
+ const server = new McpServer({ name: "app-row-tools", version: "1.0.0" });
77
+ registerAppRowTools(server);
78
+ tools = (server as unknown as { _registeredTools: Record<string, RegisteredTool> })
79
+ ._registeredTools;
80
+ });
81
+
82
+ afterAll(async () => {
83
+ closeDb();
84
+ await removeDb();
85
+ if (previousKey === undefined) delete process.env.AGENT_SWARM_API_KEY;
86
+ else process.env.AGENT_SWARM_API_KEY = previousKey;
87
+ });
88
+
89
+ async function call(
90
+ name: string,
91
+ input: Record<string, unknown>,
92
+ id: string | undefined = agentId,
93
+ ) {
94
+ const tool = tools[name]!;
95
+ const schema = normalizeObjectSchema(tool.inputSchema) ?? tool.inputSchema;
96
+ const args = { appId, model: "note", ...input };
97
+ const parsed = await safeParseAsync(schema, args);
98
+ if (!parsed.success) throw new Error(JSON.stringify(parsed.error));
99
+ // Exercise the registrar with the same trusted origin marker as the script bridge.
100
+ return tool.handler(
101
+ parsed.data,
102
+ markScriptSdkRequestOrigin({
103
+ sessionId: "app-row-tools",
104
+ requestInfo: { headers: id ? { "x-agent-id": id } : {} },
105
+ }),
106
+ );
107
+ }
108
+
109
+ test("creates, gets, filters, sorts, limits, patches, and deletes flat rows with agent provenance", async () => {
110
+ const result = await call("app-row-create", {
111
+ values: { title: "single", rank: 1, active: true },
112
+ });
113
+ expect(result.isError).not.toBe(true);
114
+ const row = result.structuredContent.row;
115
+ expect(row).toMatchObject({
116
+ title: "single",
117
+ rank: 1,
118
+ createdBy: `agent:${agentId}`,
119
+ updatedBy: `agent:${agentId}`,
120
+ });
121
+ expect(row.values).toBeUndefined();
122
+ expect((await call("app-row-get", { rowId: row.id })).structuredContent.row).toEqual(row);
123
+ const patched = await call("app-row-patch", { rowId: row.id, values: { title: "patched" } });
124
+ expect(patched.structuredContent.row).toMatchObject({
125
+ title: "patched",
126
+ createdBy: row.createdBy,
127
+ updatedBy: row.createdBy,
128
+ });
129
+ await call("app-rows-create", {
130
+ rows: [
131
+ { title: "bulk", rank: 2, active: true },
132
+ { title: "bulk", rank: 3, active: false },
133
+ { title: "bulk", rank: 4, active: true },
134
+ ],
135
+ });
136
+ const listed = await call("app-rows-list", {
137
+ equals: { title: "bulk", active: true },
138
+ sort: "rank:desc",
139
+ limit: 1,
140
+ });
141
+ expect(listed.structuredContent.total).toBe(2);
142
+ expect(listed.structuredContent.rows).toHaveLength(1);
143
+ expect(listed.structuredContent.rows[0].rank).toBe(4);
144
+ const systemFilter = await call("app-rows-list", {
145
+ equals: { id: row.id, createdBy: `agent:${agentId}` },
146
+ sort: "createdAt:asc",
147
+ });
148
+ expect(systemFilter.structuredContent.total).toBe(1);
149
+ expect((await call("app-row-delete", { rowId: row.id })).structuredContent).toMatchObject({
150
+ ok: true,
151
+ });
152
+ expect((await call("app-row-get", { rowId: row.id })).isError).toBe(true);
153
+ expect(
154
+ (await call("app-rows-delete", { equals: { title: "bulk" }, limit: 2 })).structuredContent,
155
+ ).toMatchObject({ deleted: 2 });
156
+ expect(
157
+ (await call("app-rows-delete", { equals: { title: "bulk" } })).structuredContent,
158
+ ).toMatchObject({
159
+ deleted: 1,
160
+ });
161
+ });
162
+
163
+ test("null in values clears an optional column on create, bulk create, and patch", async () => {
164
+ const created = await call("app-row-create", { values: { title: "null-create", rank: null } });
165
+ expect(created.isError).not.toBe(true);
166
+ expect(created.structuredContent.row.rank).toBeNull();
167
+ const bulk = await call("app-rows-create", {
168
+ rows: [{ title: "null-bulk", rank: null, state: null }],
169
+ });
170
+ expect(bulk.isError).not.toBe(true);
171
+ expect(bulk.structuredContent.rows[0]).toMatchObject({ rank: null, state: null });
172
+ const row = (await call("app-row-create", { values: { title: "null-patch", rank: 5 } }))
173
+ .structuredContent.row;
174
+ const patched = await call("app-row-patch", { rowId: row.id, values: { rank: null } });
175
+ expect(patched.isError).not.toBe(true);
176
+ expect(Object.hasOwn(patched.structuredContent.row, "rank")).toBe(false);
177
+ expect(
178
+ Object.hasOwn((await call("app-row-get", { rowId: row.id })).structuredContent.row, "rank"),
179
+ ).toBe(false);
180
+ });
181
+
182
+ test("app-rows-delete skips a row patched out of the filter after the select", async () => {
183
+ const row = (await call("app-row-create", { values: { title: "delete" } })).structuredContent.row;
184
+ const original = rowStore.listAppRows;
185
+ // The select sees the row as "delete"; the patch lands before the per-row delete lock.
186
+ const spy = spyOn(rowStore, "listAppRows").mockImplementation(async (...args) => {
187
+ const rows = await original(...args);
188
+ await rowStore.patchAppRow(appId, "note", definition.models.note!, row.id, { title: "keep" });
189
+ return rows;
190
+ });
191
+ let result: ToolResult;
192
+ try {
193
+ result = await call("app-rows-delete", { equals: { title: "delete" } });
194
+ } finally {
195
+ spy.mockRestore();
196
+ }
197
+ expect(result.isError).not.toBe(true);
198
+ expect(result.structuredContent.deleted).toBe(0);
199
+ expect((await call("app-row-get", { rowId: row.id })).structuredContent.row.title).toBe("keep");
200
+ await call("app-row-delete", { rowId: row.id });
201
+ });
202
+
203
+ test("rejects invalid filters, sorts, values, source projections, join keys, and missing agents", async () => {
204
+ for (const values of [
205
+ { projected: "blocked" },
206
+ { externalId: "blocked" },
207
+ { state: "invalid" },
208
+ { rank: "wrong" },
209
+ { createdBy: "forged" },
210
+ ]) {
211
+ expect((await call("app-row-create", { values })).isError).toBe(true);
212
+ }
213
+ for (const equals of [{ missing: "x" }, { active: 1 }, { rank: "1" }]) {
214
+ expect((await call("app-rows-list", { equals })).isError).toBe(true);
215
+ }
216
+ expect((await call("app-rows-list", { sort: "rank:sideways" })).isError).toBe(true);
217
+ expect((await call("app-rows-list", {}, "")).isError).toBe(true);
218
+ });
219
+
220
+ test("enforces row, bulk, filter, and limit caps before writing", async () => {
221
+ await expect(
222
+ call("app-rows-create", { rows: Array.from({ length: 501 }, () => ({ title: "too-many" })) }),
223
+ ).rejects.toThrow();
224
+ await expect(call("app-rows-create", { rows: [] })).rejects.toThrow();
225
+ await expect(call("app-rows-delete", {})).rejects.toThrow();
226
+ await expect(call("app-rows-delete", { equals: {} })).rejects.toThrow();
227
+ await expect(call("app-rows-list", { limit: 1001 })).rejects.toThrow();
228
+ const oversized = "x".repeat(1024 * 1024);
229
+ expect((await call("app-row-create", { values: { title: oversized } })).isError).toBe(true);
230
+ expect((await call("app-rows-create", { rows: [{ title: oversized }] })).isError).toBe(true);
231
+ expect(
232
+ (
233
+ await call("app-rows-create", {
234
+ rows: Array.from({ length: 20 }, () => ({ title: "x".repeat(600_000) })),
235
+ })
236
+ ).isError,
237
+ ).toBe(true);
238
+ const row = (await call("app-row-create", { values: { title: "cap-patch" } })).structuredContent
239
+ .row;
240
+ expect(
241
+ (await call("app-row-patch", { rowId: row.id, values: { title: oversized } })).isError,
242
+ ).toBe(true);
243
+ expect((await call("app-row-get", { rowId: row.id })).structuredContent.row.title).toBe(
244
+ "cap-patch",
245
+ );
246
+ expect(
247
+ (await call("app-rows-list", { equals: { title: "too-many" } })).structuredContent.total,
248
+ ).toBe(0);
249
+ });
250
+
251
+ test("denies a principal without app.use for every row operation", async () => {
252
+ const policy = LEGACY_POLICY as unknown as Record<"app.use", LegacyRule>;
253
+ const original = policy["app.use"];
254
+ policy["app.use"] = {
255
+ name: "deny-row-tools",
256
+ denyReason: "denied row tools",
257
+ evaluate: () => false,
258
+ };
259
+ try {
260
+ for (const [name, input] of [
261
+ ["app-rows-list", {}],
262
+ ["app-row-get", { rowId: "missing" }],
263
+ ["app-row-create", { values: { title: "denied" } }],
264
+ ["app-rows-create", { rows: [{ title: "denied" }] }],
265
+ ["app-row-patch", { rowId: "missing", values: { title: "denied" } }],
266
+ ["app-row-delete", { rowId: "missing" }],
267
+ ["app-rows-delete", { equals: { title: "denied" } }],
268
+ ] as const)
269
+ expect((await call(name, input)).isError).toBe(true);
270
+ } finally {
271
+ policy["app.use"] = original;
272
+ }
273
+ });
274
+
275
+ test("saved scripts can typecheck all seven row methods and cannot invoke actions", async () => {
276
+ const valid = await typecheckScript(`
277
+ import type { ScriptContext } from "swarm-sdk";
278
+ export default async function(args: unknown, ctx: ScriptContext) {
279
+ const base = { appId: "app", model: "note" };
280
+ await ctx.swarm.app_row_create({ ...base, values: { title: "test" } });
281
+ await ctx.swarm.app_rows_create({ ...base, rows: [{ title: "test" }] });
282
+ await ctx.swarm.app_rows_list({ ...base, equals: { title: "test" }, sort: "createdAt:desc", limit: 1 });
283
+ await ctx.swarm.app_row_get({ ...base, rowId: "row" });
284
+ await ctx.swarm.app_row_patch({ ...base, rowId: "row", values: { title: "changed", rank: null } });
285
+ await ctx.swarm.app_row_delete({ ...base, rowId: "row" });
286
+ await ctx.swarm.app_rows_delete({ ...base, equals: { title: "test" } });
287
+ }
288
+ `);
289
+ expect(valid).toEqual({ ok: true });
290
+ const invalid = await typecheckScript(`
291
+ import type { ScriptContext } from "swarm-sdk";
292
+ export default async function(args: unknown, ctx: ScriptContext) {
293
+ await ctx.swarm.app_action_run({ appId: "app", action: "write" });
294
+ }
295
+ `);
296
+ expect(invalid.ok).toBe(false);
297
+ if (!invalid.ok) expect(invalid.diagnostics.join("\n")).toContain("app_action_run");
298
+ });
299
+
300
+ test.skipIf(SKIP_SANDBOX_SPAWN_TESTS)(
301
+ "a saved script creates and lists rows through ctx.swarm",
302
+ async () => {
303
+ const server = createHttpServer(async (req, res) => {
304
+ const id = req.headers["x-agent-id"] as string | undefined;
305
+ if (await handleCore(req, res, id, apiKey)) return;
306
+ if (
307
+ await handleMcpBridge(
308
+ req,
309
+ res,
310
+ getPathSegments(req.url ?? ""),
311
+ parseQueryParams(req.url ?? ""),
312
+ id,
313
+ )
314
+ )
315
+ return;
316
+ res.writeHead(404).end();
317
+ });
318
+ try {
319
+ const port = await listenOnFreePort(server);
320
+ const script = await import("../be/scripts/db").then(({ upsertScriptByName }) =>
321
+ upsertScriptByName({
322
+ name: "row_tools_sdk",
323
+ scope: "agent",
324
+ scopeId: agentId,
325
+ agentId,
326
+ source: `export default async function(args, ctx) {
327
+ const created = await ctx.swarm.app_row_create({ appId: args.appId, model: "note", values: { title: "script-sdk", rank: 1 } });
328
+ const listed = await ctx.swarm.app_rows_list({ appId: args.appId, model: "note", equals: { title: "script-sdk" } });
329
+ const rowId = (created?.data ?? created).row.id;
330
+ const cleared = await ctx.swarm.app_row_patch({ appId: args.appId, model: "note", rowId, values: { rank: null } });
331
+ return { created: created?.data ?? created, listed: listed?.data ?? listed, cleared: cleared?.data ?? cleared };
332
+ }`,
333
+ description: "SDK row fixture",
334
+ intent: "Verify row tools through the script SDK",
335
+ signatureJson: "{}",
336
+ typeChecked: true,
337
+ embeddingMode: "skip",
338
+ }),
339
+ );
340
+ const output = await runScript({
341
+ source: script.script.source,
342
+ args: { appId },
343
+ agentId,
344
+ mcpBaseUrl: `http://localhost:${port}`,
345
+ });
346
+ expect(output.error).toBeUndefined();
347
+ expect(output.runtimeError).toBeUndefined();
348
+ expect(output.result).toMatchObject({
349
+ created: { row: { createdBy: `agent:${agentId}` } },
350
+ listed: { total: 1, rows: [{ title: "script-sdk" }] },
351
+ cleared: { row: { title: "script-sdk" } },
352
+ });
353
+ const cleared = (output.result as { cleared: { row: Record<string, unknown> } }).cleared.row;
354
+ expect(Object.hasOwn(cleared, "rank")).toBe(false);
355
+ } finally {
356
+ if (server.listening) await new Promise<void>((resolve) => server.close(() => resolve()));
357
+ }
358
+ },
359
+ CHILD_PROCESS_TEST_BUDGET_MS,
360
+ );
@@ -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