@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
package/src/http/tasks.ts CHANGED
@@ -37,6 +37,7 @@ import {
37
37
  } from "../be/db";
38
38
  import { modelProfileRefError, taskProfileError } from "../be/db/model-profiles";
39
39
  import { explicitModelErrorForAgent } from "../be/model-validation";
40
+ import { hasSessionAccess, type SessionAccess } from "../be/session-shares";
40
41
  import {
41
42
  getTaskSteeringFields,
42
43
  markSteeringUndeliverable,
@@ -72,6 +73,7 @@ import {
72
73
  SteeringSourceSchema,
73
74
  SteerModeSchema,
74
75
  SteerResultSchema,
76
+ SUPERSEDED_WORKFLOW_TASK_REASON,
75
77
  splitLegacyModelAlias,
76
78
  TaskAttachmentSchema,
77
79
  } from "../types";
@@ -80,7 +82,15 @@ import { getRequestAuth } from "../utils/request-auth-context";
80
82
  import { scrubSecrets } from "../utils/secret-scrubber";
81
83
  import { rejectGuest } from "./request-principal";
82
84
  import { buildTaskRequestedBy, TaskRequestedBySchema } from "./requested-by";
83
- import { route } from "./route-def";
85
+ import { bodyTask, route } from "./route-def";
86
+ import {
87
+ canReadSession,
88
+ readableByFor,
89
+ resolveSessionActor,
90
+ SESSION_READ_DENIED,
91
+ SESSION_WRITE_DENIED,
92
+ sessionAccessFor,
93
+ } from "./session-access";
84
94
  import { jsonError, parseBody } from "./utils";
85
95
 
86
96
  // ─── Response Schemas ────────────────────────────────────────────────────────
@@ -207,7 +217,7 @@ const listTasks = route({
207
217
  pattern: ["api", "tasks"],
208
218
  summary: "List tasks with filters",
209
219
  description:
210
- "Returns tasks with the full `task` text replaced by a bounded `taskPreview` and completion/integration blobs dropped by default — list views only need the preview. Pass `fields=full` to restore the full `AgentTask`, or `fields=timeline` for the narrow shape the dashboard timeline draws. Fetch a single task in full via `GET /api/tasks/{id}`. `total` (the filtered row count, ignoring limit/offset) is computed only with `includeTotal=true`.",
220
+ "Returns tasks with the full `task` text replaced by a bounded `taskPreview` and completion/integration blobs dropped by default — list views only need the preview. Pass `fields=full` to restore the full `AgentTask`, or `fields=timeline` for the narrow shape the dashboard timeline draws. Every shape gives a user caller only tasks of sessions they can read. Fetch a single task in full via `GET /api/tasks/{id}`. `total` (the filtered row count, ignoring limit/offset) is computed only with `includeTotal=true`.",
211
221
  tags: ["Tasks"],
212
222
  query: z.object({
213
223
  /** Single status, or comma-separated list (e.g. "failed,cancelled"). */
@@ -263,6 +273,7 @@ const createTask = route({
263
273
  method: "post",
264
274
  path: "/api/tasks",
265
275
  pattern: ["api", "tasks"],
276
+ session: bodyTask("parentTaskId"),
266
277
  summary: "Create a new task",
267
278
  tags: ["Tasks"],
268
279
  body: z
@@ -340,6 +351,7 @@ const promoteDraftTaskRoute = route({
340
351
  method: "post",
341
352
  path: "/api/tasks/{id}/promote-draft",
342
353
  pattern: ["api", "tasks", null, "promote-draft"],
354
+ session: { taskParam: "id" },
343
355
  summary: "Promote a draft task out of the pre-dispatch draft state",
344
356
  description:
345
357
  "Transitions a `draft` task (#1240 — created with attachments still uploading) to its normal dispatch-eligible status: `offered` if it was offered to an agent, `pending` if it has an owning agent (the common case — UI-composer tasks default to Lead), otherwise `unassigned`. Called by the UI composer once its attachment upload batch settles, whether every file uploaded, some failed, or all failed — a draft must never be stranded by an upload error. Idempotent: calling it on a task that already left `draft` returns the current task unchanged rather than erroring, so a retried request is safe.",
@@ -357,6 +369,7 @@ const updateSession = route({
357
369
  method: "put",
358
370
  path: "/api/tasks/{id}/session",
359
371
  pattern: ["api", "tasks", null, "session"],
372
+ session: { taskParam: "id" },
360
373
  summary: "Update provider session ID and harness metadata for a task",
361
374
  tags: ["Tasks"],
362
375
  params: z.object({ id: z.string() }),
@@ -392,6 +405,7 @@ const cancelTaskRoute = route({
392
405
  method: "post",
393
406
  path: "/api/tasks/{id}/cancel",
394
407
  pattern: ["api", "tasks", null, "cancel"],
408
+ session: { taskParam: "id" },
395
409
  summary: "Cancel a pending or in-progress task",
396
410
  tags: ["Tasks"],
397
411
  params: z.object({ id: z.string() }),
@@ -407,6 +421,7 @@ const steerTaskRoute = route({
407
421
  method: "post",
408
422
  path: "/api/tasks/{id}/steer",
409
423
  pattern: ["api", "tasks", null, "steer"],
424
+ session: { taskParam: "id" },
410
425
  summary: "Deliver a steering message to a running task",
411
426
  tags: ["Tasks"],
412
427
  params: z.object({ id: z.string() }),
@@ -439,6 +454,7 @@ const getTaskSteeringMessagesRoute = route({
439
454
  method: "get",
440
455
  path: "/api/tasks/{id}/steering-messages",
441
456
  pattern: ["api", "tasks", null, "steering-messages"],
457
+ session: { taskParam: "id" },
442
458
  summary: "List steering messages for a task",
443
459
  tags: ["Tasks"],
444
460
  params: z.object({ id: z.string() }),
@@ -467,6 +483,7 @@ const markSteeringDeliveredRoute = route({
467
483
  method: "post",
468
484
  path: "/api/steering-messages/{id}/delivered",
469
485
  pattern: ["api", "steering-messages", null, "delivered"],
486
+ session: { steeringParam: "id" },
470
487
  summary: "Mark a steering message delivered",
471
488
  tags: ["Tasks"],
472
489
  params: z.object({ id: z.string() }),
@@ -492,6 +509,7 @@ const markSteeringHandledRoute = route({
492
509
  method: "post",
493
510
  path: "/api/steering-messages/{id}/handled",
494
511
  pattern: ["api", "steering-messages", null, "handled"],
512
+ session: { steeringParam: "id" },
495
513
  summary: "Mark a steering message handled",
496
514
  description:
497
515
  "Optionally accepts a JSON body `{ note?: string }` — a short acceptance note describing how the steering was incorporated, persisted as `handledNote`.",
@@ -517,6 +535,7 @@ const markSteeringUndeliverableRoute = route({
517
535
  method: "post",
518
536
  path: "/api/steering-messages/{id}/undeliverable",
519
537
  pattern: ["api", "steering-messages", null, "undeliverable"],
538
+ session: { steeringParam: "id" },
520
539
  summary: "Promote an undeliverable steering message",
521
540
  tags: ["Tasks"],
522
541
  params: z.object({ id: z.string() }),
@@ -542,6 +561,7 @@ const getTask = route({
542
561
  method: "get",
543
562
  path: "/api/tasks/{id}",
544
563
  pattern: ["api", "tasks", null],
564
+ session: { taskParam: "id" },
545
565
  summary: "Get task details with logs and attachments",
546
566
  description:
547
567
  "Returns the full `AgentTask` row decorated with `logs` (capped by `logsLimit`) and `attachments` (pointer-based artifacts stored on the task, ordered by `created_at`).",
@@ -553,6 +573,7 @@ const getTask = route({
553
573
  }),
554
574
  responses: {
555
575
  200: { description: "Task with logs and attachments", schema: GetTaskResponseSchema },
576
+ 403: { description: "A user caller cannot read the task's session" },
556
577
  404: { description: "Task not found" },
557
578
  },
558
579
  });
@@ -561,6 +582,7 @@ const updateTaskProgressRoute = route({
561
582
  method: "post",
562
583
  path: "/api/tasks/{id}/progress",
563
584
  pattern: ["api", "tasks", null, "progress"],
585
+ session: { taskParam: "id" },
564
586
  summary: "Update task progress text",
565
587
  tags: ["Tasks"],
566
588
  params: z.object({ id: z.string() }),
@@ -579,6 +601,7 @@ const finishTask = route({
579
601
  method: "post",
580
602
  path: "/api/tasks/{id}/finish",
581
603
  pattern: ["api", "tasks", null, "finish"],
604
+ session: { taskParam: "id" },
582
605
  summary: "Mark task as completed or failed (runner endpoint)",
583
606
  tags: ["Tasks"],
584
607
  params: z.object({ id: z.string() }),
@@ -631,6 +654,7 @@ const pauseTaskRoute = route({
631
654
  method: "post",
632
655
  path: "/api/tasks/{id}/pause",
633
656
  pattern: ["api", "tasks", null, "pause"],
657
+ session: { taskParam: "id" },
634
658
  summary: "Pause an in-progress task",
635
659
  tags: ["Tasks"],
636
660
  params: z.object({ id: z.string() }),
@@ -646,6 +670,7 @@ const resumeTaskRoute = route({
646
670
  method: "post",
647
671
  path: "/api/tasks/{id}/resume",
648
672
  pattern: ["api", "tasks", null, "resume"],
673
+ session: { taskParam: "id" },
649
674
  summary: "Resume a paused task",
650
675
  tags: ["Tasks"],
651
676
  params: z.object({ id: z.string() }),
@@ -661,6 +686,7 @@ const supersedeTaskRoute = route({
661
686
  method: "post",
662
687
  path: "/api/tasks/{id}/supersede",
663
688
  pattern: ["api", "tasks", null, "supersede"],
689
+ session: { taskParam: "id" },
664
690
  summary: "Supersede an in-progress task (terminate + spawn resume follow-up)",
665
691
  description:
666
692
  'Marks the original task `superseded` (terminal) and creates a fresh `taskType="resume"` follow-up so a worker can pick up the work in a new provider session. Workflow-step tasks (those with `workflowRunStepId`) are carved out: the original is marked `failed` with reason `superseded_workflow_task` and no follow-up is created — the workflow engine\'s retry/failure policy applies.',
@@ -683,6 +709,7 @@ const updateTaskVcsRoute = route({
683
709
  method: "patch",
684
710
  path: "/api/tasks/{id}/vcs",
685
711
  pattern: ["api", "tasks", null, "vcs"],
712
+ session: { taskParam: "id" },
686
713
  summary: "Update VCS (PR/MR) info for a task",
687
714
  tags: ["Tasks"],
688
715
  params: z.object({ id: z.string() }),
@@ -704,6 +731,7 @@ const updateTaskTitleRoute = route({
704
731
  method: "patch",
705
732
  path: "/api/tasks/{id}/title",
706
733
  pattern: ["api", "tasks", null, "title"],
734
+ session: { taskParam: "id" },
707
735
  summary: "Set or clear a task's display title (session rename)",
708
736
  description:
709
737
  "Sets a human-facing display title override on a task. The sessions UI only reads this from root tasks (session list items), but titles on child tasks are harmless. Pass `title: null` (or an empty string) to clear the override and fall back to the task prompt.",
@@ -722,22 +750,37 @@ const updateTaskTitleRoute = route({
722
750
  },
723
751
  });
724
752
 
753
+ /** True unless the request acts for a user who lacks write access to the task's session. */
754
+ async function hasSessionWrite(
755
+ req: IncomingMessage,
756
+ myAgentId: string | undefined,
757
+ taskId: string,
758
+ ): Promise<boolean> {
759
+ const access = await sessionAccessFor(await resolveSessionActor(req, myAgentId), taskId);
760
+ return hasSessionAccess(access, "write");
761
+ }
762
+
725
763
  async function canSteerTask(
726
764
  req: IncomingMessage,
727
765
  myAgentId: string | undefined,
728
766
  task: AgentTask,
767
+ sessionAccess: SessionAccess = "none",
729
768
  ): Promise<boolean> {
769
+ const auth = getRequestAuth(req);
730
770
  const resource: RbacResource = {
731
771
  kind: "task",
732
772
  taskId: task.id,
733
- requestedByUserId: task.requestedByUserId,
773
+ // A user with a write share steers as if the session were their own; the
774
+ // verb gate (and any role grant) still applies. Only an actual share
775
+ // counts: `owner` from an ownerless root must not widen steering.
776
+ requestedByUserId:
777
+ auth?.kind === "user" && sessionAccess === "write" ? auth.userId : task.requestedByUserId,
734
778
  creatorAgentId: task.creatorAgentId,
735
779
  agentId: task.agentId,
736
780
  };
737
781
  let principal: RbacPrincipal;
738
782
  let verb: "task.steer.any" | "task.steer.own";
739
783
 
740
- const auth = getRequestAuth(req);
741
784
  if (auth?.kind === "operator") {
742
785
  principal = { kind: "operator" };
743
786
  verb = "task.steer.own";
@@ -906,6 +949,9 @@ export async function handleTasks(
906
949
  if (validated.length > 0) source = validated;
907
950
  }
908
951
 
952
+ // Every shape carries task text, so a user sees only sessions they can
953
+ // read, and `total` counts the same rows.
954
+ const actor = await resolveSessionActor(req, myAgentId);
909
955
  const filters = {
910
956
  status,
911
957
  agentId: parsed.query.agentId || undefined,
@@ -925,6 +971,7 @@ export async function handleTasks(
925
971
  orderBy: parsed.query.orderBy,
926
972
  limit: parsed.query.limit,
927
973
  offset: parsed.query.offset,
974
+ readableBy: actor.kind === "user" ? { userId: actor.userId } : undefined,
928
975
  };
929
976
  // List responses default to slim (full `task` text → bounded `taskPreview`,
930
977
  // heavy blobs dropped); `?fields=full` restores the full `AgentTask`.
@@ -1013,6 +1060,11 @@ export async function handleTasks(
1013
1060
  jsonError(res, `You don't have context for parent task ${parentTask.id}.`, 403);
1014
1061
  return true;
1015
1062
  }
1063
+ // A follow-up in someone else's session needs a write share.
1064
+ if (parentTask && !(await hasSessionWrite(req, myAgentId, parentTask.id))) {
1065
+ jsonError(res, SESSION_WRITE_DENIED, 403);
1066
+ return true;
1067
+ }
1016
1068
  // A chat-sdk contextKey binds the task to a chat thread that its assignee and creator
1017
1069
  // may then read and post to. An agent may only name a thread other than the parent's
1018
1070
  // with the lead's chat post permission.
@@ -1285,7 +1337,13 @@ export async function handleTasks(
1285
1337
  jsonError(res, "Task not found", 404);
1286
1338
  return true;
1287
1339
  }
1288
- if (!(await canSteerTask(req, myAgentId, task))) {
1340
+ const sessionActor = await resolveSessionActor(req, myAgentId);
1341
+ const steerAccess = await sessionAccessFor(sessionActor, task.id);
1342
+ if (!hasSessionAccess(steerAccess, "write")) {
1343
+ jsonError(res, SESSION_WRITE_DENIED, 403);
1344
+ return true;
1345
+ }
1346
+ if (!(await canSteerTask(req, myAgentId, task, steerAccess))) {
1289
1347
  jsonError(res, "Forbidden: caller cannot steer this task", 403);
1290
1348
  return true;
1291
1349
  }
@@ -1338,6 +1396,10 @@ export async function handleTasks(
1338
1396
  jsonError(res, "Task not found", 404);
1339
1397
  return true;
1340
1398
  }
1399
+ if (!(await canReadSession(req, myAgentId, parsed.params.id))) {
1400
+ jsonError(res, SESSION_READ_DENIED, 403);
1401
+ return true;
1402
+ }
1341
1403
  getTaskSteeringMessagesRoute.respond(res, 200, {
1342
1404
  messages: await getSteeringMessagesForTask(parsed.params.id),
1343
1405
  });
@@ -1357,7 +1419,12 @@ export async function handleTasks(
1357
1419
  return true;
1358
1420
  }
1359
1421
 
1422
+ // A user token naming an agent in X-Agent-ID still reads only its sessions.
1360
1423
  if (parsed.query.taskId) {
1424
+ if (!(await canReadSession(req, myAgentId, parsed.query.taskId))) {
1425
+ jsonError(res, SESSION_READ_DENIED, 403);
1426
+ return true;
1427
+ }
1361
1428
  const task = await getTaskById(parsed.query.taskId);
1362
1429
  const messages =
1363
1430
  task?.agentId === myAgentId ? await getPendingSteeringForTask(parsed.query.taskId) : [];
@@ -1365,8 +1432,9 @@ export async function handleTasks(
1365
1432
  return true;
1366
1433
  }
1367
1434
 
1435
+ const actor = await resolveSessionActor(req, myAgentId);
1368
1436
  getPendingSteeringMessagesRoute.respond(res, 200, {
1369
- messages: await getPendingSteeringForAgent(myAgentId),
1437
+ messages: await getPendingSteeringForAgent(myAgentId, readableByFor(actor)),
1370
1438
  });
1371
1439
  return true;
1372
1440
  }
@@ -1529,6 +1597,11 @@ export async function handleTasks(
1529
1597
  jsonError(res, "Task not found", 404);
1530
1598
  return true;
1531
1599
  }
1600
+ const readAccess = await sessionAccessFor(await resolveSessionActor(req, myAgentId), task.id);
1601
+ if (!hasSessionAccess(readAccess, "read")) {
1602
+ jsonError(res, "You don't have access to this session. Ask the owner to share it.", 403);
1603
+ return true;
1604
+ }
1532
1605
 
1533
1606
  const logs = await getLogsByTaskId(parsed.params.id, parsed.query.logsLimit ?? 200);
1534
1607
  const attachments = await getTaskAttachments(parsed.params.id);
@@ -1758,7 +1831,11 @@ export async function handleTasks(
1758
1831
  jsonError(res, "Missing X-Agent-ID header", 400);
1759
1832
  return true;
1760
1833
  }
1761
- const pausedTasks = await getPausedTasksForAgent(myAgentId);
1834
+ // A user token naming an agent in X-Agent-ID still reads only its sessions.
1835
+ const pausedTasks = await getPausedTasksForAgent(
1836
+ myAgentId,
1837
+ readableByFor(await resolveSessionActor(req, myAgentId)),
1838
+ );
1762
1839
  const tasks = await Promise.all(
1763
1840
  pausedTasks.map(async (task) => {
1764
1841
  const requestedBy = await buildTaskRequestedBy(task);
@@ -1938,7 +2015,7 @@ export async function handleTasks(
1938
2015
  // Check this BEFORE the supersede UPDATE so we don't leave a workflow
1939
2016
  // step in `superseded` if the engine expects `failed`.
1940
2017
  if (task.workflowRunStepId != null) {
1941
- const failed = await failTask(parsed.params.id, "superseded_workflow_task");
2018
+ const failed = await failTask(parsed.params.id, SUPERSEDED_WORKFLOW_TASK_REASON);
1942
2019
  ensure({
1943
2020
  id: "task.workflow_step_failed_on_supersede",
1944
2021
  flow: "task",
@@ -43,7 +43,7 @@ const enableTelemetry = route({
43
43
  description:
44
44
  "Stores `ANONYMIZED_TELEMETRY=true` and applies it to the running server: the install ID is minted if needed and `telemetry.enabled` is sent once, so no restart is needed.",
45
45
  tags: ["Telemetry"],
46
- body: z.object({ via: z.enum(["banner", "settings"]).default("banner") }),
46
+ body: z.object({ via: z.enum(["banner", "settings", "benchmark"]).default("banner") }),
47
47
  responses: {
48
48
  200: {
49
49
  description: "Telemetry is on",
@@ -1,11 +1,14 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import { z } from "zod";
3
- import { getSwarmConfigs, upsertSwarmConfig } from "../be/db";
3
+ import { getDbClient, upsertSwarmConfig } from "../be/db";
4
4
  import { isTelemetryEnabled, trackRelayed } from "../telemetry";
5
+ import { USER_FUNCTIONS } from "../utils/company-profile";
6
+ import { orgIdentityStatus } from "../utils/org-identity";
5
7
  import { normalizeOrgWebsite, TELEMETRY_PROXY_ORIGIN } from "../utils/org-website";
6
8
  import { ensureConfigAdmin } from "./config";
7
9
  import { scheduleIntegrationsReload } from "./core";
8
10
  import { resolveHttpFavoriteOwner } from "./favorite-owner";
11
+ import { globalConfig } from "./global-config";
9
12
  import { route } from "./route-def";
10
13
  import { jsonError } from "./utils";
11
14
 
@@ -15,6 +18,9 @@ const CONSENT_AT_KEY = "SWARM_USED_BY_CONSENT_AT";
15
18
  // still reaches the proxy after the name or website is changed or cleared.
16
19
  const GRANTED_NAME_KEY = "SWARM_USED_BY_ORG_NAME";
17
20
  const GRANTED_WEBSITE_KEY = "SWARM_USED_BY_ORG_WEBSITE";
21
+ const CONTACT_EMAIL_KEY = "SWARM_USED_BY_CONTACT_EMAIL";
22
+ const CONTACT_FUNCTION_KEY = "SWARM_USED_BY_CONTACT_FUNCTION";
23
+ const QUOTE_KEY = "SWARM_USED_BY_QUOTE";
18
24
  const TIMEOUT_MS = 10_000;
19
25
 
20
26
  // ─── Route Definitions ───────────────────────────────────────────────────────
@@ -25,17 +31,30 @@ const usedBy = route({
25
31
  pattern: ["api", "used-by"],
26
32
  summary: "Grant or revoke the 'used by' logo consent",
27
33
  description:
28
- "Records whether agent-swarm.dev may show this organization's logo as a company that uses Agent Swarm. Sends the organization name and website to the telemetry proxy, where a person approves each logo by hand. An explicit admin action, so it is not gated by the telemetry opt-out.",
34
+ "Records consent to show the organization's logo on agent-swarm.dev. A grant requires a valid contact email. It sends organization identity, contact details, and an optional quote to the proxy for manual review. This explicit admin action works when telemetry is off. A revoke clears the stored contact details and quote.",
29
35
  tags: ["Telemetry"],
30
- body: z.object({ granted: z.boolean() }),
36
+ body: z
37
+ .object({
38
+ granted: z.boolean(),
39
+ contactEmail: z
40
+ .string()
41
+ .max(320, "Contact email must be at most 320 characters")
42
+ .email("Contact email must be a valid email address")
43
+ .optional(),
44
+ contactFunction: z.enum(USER_FUNCTIONS).optional(),
45
+ quote: z.string().max(280).optional(),
46
+ })
47
+ .refine((body) => !body.granted || body.contactEmail !== undefined, {
48
+ path: ["contactEmail"],
49
+ message: "Contact email is required when granting logo consent",
50
+ }),
31
51
  responses: {
32
52
  200: {
33
53
  description: "Consent recorded",
34
54
  schema: z.object({ granted: z.boolean(), changedAt: z.string() }),
35
55
  },
36
56
  400: {
37
- description:
38
- "Organization name or website is not set (a revoke falls back to the identity of the last grant)",
57
+ description: "Invalid contact details, or missing organization name or website",
39
58
  },
40
59
  401: { description: "Unauthorized" },
41
60
  403: { description: "Only a config admin can change the consent" },
@@ -46,11 +65,6 @@ const usedBy = route({
46
65
 
47
66
  // ─── Helpers ─────────────────────────────────────────────────────────────────
48
67
 
49
- async function globalConfig(key: string): Promise<string | undefined> {
50
- const stored = (await getSwarmConfigs({ scope: "global", key }))?.[0]?.value?.trim();
51
- return stored || process.env[key]?.trim() || undefined;
52
- }
53
-
54
68
  /** A proxy path next to the configured `/v1/feedback` endpoint, so a stubbed proxy gets every route. */
55
69
  export async function telemetryProxyEndpoint(path: `/v1/${string}`): Promise<string> {
56
70
  const feedback = await globalConfig("feedback_endpoint");
@@ -78,9 +92,15 @@ export async function handleUsedBy(
78
92
  if (!parsed) return true;
79
93
  if (!(await ensureConfigAdmin(req, res, "config.write.any"))) return true;
80
94
 
81
- const { granted } = parsed.body;
82
- const currentName = await globalConfig("SWARM_ORG_NAME");
83
- const currentWebsite = normalizeOrgWebsite(await globalConfig("SWARM_ORG_WEBSITE"));
95
+ const { granted, contactEmail, contactFunction, quote } = parsed.body;
96
+ // The shared identity reader. The grant keeps its own rule (any non-empty
97
+ // name), so it does not apply the ranking's placeholder-name check.
98
+ const current = orgIdentityStatus({
99
+ SWARM_ORG_NAME: await globalConfig("SWARM_ORG_NAME"),
100
+ SWARM_ORG_WEBSITE: await globalConfig("SWARM_ORG_WEBSITE"),
101
+ });
102
+ const currentName = current.name ?? undefined;
103
+ const currentWebsite = current.website;
84
104
  const grantedName = granted ? undefined : await globalConfig(GRANTED_NAME_KEY);
85
105
  const grantedWebsite = granted
86
106
  ? null
@@ -108,6 +128,9 @@ export async function handleUsedBy(
108
128
  org_website: orgWebsite,
109
129
  ...(orgDomain ? { org_domain: orgDomain } : {}),
110
130
  granted,
131
+ ...(granted
132
+ ? { contact_email: contactEmail, contact_function: contactFunction, quote }
133
+ : {}),
111
134
  changed_at: changedAt,
112
135
  }),
113
136
  signal: AbortSignal.timeout(TIMEOUT_MS),
@@ -121,18 +144,39 @@ export async function handleUsedBy(
121
144
  return true;
122
145
  }
123
146
 
124
- await upsertSwarmConfig({ scope: "global", key: CONSENT_KEY, value: String(granted) });
125
- await upsertSwarmConfig({ scope: "global", key: CONSENT_AT_KEY, value: changedAt });
126
- if (granted) {
127
- await upsertSwarmConfig({ scope: "global", key: GRANTED_NAME_KEY, value: orgName });
128
- await upsertSwarmConfig({ scope: "global", key: GRANTED_WEBSITE_KEY, value: orgWebsite });
129
- }
147
+ await getDbClient().transaction(async () => {
148
+ await upsertSwarmConfig({ scope: "global", key: CONSENT_KEY, value: String(granted) });
149
+ await upsertSwarmConfig({ scope: "global", key: CONSENT_AT_KEY, value: changedAt });
150
+ if (granted) {
151
+ await upsertSwarmConfig({ scope: "global", key: GRANTED_NAME_KEY, value: orgName });
152
+ await upsertSwarmConfig({ scope: "global", key: GRANTED_WEBSITE_KEY, value: orgWebsite });
153
+ }
154
+ // Contact details stay out of agent env. The email is also a secret row,
155
+ // so `GET /api/config` masks it for every reader.
156
+ for (const [key, value, isSecret] of [
157
+ [CONTACT_EMAIL_KEY, contactEmail, true],
158
+ [CONTACT_FUNCTION_KEY, contactFunction, false],
159
+ [QUOTE_KEY, quote, false],
160
+ ] as const) {
161
+ await upsertSwarmConfig({
162
+ scope: "global",
163
+ key,
164
+ value: granted ? (value ?? "") : "",
165
+ isSecret,
166
+ exportToEnv: false,
167
+ });
168
+ }
169
+ });
130
170
  scheduleIntegrationsReload();
131
171
 
132
172
  const owner = await resolveHttpFavoriteOwner(req, myAgentId);
133
173
  trackRelayed(
134
174
  "used_by.consent_changed",
135
- { granted },
175
+ {
176
+ granted,
177
+ has_quote: granted && Boolean(quote),
178
+ has_function: granted && Boolean(contactFunction),
179
+ },
136
180
  { actor: owner?.userId ? { userId: owner.userId } : undefined, emitter: "ui" },
137
181
  );
138
182
  usedBy.respond(res, 200, { granted, changedAt });
package/src/http/users.ts CHANGED
@@ -205,7 +205,7 @@ const createUserRoute = route({
205
205
  tags: ["Users"],
206
206
  body: z.object({
207
207
  name: z.string().min(1),
208
- email: z.string().optional(),
208
+ email: z.string().email("Email must be a valid email address").optional(),
209
209
  role: z.string().optional(),
210
210
  notes: z.string().optional(),
211
211
  emailAliases: z.array(z.string()).optional(),
@@ -307,7 +307,7 @@ const updateUserRoute = route({
307
307
  body: z
308
308
  .object({
309
309
  name: z.string().min(1).optional(),
310
- email: z.string().optional(),
310
+ email: z.string().email("Email must be a valid email address").nullable().optional(),
311
311
  role: z.string().optional(),
312
312
  notes: z.string().optional(),
313
313
  emailAliases: z.array(z.string()).optional(),
@@ -555,12 +555,14 @@ registerTemplate({
555
555
  header: "",
556
556
  defaultBody: `{{prompt}}
557
557
 
558
- [App action] app={{app_id}} action={{action_name}} input={{input_json}}`,
558
+ [App action] app={{app_id}} action={{action_name}} input={{input_json}}
559
+ Caller (server-verified): {{caller_json}}`,
559
560
  variables: [
560
561
  { name: "prompt", description: "The task prompt configured on the app action" },
561
562
  { name: "app_id", description: "The app identifier" },
562
563
  { name: "action_name", description: "The invoked action name" },
563
564
  { name: "input_json", description: "The action input serialized as JSON" },
565
+ { name: "caller_json", description: "The server-verified action caller serialized as JSON" },
564
566
  ],
565
567
  category: "task_lifecycle",
566
568
  });
@@ -44,7 +44,7 @@ import type {
44
44
  * default; the image installs exactly this version and the per-task settings
45
45
  * set `amp.updates.mode: "disabled"`. Bump it deliberately, with a live run.
46
46
  */
47
- export const AMP_PACKAGE = "@ampcode/cli@0.0.1792296040-g5ce754";
47
+ export const AMP_PACKAGE = "@ampcode/cli@0.0.1792368068-g91990d";
48
48
 
49
49
  /** MCP server name. Amp exposes its tools as `agent_swarm` / `store_progress` (dashes become underscores). */
50
50
  const AMP_MCP_SERVER_NAME = "agent-swarm";
@@ -115,6 +115,7 @@ function modelToCredKeys(modelStr: string | undefined): string[] | null {
115
115
  if (provider === "openrouter") return ["OPENROUTER_API_KEY"];
116
116
  if (provider === "openai") return ["OPENAI_API_KEY"];
117
117
  if (provider === "google") return ["GOOGLE_API_KEY", "GEMINI_API_KEY"];
118
+ if (provider === "opencode-go") return ["OPENCODE_API_KEY"];
118
119
  // models.json custom providers (`writeSessionModelsJson`). The
119
120
  // runner normalizes key aliases (HELMCODE_API_NAN → NAN_API_KEY), but a
120
121
  // worker without that step (or a boot-time check) may only see the alias.
@@ -145,6 +146,19 @@ function getHarnessDrivableBedrockModels(): Array<{ id: string; name: string }>
145
146
  }
146
147
  }
147
148
 
149
+ /**
150
+ * The `opencode-go` model ids this pi-ai build resolves. models.dev can list
151
+ * ids pi-ai does not know yet, and pi runs its default model for those, so the
152
+ * worker reports this list and the picker offers only these ids.
153
+ */
154
+ export function listPiOpencodeGoModelIds(): string[] {
155
+ try {
156
+ return getModels("opencode-go").map((m) => m.id);
157
+ } catch {
158
+ return [];
159
+ }
160
+ }
161
+
148
162
  /**
149
163
  * Enumerate the Bedrock models that are both invocable by this AWS account and
150
164
  * drivable by the pi-ai Converse harness, and verify the credential chain in
@@ -480,6 +494,7 @@ const PI_RUNTIME_API_KEYS: readonly (readonly [envKey: string, provider: string]
480
494
  ["OPENAI_API_KEY", "openai"],
481
495
  ["GEMINI_API_KEY", "google"],
482
496
  ["GOOGLE_API_KEY", "google"],
497
+ ["OPENCODE_API_KEY", "opencode-go"],
483
498
  // models.json custom providers (the registry). A runtime key beats the
484
499
  // file's `apiKey: "$NAN_API_KEY"`, which pi would otherwise read from the
485
500
  // shared process.env, where a credential pool is still the raw comma list.
@@ -168,7 +168,8 @@ function buildOpencodeReasoningOptions(
168
168
  // qualitative level — translate internally (see "What We're NOT Doing" in
169
169
  // the plan: no numeric budget surface for operators, only this
170
170
  // adapter-internal transport detail).
171
- if (level === "max") return { reasoningEffort: level };
171
+ // Its Anthropic transport reads `effort` into `output_config.effort`.
172
+ if (level === "max") return { effort: level };
172
173
  return { thinking: { type: "enabled", budgetTokens: ANTHROPIC_BUDGET_TOKENS_BY_LEVEL[level] } };
173
174
  }
174
175
  if (providerId === "openrouter") {
@@ -320,6 +320,8 @@ export interface CredStatus {
320
320
  * string signals Bedrock mode with `AWS_REGION` unset (no region fabricated).
321
321
  */
322
322
  bedrockRegion?: string;
323
+ /** Pi-mono only: the `opencode-go` model ids pi-ai resolves. Undefined for other harnesses. */
324
+ opencodeGoModels?: string[];
323
325
  }
324
326
 
325
327
  /**