@desplega.ai/agent-swarm 2.6.0 → 2.7.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 (324) hide show
  1. package/README.md +2 -0
  2. package/dist/{acp-adapter-dgfakfpw.js → acp-adapter-8dn5tq56.js} +6 -6
  3. package/dist/{actions-fbqma8aa.js → actions-3r6gw7dy.js} +18 -18
  4. package/dist/{amp-adapter-en0jy6hs.js → amp-adapter-wjtq413p.js} +3 -3
  5. package/dist/{anthropic-messages-txs190dd.js → anthropic-messages-n4cby1rz.js} +4 -4
  6. package/dist/{app-k8a5cgpp.js → app-d53yhykk.js} +3 -3
  7. package/dist/{assistant-a16433gr.js → assistant-ksrawj3k.js} +20 -20
  8. package/dist/{boot-reembed-6c1sjfnr.js → boot-reembed-fxfw17m2.js} +11 -3
  9. package/dist/{boot-reembed-f4nn561n.js → boot-reembed-k5y5xbj0.js} +15 -8
  10. package/dist/{boot-scrub-sweep-d4w9gajf.js → boot-scrub-sweep-se41j9fd.js} +3 -3
  11. package/dist/{boot-seeds-9bmkspyw.js → boot-seeds-r3kwwv8b.js} +4 -4
  12. package/dist/{claude-adapter-nys1kjxa.js → claude-adapter-ahmyf231.js} +8 -8
  13. package/dist/{claude-managed-adapter-55207973.js → claude-managed-adapter-5knamegc.js} +3 -3
  14. package/dist/{claude-managed-setup-3cm35gey.js → claude-managed-setup-mdc7dy6b.js} +3 -3
  15. package/dist/{claude-sdk-session-nw5yd8cp.js → claude-sdk-session-n5wen9zk.js} +8729 -8600
  16. package/dist/{cli-683y0b3a.js → cli-0dqtv2jg.js} +2 -2
  17. package/dist/{cli-a34pr3w1.js → cli-0tww7dnz.js} +2 -2
  18. package/dist/{cli-w2656ka1.js → cli-0w4e4v58.js} +7 -7
  19. package/dist/{cli-5sckpqcx.js → cli-2sjm3t4r.js} +1 -1
  20. package/dist/{cli-gf2850q7.js → cli-3cywb44q.js} +18 -1
  21. package/dist/{cli-8wkb09q5.js → cli-3k9jej0c.js} +2 -2
  22. package/dist/{cli-3aqd4jaa.js → cli-3v2cyqb8.js} +3 -3
  23. package/dist/{cli-emhjcj2y.js → cli-5qt9vzmq.js} +89 -20
  24. package/dist/{cli-abkm71vr.js → cli-5rp1w4cr.js} +1 -1
  25. package/dist/{cli-gf95q3bm.js → cli-6vwjsetx.js} +4 -4
  26. package/dist/{cli-k21gx5ht.js → cli-6w2yrrc0.js} +10 -10
  27. package/dist/{cli-m70bajzr.js → cli-75tdydgv.js} +1 -1
  28. package/dist/{cli-5rj83bam.js → cli-8feswf2v.js} +47 -36
  29. package/dist/{cli-sxgtf5fw.js → cli-8wd3mbyj.js} +1 -1
  30. package/dist/{cli-ca6banwz.js → cli-9csfthbw.js} +10 -8
  31. package/dist/{cli-dqygeeh5.js → cli-b41p764a.js} +31 -3
  32. package/dist/{cli-m5a9m0ak.js → cli-c1t533hh.js} +3 -3
  33. package/dist/{cli-2sjhpsns.js → cli-c28jxpcv.js} +1 -1
  34. package/dist/{cli-bqztfyve.js → cli-cb50g9cd.js} +1 -1
  35. package/dist/{cli-ag2yrnt5.js → cli-d8gaphtr.js} +2 -2
  36. package/dist/{cli-9abdgn5t.js → cli-dgtfysne.js} +3 -3
  37. package/dist/{cli-nq677csy.js → cli-dscnhn18.js} +1 -1
  38. package/dist/{cli-3fwqy71j.js → cli-dv11pgpv.js} +39 -32
  39. package/dist/{cli-v49fkghp.js → cli-ekptgk2m.js} +6 -6
  40. package/dist/{cli-c2habvsf.js → cli-f3mzy662.js} +17 -17
  41. package/dist/{cli-zqne1qcj.js → cli-fk7k8s9x.js} +2 -2
  42. package/dist/{cli-1tem6ns9.js → cli-gt4h8cxw.js} +3 -2
  43. package/dist/{cli-vbhgkw9h.js → cli-h3e7bhvn.js} +3 -3
  44. package/dist/{cli-ffvsj2qw.js → cli-hkxqdmnb.js} +3 -3
  45. package/dist/{cli-a2rr0fmg.js → cli-hpkvgdad.js} +7 -4
  46. package/dist/{cli-art7c1zh.js → cli-hq2v4rr5.js} +37 -9
  47. package/dist/{cli-sqma6vw1.js → cli-k1zb7raw.js} +5 -5
  48. package/dist/{cli-4rvr7zfx.js → cli-k4awgvzm.js} +5 -0
  49. package/dist/{cli-3ndmpxxv.js → cli-k7d3hvya.js} +2 -2
  50. package/dist/{cli-3gfvmzht.js → cli-kbtdftbw.js} +74 -26
  51. package/dist/{cli-hfeb68z4.js → cli-kj69b9tw.js} +34 -6
  52. package/dist/{cli-az25fn5f.js → cli-kk9jy4nm.js} +2 -2
  53. package/dist/{cli-39aet655.js → cli-m52t4cta.js} +1 -1
  54. package/dist/{cli-cz37jqss.js → cli-mdadewqp.js} +5 -5
  55. package/dist/{cli-esakngh8.js → cli-nf6k60sn.js} +1 -1
  56. package/dist/{cli-j6rktc91.js → cli-nzdf09js.js} +1 -1
  57. package/dist/{cli-rr7zx19j.js → cli-prcyzecg.js} +12 -12
  58. package/dist/{cli-4hxrf0p4.js → cli-pzafyqtk.js} +393 -62
  59. package/dist/{cli-g8ka5j4j.js → cli-qvaa1gjp.js} +173 -52
  60. package/dist/{cli-d2dpf95n.js → cli-rg4b2g6j.js} +1 -1
  61. package/dist/{cli-kf4jree9.js → cli-rm66vqdz.js} +1 -1
  62. package/dist/{cli-4rf0f5vg.js → cli-s504c3w7.js} +5 -3
  63. package/dist/{cli-nn6vx2mq.js → cli-swkx9jm2.js} +7 -5
  64. package/dist/{cli-g20jjwt4.js → cli-thsaq4pd.js} +16 -16
  65. package/dist/{cli-aj8djyqt.js → cli-twjnyvs8.js} +2 -2
  66. package/dist/{cli-03gg9pd1.js → cli-typdpe8g.js} +4 -4
  67. package/dist/{cli-mtnj4fwa.js → cli-vg8pwmsd.js} +6 -6
  68. package/dist/{cli-gnsb1zxa.js → cli-vhpckzvp.js} +1 -1
  69. package/dist/{cli-7enjwya9.js → cli-vz78s5wm.js} +1 -1
  70. package/dist/{cli-dv97kdzv.js → cli-wdxgsdz5.js} +2 -2
  71. package/dist/{cli-7a6p4e65.js → cli-wtjg6je3.js} +1 -1
  72. package/dist/{cli-6y51ryva.js → cli-x3cngb1h.js} +44 -12
  73. package/dist/{cli-n7asmce6.js → cli-xbz6mcg4.js} +3 -3
  74. package/dist/{cli-24j1jeeb.js → cli-y4tdxsqb.js} +3 -3
  75. package/dist/{cli-mvjz5an3.js → cli-yqrsp3ny.js} +7 -7
  76. package/dist/cli.js +31 -21
  77. package/dist/{codex-adapter-d95q4qsn.js → codex-adapter-7wepzq15.js} +6 -6
  78. package/dist/{codex-hook-brpnycn5.js → codex-hook-pbdh4xhx.js} +2 -2
  79. package/dist/{codex-session-runner-gdwd8h0c.js → codex-session-runner-02sb49e4.js} +6 -6
  80. package/dist/{commands-jqmgk54b.js → commands-3rcspxct.js} +2 -2
  81. package/dist/{controller-z3jq589f.js → controller-8ar4avd6.js} +3 -3
  82. package/dist/{controllers-connect-r2za39ef.js → controllers-connect-ajvh9h0v.js} +19 -19
  83. package/dist/{cursor-adapter-4mr4jgkw.js → cursor-adapter-gd0sa7bh.js} +1 -1
  84. package/dist/{db-w36wwb5g.js → db-dv9j7ts5.js} +2 -2
  85. package/dist/{db-ppv2tze7.js → db-z26vgdvj.js} +3 -3
  86. package/dist/{dispatcher-pv5h77vf.js → dispatcher-8m0htjq8.js} +17 -17
  87. package/dist/{dsh-adapter-d4pdpb3r.js → dsh-adapter-y8gyk130.js} +1 -1
  88. package/dist/{e2b-fn3zacvt.js → e2b-74vg9b7e.js} +1 -1
  89. package/dist/{e2b-222hsajc.js → e2b-jyay8wzn.js} +1 -1
  90. package/dist/{easter-egg-3d-4sp4dvmc.js → easter-egg-3d-28f880xx.js} +1 -1
  91. package/dist/{execute-jev839y1.js → execute-9seb79vh.js} +1 -1
  92. package/dist/{external-channel-y6nkzhnn.js → external-channel-smz7jyth.js} +2 -2
  93. package/dist/{getMachineId-bsd-brn0xym3.js → getMachineId-bsd-whfre6gr.js} +3 -3
  94. package/dist/{getMachineId-darwin-t339sfxm.js → getMachineId-darwin-5zjynb19.js} +3 -3
  95. package/dist/{getMachineId-linux-5eq95d8b.js → getMachineId-linux-8mksrrsq.js} +3 -3
  96. package/dist/{getMachineId-unsupported-qavrtc7j.js → getMachineId-unsupported-fxtqsdt2.js} +3 -3
  97. package/dist/{getMachineId-win-q7fe1wx7.js → getMachineId-win-nzfmz857.js} +3 -3
  98. package/dist/{grok-adapter-t08b9ntm.js → grok-adapter-z0nnvtbn.js} +7 -7
  99. package/dist/handlers-pspryh6g.js +41 -0
  100. package/dist/{heartbeat-vn31xwdk.js → heartbeat-f928aqyp.js} +20 -20
  101. package/dist/{hook-cpjs8q51.js → hook-d7c605r4.js} +7 -7
  102. package/dist/{hook-6d0zbvvt.js → hook-q9hv5xf9.js} +5 -5
  103. package/dist/{http-9c6medda.js → http-q6j4k989.js} +1000 -251
  104. package/dist/{index-k0yt1tvq.js → index-5nb266jr.js} +17 -17
  105. package/dist/{index-ysd45kgc.js → index-8fxt5eah.js} +10 -10
  106. package/dist/{index-zgvqzjsw.js → index-cekzwhez.js} +5 -5
  107. package/dist/{index-38t35h0n.js → index-ch0c1xff.js} +17 -17
  108. package/dist/{index-pkma1yf8.js → index-ebnjhxh1.js} +1 -1
  109. package/dist/{index-yqgbfgaj.js → index-ed8dy8rn.js} +20 -20
  110. package/dist/{index-qhjf2htm.js → index-ejqdkkqx.js} +262 -96
  111. package/dist/{index-dr94zbnh.js → index-farm1zsn.js} +32661 -29443
  112. package/dist/{index-g6k9tctj.js → index-yn1yxg85.js} +1 -1
  113. package/dist/{keepalive-em0117bp.js → keepalive-zzc3hks1.js} +3 -3
  114. package/dist/lead-c0pjcaw4.js +79 -0
  115. package/dist/{maintenance-kbme3qy9.js → maintenance-kqsxzpzx.js} +1 -1
  116. package/dist/{oauth-refresh-sweep-xysj4yse.js → oauth-refresh-sweep-7qt0g55z.js} +8 -3
  117. package/dist/{onboard-p6q0vz97.js → onboard-k7x13fqg.js} +121 -8
  118. package/dist/{opencode-adapter-esv3k9yn.js → opencode-adapter-tqvrtter.js} +5 -5
  119. package/dist/{otel-impl-54qhqh1z.js → otel-impl-sqkf6k3b.js} +97 -97
  120. package/dist/{pi-mono-adapter-mymbwhj0.js → pi-mono-adapter-m2qnafxm.js} +89 -14
  121. package/dist/{pricing-refresh-yb75wv33.js → pricing-refresh-wkpwhgvs.js} +3 -3
  122. package/dist/{rbac-roles-dexhc9x8.js → rbac-roles-dpn0c0wj.js} +7 -4
  123. package/dist/{rbac-roles-1vp2mw4p.js → rbac-roles-wk7xd6a6.js} +2 -2
  124. package/dist/{render-v2-tjay5hhf.js → render-v2-ex2m4g2k.js} +3 -3
  125. package/dist/{resume-8ayf37g5.js → resume-725g9b0b.js} +7 -7
  126. package/dist/{runtime-hqe5cqcd.js → runtime-kajas15g.js} +1 -1
  127. package/dist/{session-access-fdhp987s.js → session-access-0j2ezjje.js} +8 -4
  128. package/dist/{session-source-backfill-qgtc86z1.js → session-source-backfill-62202awe.js} +2 -2
  129. package/dist/{setup-4rrmqcq3.js → setup-9sj11ax6.js} +1 -1
  130. package/dist/{virtual-modules-52km00d6.js → virtual-modules-c6fcq6zr.js} +2 -2
  131. package/dist/{work-objects-p28fgd70.js → work-objects-0y07495y.js} +20 -20
  132. package/dist/worker-d4q9sqbc.js +79 -0
  133. package/openapi.json +1536 -407
  134. package/package.json +7 -5
  135. package/src/be/automation-preflight.ts +2 -0
  136. package/src/be/connector-codes.ts +65 -22
  137. package/src/be/db/api-keys.ts +9 -0
  138. package/src/be/db/workflow-runs.ts +36 -5
  139. package/src/be/db.ts +80 -22
  140. package/src/be/memory/boot-reembed.ts +4 -1
  141. package/src/be/memory/constants.ts +6 -0
  142. package/src/be/memory/embedding-recovery.ts +48 -0
  143. package/src/be/memory/index-content.ts +5 -2
  144. package/src/be/memory/providers/openai-embedding.ts +65 -14
  145. package/src/be/memory/task-learning.ts +257 -0
  146. package/src/be/memory/types.ts +13 -2
  147. package/src/be/migrations/245_script_executor.sql +3 -0
  148. package/src/be/migrations/246_connector_code_kind.sql +11 -0
  149. package/src/be/oauth-refresh-sweep.ts +2 -1
  150. package/src/be/onboarding.ts +125 -15
  151. package/src/be/rbac-roles.ts +4 -1
  152. package/src/be/scripts/boot-reembed.ts +2 -0
  153. package/src/be/scripts/db.ts +14 -4
  154. package/src/be/scripts/embeddings.ts +4 -2
  155. package/src/be/scripts/run-saved.ts +1 -0
  156. package/src/be/scripts/typecheck.ts +9 -3
  157. package/src/be/seed/agent-fs-provision.ts +75 -14
  158. package/src/be/seed-skills/index.ts +7 -0
  159. package/src/be/swarm-config-guard.ts +3 -0
  160. package/src/cli.tsx +9 -0
  161. package/src/comb/config.ts +6 -1
  162. package/src/commands/onboard/agent-fs-version.ts +5 -0
  163. package/src/commands/onboard/compose-generator.ts +117 -4
  164. package/src/commands/onboard/env-generator.ts +9 -0
  165. package/src/commands/onboard/tei-image.ts +12 -0
  166. package/src/commands/onboard/types.ts +7 -0
  167. package/src/commands/onboard.tsx +11 -1
  168. package/src/commands/provider-credentials.ts +2 -1
  169. package/src/commands/runner.ts +14 -1
  170. package/src/heartbeat/heartbeat.ts +14 -1
  171. package/src/heartbeat/script-runner-probe.ts +47 -0
  172. package/src/http/agentfs-proxy.ts +369 -0
  173. package/src/http/all-routes.ts +1 -0
  174. package/src/http/api-keys.ts +24 -9
  175. package/src/http/codex-oauth-device.ts +8 -1
  176. package/src/http/fs.ts +64 -10
  177. package/src/http/idempotency-key.ts +1 -1
  178. package/src/http/inbox-state.ts +27 -1
  179. package/src/http/index.ts +7 -4
  180. package/src/http/memory.ts +7 -4
  181. package/src/http/onboarding.ts +139 -11
  182. package/src/http/pages-public.ts +38 -5
  183. package/src/http/scripts.ts +27 -0
  184. package/src/http/session-access.ts +24 -0
  185. package/src/http/sessions.ts +10 -6
  186. package/src/http/tasks.ts +85 -11
  187. package/src/http/telemetry-ui.ts +1 -1
  188. package/src/http/user-roles.ts +4 -1
  189. package/src/http/users.ts +272 -3
  190. package/src/http/utils.ts +22 -3
  191. package/src/http/workflows.ts +10 -0
  192. package/src/http/x.ts +5 -0
  193. package/src/prompts/base-prompt.ts +45 -10
  194. package/src/prompts/session-templates.ts +43 -5
  195. package/src/providers/amp-adapter.ts +1 -1
  196. package/src/providers/codex-adapter.ts +6 -1
  197. package/src/providers/cursor-adapter.ts +1 -1
  198. package/src/providers/dsh-adapter.ts +1 -1
  199. package/src/providers/pi-mono-adapter.ts +99 -14
  200. package/src/rbac/legacy-policy.ts +1 -0
  201. package/src/rbac/permissions.ts +4 -0
  202. package/src/scheduler/scheduler.ts +2 -1
  203. package/src/scripts-runtime/eval-harness.ts +8 -1
  204. package/src/scripts-runtime/executors/native.ts +22 -16
  205. package/src/scripts-runtime/executors/quickjs-prelude.ts +103 -1
  206. package/src/scripts-runtime/executors/quickjs-runner.ts +2 -1
  207. package/src/scripts-runtime/executors/quickjs.ts +7 -0
  208. package/src/scripts-runtime/executors/registry.ts +32 -0
  209. package/src/scripts-runtime/executors/types.ts +7 -0
  210. package/src/scripts-runtime/host-validated-args.ts +133 -0
  211. package/src/scripts-runtime/loader.ts +30 -3
  212. package/src/scripts-runtime/swarm-sdk.ts +9 -2
  213. package/src/scripts-runtime/types/stdlib.d.ts +9 -1
  214. package/src/scripts-runtime/types/swarm-sdk.d.ts +9 -1
  215. package/src/tests/agent-fs-provision-seeder.test.ts +261 -92
  216. package/src/tests/agentfs-proxy.test.ts +1106 -0
  217. package/src/tests/api-key-status-by-suffix.test.ts +242 -0
  218. package/src/tests/base-prompt.test.ts +139 -4
  219. package/src/tests/codex-adapter.test.ts +46 -8
  220. package/src/tests/codex-oauth-device.test.ts +12 -1
  221. package/src/tests/comb-config-guard.test.ts +20 -0
  222. package/src/tests/comb-links.test.ts +79 -1
  223. package/src/tests/comb-status.test.ts +44 -0
  224. package/src/tests/community-linear-delivery-loop.test.ts +2099 -0
  225. package/src/tests/credential-check.test.ts +4 -2
  226. package/src/tests/docs-index-footprint.test.js +3 -3
  227. package/src/tests/docs-index.test.ts +135 -0
  228. package/src/tests/dsh-adapter.test.ts +1 -1
  229. package/src/tests/fetch-resolved-env.test.ts +16 -2
  230. package/src/tests/fixtures/onboard/baseline-states.ts +68 -0
  231. package/src/tests/fixtures/onboard/content.compose.yml.txt +190 -0
  232. package/src/tests/fixtures/onboard/content.env.txt +29 -0
  233. package/src/tests/fixtures/onboard/dev.compose.yml.txt +155 -0
  234. package/src/tests/fixtures/onboard/dev.env.txt +28 -0
  235. package/src/tests/fixtures/onboard/full-all.compose.yml.txt +582 -0
  236. package/src/tests/fixtures/onboard/full-all.env.txt +59 -0
  237. package/src/tests/fixtures/onboard/full.compose.yml.txt +435 -0
  238. package/src/tests/fixtures/onboard/full.env.txt +36 -0
  239. package/src/tests/fixtures/onboard/research.compose.yml.txt +155 -0
  240. package/src/tests/fixtures/onboard/research.env.txt +28 -0
  241. package/src/tests/fixtures/onboard/solo.compose.yml.txt +85 -0
  242. package/src/tests/fixtures/onboard/solo.env.txt +26 -0
  243. package/src/tests/heartbeat-checklist.test.ts +89 -0
  244. package/src/tests/heartbeat-script-runner-probe.test.ts +83 -0
  245. package/src/tests/http-log-scrubbing.test.ts +34 -1
  246. package/src/tests/http-task-write-ownership.test.ts +5 -1
  247. package/src/tests/invites-http.test.ts +464 -0
  248. package/src/tests/memory-embedding-input-cap.test.ts +122 -1
  249. package/src/tests/memory-embedding-recovery.test.ts +137 -0
  250. package/src/tests/oauth-refresh-sweep.test.ts +34 -1
  251. package/src/tests/onboard-compose.test.ts +162 -0
  252. package/src/tests/onboard-env.test.ts +24 -0
  253. package/src/tests/onboard-non-interactive.test.ts +29 -0
  254. package/src/tests/onboard-tei-image.test.ts +36 -0
  255. package/src/tests/onboarding.test.ts +451 -4
  256. package/src/tests/openai-embedding-provider.test.ts +105 -2
  257. package/src/tests/opencode-adapter.test.ts +2 -1
  258. package/src/tests/pages-frame-ancestors.test.ts +162 -0
  259. package/src/tests/pi-codemode.test.ts +2 -1
  260. package/src/tests/pi-mono-adapter.test.ts +54 -0
  261. package/src/tests/pi-mono-session-cost-on-abort.test.ts +270 -0
  262. package/src/tests/pi-native-mcp.test.ts +13 -1
  263. package/src/tests/pi-tool-exposure.test.ts +8 -8
  264. package/src/tests/preload-env.test.ts +4 -0
  265. package/src/tests/preload.ts +4 -0
  266. package/src/tests/probe-tool-schema-tokens.test.ts +144 -0
  267. package/src/tests/prompt-template-session.test.ts +20 -3
  268. package/src/tests/rbac-admission-e2e.test.ts +22 -6
  269. package/src/tests/rbac-admission.test.ts +342 -4
  270. package/src/tests/rbac-engine.test.ts +1 -0
  271. package/src/tests/rbac-roles.test.ts +9 -1
  272. package/src/tests/request-composition-derive.test.ts +329 -0
  273. package/src/tests/script-connections.test.ts +2 -0
  274. package/src/tests/script-executor-conformance.test.ts +36 -1
  275. package/src/tests/script-executor-quickjs-pool.test.ts +50 -0
  276. package/src/tests/script-executor-quickjs.test.ts +140 -0
  277. package/src/tests/script-host-validated-args-native.test.ts +196 -0
  278. package/src/tests/script-per-script-executor.test.ts +415 -0
  279. package/src/tests/scripts-external-api.test.ts +45 -0
  280. package/src/tests/sdk-allowlist.test.ts +3 -2
  281. package/src/tests/self-improvement.test.ts +4 -4
  282. package/src/tests/send-task-requester-gate.test.ts +64 -3
  283. package/src/tests/sessions-move.test.ts +39 -1
  284. package/src/tests/store-progress-learning.test.ts +381 -0
  285. package/src/tests/ui-logs-parser.test.ts +180 -0
  286. package/src/tests/whoami-route.test.ts +41 -0
  287. package/src/tests/workflow-engine-v2.test.ts +116 -1
  288. package/src/tests/workflow-mcp-trigger-schema.test.ts +209 -0
  289. package/src/tests/workflow-run-finished-event.test.ts +263 -0
  290. package/src/tests/workflow-swarm-script.test.ts +34 -0
  291. package/src/tools/inject-learning.ts +1 -1
  292. package/src/tools/memory-edit.ts +1 -1
  293. package/src/tools/memory-search.ts +1 -1
  294. package/src/tools/script-common.ts +1 -0
  295. package/src/tools/script-run.ts +7 -0
  296. package/src/tools/script-upsert.ts +7 -0
  297. package/src/tools/store-progress.ts +71 -1
  298. package/src/tools/utils.ts +12 -1
  299. package/src/tools/workflows/trigger-workflow.ts +104 -10
  300. package/src/types.ts +12 -1
  301. package/src/utils/constants.ts +39 -0
  302. package/src/utils/credentials.ts +5 -2
  303. package/src/utils/task-attachment-links.ts +15 -3
  304. package/src/workflows/engine.ts +18 -16
  305. package/src/workflows/executors/swarm-script.ts +5 -0
  306. package/src/workflows/foreach-driver.ts +5 -11
  307. package/src/workflows/node-timeout.ts +30 -0
  308. package/templates/community/linear-delivery-loop/PLAYBOOK.md +323 -0
  309. package/templates/community/linear-delivery-loop/README.md +38 -0
  310. package/templates/community/linear-delivery-loop/config.example.json +31 -0
  311. package/templates/community/linear-delivery-loop/lead-prompt.md +34 -0
  312. package/templates/community/linear-delivery-loop/schedule.json +10 -0
  313. package/templates/community/linear-delivery-loop/scripts/linear-delivery-setup.ts +148 -0
  314. package/templates/community/linear-delivery-loop/scripts/linear-delivery-tick.ts +1781 -0
  315. package/templates/skills/swarm-docs/config.json +14 -0
  316. package/templates/skills/swarm-docs/content.md +144 -0
  317. package/dist/handlers-eqsnmv95.js +0 -41
  318. package/dist/lead-23e7z59t.js +0 -79
  319. package/dist/worker-e3w7vye1.js +0 -79
  320. package/dist/{azure-openai-responses-9kfwhmn7.js → azure-openai-responses-43asa23s.js} +0 -0
  321. package/dist/{node-vxx896fa.js → node-t28cc56b.js} +0 -0
  322. package/dist/{openai-codex-responses-79s8xnbh.js → openai-codex-responses-jhr3sjtm.js} +3 -3
  323. package/dist/{openai-completions-tdbyts0x.js → openai-completions-gs6yn37k.js} +3 -3
  324. /package/dist/{openai-responses-dm3t5vqp.js → openai-responses-jqm9vwwk.js} +0 -0
package/src/http/fs.ts CHANGED
@@ -2,6 +2,7 @@ import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import { z } from "zod";
3
3
  import { deleteTaskAttachment, getTaskAttachments, getTaskById } from "../be/db";
4
4
  import { appendDraftTaskText } from "../be/db/tasks/draft-text";
5
+ import { getUserGrant } from "../be/rbac-roles";
5
6
  import {
6
7
  ensureAgentFsCredentialsForAgent,
7
8
  inviteEmailToSharedOrg,
@@ -14,10 +15,10 @@ import {
14
15
  } from "../be/task-attachment-store";
15
16
  import { type FileObject, FilesError, normalizeFilesError } from "../fs/provider";
16
17
  import { getFileStorageProvider } from "../fs/registry";
17
- import { can, type RbacResource } from "../rbac";
18
+ import { can, isRbacEnabled, type RbacResource } from "../rbac";
18
19
  import { type TaskAttachment, TaskAttachmentSchema } from "../types";
19
20
  import { attachmentContentDisposition } from "../utils/content-disposition";
20
- import { getCurrentRequestAuth } from "../utils/request-auth-context";
21
+ import { getCurrentRequestAuth, getRequestAuth } from "../utils/request-auth-context";
21
22
  import { scrubSecrets } from "../utils/secret-scrubber";
22
23
  import { transcribeAudioNotes } from "../utils/transcription";
23
24
  import { requestPrincipal } from "./request-principal";
@@ -66,7 +67,20 @@ const agentFsCredentialStateSchema = z.object({
66
67
 
67
68
  const agentFsInviteStateSchema = z.object({
68
69
  orgId: z.string(),
69
- invited: z.boolean(),
70
+ invited: z.boolean().describe("False only when the email already holds the requested role."),
71
+ status: z
72
+ .enum(["already_member", "added", "pending"])
73
+ .describe(
74
+ "already_member: no change. added: an existing agent-fs account joined the org. pending: the email has no account, so agent-fs created an invite the person must accept.",
75
+ ),
76
+ inviteId: z.string().optional().describe("Pending invite id (status pending)."),
77
+ expiresAt: z.string().optional().describe("ISO time the accept link expires (status pending)."),
78
+ acceptUrl: z
79
+ .string()
80
+ .optional()
81
+ .describe(
82
+ "Link the invitee opens to accept (status pending). agent-fs sends no email, so hand it to the person. Absent when agent-fs has no app URL configured.",
83
+ ),
70
84
  });
71
85
 
72
86
  const signedUrlResponseSchema = z.object({
@@ -112,22 +126,26 @@ const inviteMemberRoute = route({
112
126
  pattern: ["api", "fs", "members", "invite"],
113
127
  summary: "Invite an external member into the agent-fs shared org",
114
128
  description:
115
- "The API server performs the invite with its own bootstrap credentials (which are API-only and never served over HTTP), provisioning the shared org/drive first when needed. Intended for the cloud control plane's Connect-to-Drive flow. No keys are returned; the invitee obtains their own key via agent-fs registration.",
129
+ "The API server performs the invite with its own bootstrap credentials (which are API-only and never served over HTTP), provisioning the shared org/drive first when needed. Intended for the cloud control plane's Connect-to-Drive flow. An email with an agent-fs account is added directly (status added). An email without one gets a pending invite (status pending) with an acceptUrl when agent-fs returns one; agent-fs sends no email, so the caller must hand the link to the invitee. No keys are returned; the invitee obtains their own key via agent-fs registration.",
116
130
  tags: ["FS"],
117
131
  body: z.object({
118
132
  email: z.email(),
119
133
  role: z.enum(["viewer", "editor", "admin"]).default("editor"),
120
134
  }),
121
135
  responses: {
122
- 200: { description: "Invite state ({ orgId, invited })", schema: agentFsInviteStateSchema },
136
+ 200: {
137
+ description: "Invite state ({ orgId, invited, status, acceptUrl? })",
138
+ schema: agentFsInviteStateSchema,
139
+ },
123
140
  400: { description: "Invalid body" },
141
+ 403: { description: "A non-admin user token named an email other than its own" },
124
142
  500: { description: "Provisioning or invite failed" },
125
143
  },
126
144
  auth: { apiKey: true },
127
- rbac: {
128
- ungated:
129
- "Tenant API key is the org-owner credential; the invite runs under the API server's own bootstrap identity, no per-agent principal applies.",
130
- },
145
+ // The operator key and grant-all users invite any email at any role. Other
146
+ // users may only join with their own account email, at a role the handler
147
+ // derives from their grant.
148
+ rbac: { permission: "fs.join.own" },
131
149
  });
132
150
 
133
151
  const listTaskFilesRoute = route({
@@ -234,6 +252,37 @@ const deleteTaskFileRoute = route({
234
252
  rbac: { permission: "task.fs.mutate" },
235
253
  });
236
254
 
255
+ type InviteRole = "viewer" | "editor" | "admin";
256
+
257
+ /**
258
+ * The email and role an invite runs with. The operator key and grant-all users
259
+ * keep the body as sent. Any other user joins only as itself: the body email
260
+ * must match the account email (ignoring case), the stored spelling is used,
261
+ * and the role is `editor` when the user may change task files, else `viewer`.
262
+ */
263
+ async function resolveInvite(
264
+ req: IncomingMessage,
265
+ body: { email: string; role: InviteRole },
266
+ ): Promise<{ email: string; role: InviteRole } | { denied: string }> {
267
+ const auth = getRequestAuth(req);
268
+ if (auth?.kind !== "user" || !isRbacEnabled()) return body;
269
+ const grant = await getUserGrant(auth.userId);
270
+ if (grant.grantsAll) return body;
271
+ const decision = can({
272
+ principal: { kind: "user", userId: auth.userId },
273
+ verb: "fs.join.own",
274
+ resource: { kind: "none" },
275
+ source: "http",
276
+ });
277
+ // Admission already required the role to hold `fs.join.own`.
278
+ if (!decision.allow) return { denied: decision.reason };
279
+ const ownEmail = auth.user.email?.trim();
280
+ if (!ownEmail || ownEmail.toLowerCase() !== body.email.trim().toLowerCase()) {
281
+ return { denied: "You can only join the shared drive with your own account email" };
282
+ }
283
+ return { email: ownEmail, role: grant.verbs.has("task.fs.mutate") ? "editor" : "viewer" };
284
+ }
285
+
237
286
  export async function handleFs(
238
287
  req: IncomingMessage,
239
288
  res: ServerResponse,
@@ -264,8 +313,13 @@ export async function handleFs(
264
313
  if (inviteMemberRoute.match(req.method, pathSegments)) {
265
314
  const parsed = await inviteMemberRoute.parse(req, res, pathSegments, queryParams);
266
315
  if (!parsed) return true;
316
+ const invite = await resolveInvite(req, parsed.body);
317
+ if ("denied" in invite) {
318
+ jsonError(res, invite.denied, 403);
319
+ return true;
320
+ }
267
321
  try {
268
- const result = await inviteEmailToSharedOrg(parsed.body.email, parsed.body.role);
322
+ const result = await inviteEmailToSharedOrg(invite.email, invite.role);
269
323
  inviteMemberRoute.respond(res, 200, result);
270
324
  } catch (error) {
271
325
  const message = scrubSecrets(error instanceof Error ? error.message : String(error));
@@ -8,7 +8,7 @@ import { getRequestAuth } from "../utils/request-auth-context";
8
8
  import { jsonError } from "./utils";
9
9
 
10
10
  /** Visible ASCII, at most 255 characters: room for a UUID, a ULID or a client's own scheme. */
11
- const IDEMPOTENCY_KEY_PATTERN = /^[\x21-\x7E]{1,255}$/;
11
+ export const IDEMPOTENCY_KEY_PATTERN = /^[\x21-\x7E]{1,255}$/;
12
12
 
13
13
  /** Set on a response answered from an earlier accepted request. */
14
14
  export const IDEMPOTENT_REPLAYED_HEADER = "Idempotent-Replayed";
@@ -1,7 +1,10 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import { z } from "zod";
3
3
  import { getDbClient, listInboxState, upsertInboxState } from "../be/db";
4
+ import { getUserGrant } from "../be/rbac-roles";
5
+ import { isRbacEnabled } from "../rbac";
4
6
  import { InboxItemStateSchema, InboxItemStatusSchema, InboxItemTypeSchema } from "../types";
7
+ import { getRequestAuth } from "../utils/request-auth-context";
5
8
  import { route } from "./route-def";
6
9
  import { jsonError } from "./utils";
7
10
 
@@ -59,8 +62,10 @@ const upsertState = route({
59
62
  },
60
63
  400: { description: "Validation error" },
61
64
  401: { description: "Unauthorized" },
65
+ 403: { description: "A non-admin user token wrote another user's state" },
62
66
  },
63
67
  auth: { apiKey: true },
68
+ rbac: { permission: "favorite.write.own" },
64
69
  });
65
70
 
66
71
  const bulkUpsertState = route({
@@ -80,13 +85,26 @@ const bulkUpsertState = route({
80
85
  },
81
86
  400: { description: "Validation error" },
82
87
  401: { description: "Unauthorized" },
88
+ 403: { description: "A non-admin user token wrote another user's state" },
83
89
  },
84
90
  auth: { apiKey: true },
85
- rbac: { ungated: "per-user inbox UI state, same posture as PATCH /api/inbox-state" },
91
+ rbac: { permission: "favorite.write.own" },
86
92
  });
87
93
 
88
94
  // ─── Handler ─────────────────────────────────────────────────────────────────
89
95
 
96
+ const OTHER_USER_STATE_DENIED = "You can only change your own inbox state";
97
+
98
+ /**
99
+ * True when a user token without a grant-all role names another user's
100
+ * `userId`. Operator-key and admin callers keep writing any user's state.
101
+ */
102
+ async function writesOtherUsersState(req: IncomingMessage, userId: string): Promise<boolean> {
103
+ const auth = getRequestAuth(req);
104
+ if (auth?.kind !== "user" || auth.userId === userId || !isRbacEnabled()) return false;
105
+ return !(await getUserGrant(auth.userId)).grantsAll;
106
+ }
107
+
90
108
  export async function handleInboxState(
91
109
  req: IncomingMessage,
92
110
  res: ServerResponse,
@@ -108,6 +126,10 @@ export async function handleInboxState(
108
126
  if (bulkUpsertState.match(req.method, pathSegments)) {
109
127
  const parsed = await bulkUpsertState.parse(req, res, pathSegments, queryParams);
110
128
  if (!parsed) return true;
129
+ if (await writesOtherUsersState(req, parsed.body.userId)) {
130
+ jsonError(res, OTHER_USER_STATE_DENIED, 403);
131
+ return true;
132
+ }
111
133
  try {
112
134
  const items = await getDbClient().transaction(async () => {
113
135
  const rows = [];
@@ -126,6 +148,10 @@ export async function handleInboxState(
126
148
  if (upsertState.match(req.method, pathSegments)) {
127
149
  const parsed = await upsertState.parse(req, res, pathSegments, queryParams);
128
150
  if (!parsed) return true;
151
+ if (await writesOtherUsersState(req, parsed.body.userId)) {
152
+ jsonError(res, OTHER_USER_STATE_DENIED, 403);
153
+ return true;
154
+ }
129
155
  try {
130
156
  const item = await upsertInboxState({
131
157
  userId: parsed.body.userId,
package/src/http/index.ts CHANGED
@@ -70,6 +70,7 @@ import { isEnvFlagEnabled } from "../utils/env-flag";
70
70
  import { scrubSecrets } from "../utils/secret-scrubber";
71
71
  import { initWorkflows } from "../workflows";
72
72
  import { handleActiveSessions } from "./active-sessions";
73
+ import { handleAgentFsProxy } from "./agentfs-proxy";
73
74
  import { handleAgentRegister, handleAgentsRest } from "./agents";
74
75
  import { handleApiKeys } from "./api-keys";
75
76
  import { handleApprovalRequests } from "./approval-requests";
@@ -147,6 +148,7 @@ import {
147
148
  getPathSegments,
148
149
  httpServerSemconvAttributes,
149
150
  parseQueryParams,
151
+ redactShareTokenInPath,
150
152
  safeRequestUrlForLog,
151
153
  setCorsHeaders,
152
154
  warnIfCorsAllowsAnyOrigin,
@@ -319,7 +321,7 @@ const httpServer = createHttpServer(async (req, res) => {
319
321
  spanName,
320
322
  {
321
323
  "http.request.method": req.method ?? "",
322
- "url.path": reqPath,
324
+ "url.path": redactShareTokenInPath(reqPath),
323
325
  "url.scheme": semconv["url.scheme"],
324
326
  "http.route": httpRoute,
325
327
  "server.address": semconv["server.address"],
@@ -373,6 +375,7 @@ const httpServer = createHttpServer(async (req, res) => {
373
375
  () => handlePoolKeys(req, res, pathSegments, queryParams),
374
376
  () => handleConfig(req, res, pathSegments, queryParams),
375
377
  () => handleFs(req, res, pathSegments, queryParams, myAgentId),
378
+ () => handleAgentFsProxy(req, res),
376
379
  () => handleComb(req, res, pathSegments, queryParams, myAgentId),
377
380
  () => handleKv(req, res, pathSegments, queryParams),
378
381
  () => handleRooms(req, res, pathSegments, queryParams),
@@ -517,7 +520,7 @@ async function shutdown() {
517
520
  await stopChatSdk();
518
521
 
519
522
  // Stop OAuth keepalive
520
- if (process.env.OAUTH_KEEPALIVE_DISABLE !== "true") {
523
+ if (!isEnvFlagEnabled("OAUTH_KEEPALIVE_DISABLE", false)) {
521
524
  const { stopOAuthKeepalive } = await import("../oauth/keepalive");
522
525
  await stopOAuthKeepalive();
523
526
  }
@@ -847,7 +850,7 @@ httpServer
847
850
  startRuntimeSupervisor();
848
851
 
849
852
  // Start OAuth token keepalive (proactive refresh to prevent expiry)
850
- if (process.env.OAUTH_KEEPALIVE_DISABLE !== "true") {
853
+ if (!isEnvFlagEnabled("OAUTH_KEEPALIVE_DISABLE", false)) {
851
854
  const { startOAuthKeepalive } = await import("../oauth/keepalive");
852
855
  startOAuthKeepalive();
853
856
  }
@@ -855,7 +858,7 @@ httpServer
855
858
  // Start generic OAuth token refresh sweep (all oauth_apps providers,
856
859
  // 15-min tick, first run ~1 min after boot). Complements the tracker-only
857
860
  // keepalive above — see src/be/oauth-refresh-sweep.ts.
858
- if (process.env.OAUTH_REFRESH_SWEEP_DISABLE !== "true") {
861
+ if (!isEnvFlagEnabled("OAUTH_REFRESH_SWEEP_DISABLE", false)) {
859
862
  const { startOAuthRefreshSweep } = await import("../be/oauth-refresh-sweep");
860
863
  startOAuthRefreshSweep();
861
864
  }
@@ -914,7 +914,7 @@ export async function handleMemory(
914
914
  try {
915
915
  const provider = getEmbeddingProvider();
916
916
  const store = getMemoryStore();
917
- const queryEmbedding = await provider.embed(query);
917
+ const queryEmbedding = await provider.embed(query, { kind: "query" });
918
918
 
919
919
  const candidateLimit = Math.min(limit, 20) * CANDIDATE_SET_MULTIPLIER;
920
920
  const candidates = await searchMemoryCandidates(
@@ -1039,7 +1039,7 @@ export async function handleMemory(
1039
1039
  try {
1040
1040
  if (query && query.trim().length > 0) {
1041
1041
  const provider = getEmbeddingProvider();
1042
- const queryEmbedding = await provider.embed(query.trim());
1042
+ const queryEmbedding = await provider.embed(query.trim(), { kind: "query" });
1043
1043
 
1044
1044
  const candidateLimit = Math.min(
1045
1045
  4096,
@@ -1289,7 +1289,7 @@ export async function handleMemory(
1289
1289
  });
1290
1290
  if (result.changed) {
1291
1291
  const provider = getEmbeddingProvider();
1292
- const embedding = await provider.embed(result.memory.content);
1292
+ const embedding = await provider.embed(result.memory.content, { kind: "document" });
1293
1293
  if (embedding) await store.updateEmbedding(result.memory.id, embedding, provider.name);
1294
1294
  try {
1295
1295
  // Edit path: prune links derived from removed content (sequel links survive).
@@ -1378,7 +1378,10 @@ export async function handleMemory(
1378
1378
  for (let i = 0; i < memories.length; i += batchSize) {
1379
1379
  const batch = memories.slice(i, i + batchSize);
1380
1380
  try {
1381
- const embeddings = await provider.embedBatch(batch.map((m) => m.content));
1381
+ const embeddings = await provider.embedBatch(
1382
+ batch.map((m) => m.content),
1383
+ { kind: "document" },
1384
+ );
1382
1385
  for (let j = 0; j < embeddings.length; j++) {
1383
1386
  if (embeddings[j]) {
1384
1387
  await store.updateEmbedding(batch[j]!.id, embeddings[j]!, provider.name);
@@ -2,12 +2,15 @@ import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import OpenAI from "openai";
3
3
  import { getAutomationSetupStates } from "../be/automation-preflight";
4
4
  import { getAgentHarnessProviders, getReadyAgentCounts, getTaskById } from "../be/db";
5
- import { getEmbeddingProvider } from "../be/memory";
5
+ import { getEmbeddingProvider, resetEmbeddingProvider } from "../be/memory";
6
6
  import { EMBEDDING_DIMENSIONS } from "../be/memory/constants";
7
7
  import {
8
+ EMBEDDING_PREFIX_KEYS,
9
+ type MemoryConfigChange,
8
10
  OnboardingActionSchema,
9
11
  type OnboardingErrorClass,
10
12
  type OnboardingMemoryPreset,
13
+ type OnboardingMemoryProvider,
11
14
  type OnboardingMemoryRequest,
12
15
  OnboardingMemoryRequestSchema,
13
16
  type OnboardingMemoryResponse,
@@ -21,16 +24,89 @@ import {
21
24
  } from "../be/onboarding";
22
25
  import { assertUrlSafe, publicEndpointSsrfOptions } from "../oauth/mcp-wrapper";
23
26
  import { ProviderNameSchema } from "../types";
27
+ import { isCombEnabled } from "../utils/constants";
24
28
  import { splitCredentialPool } from "../utils/credentials";
29
+ import { isPrivateAddress } from "../utils/egress-guard";
25
30
  import { getOpenRouterAttributionHeaders } from "../utils/openrouter-base-url";
26
31
  import { ensureConfigAdmin } from "./config";
27
- import { scheduleIntegrationsReload } from "./core";
32
+ import {
33
+ getDeploymentEnvValue,
34
+ loadGlobalConfigsIntoEnv,
35
+ scheduleIntegrationsReload,
36
+ } from "./core";
28
37
  import { route } from "./route-def";
29
38
  import { rollupCredStatusForProvider } from "./status";
30
39
  import { jsonError } from "./utils";
31
40
 
32
41
  export type { OnboardingState } from "../be/onboarding";
33
42
 
43
+ /**
44
+ * The embeddings endpoint runs on this server or its private network: a
45
+ * loopback name, a private IP, or a single-label host such as the bundled
46
+ * `tei` service. Unset means the hosted OpenAI default.
47
+ */
48
+ function isLocalEndpoint(baseUrl: string | undefined): boolean {
49
+ if (!baseUrl?.trim()) return false;
50
+ let host: string;
51
+ try {
52
+ host = new URL(baseUrl.trim()).hostname.toLowerCase().replace(/\.$/, "");
53
+ } catch {
54
+ return false;
55
+ }
56
+ if (!host) return false;
57
+ if (host === "localhost" || host.endsWith(".localhost")) return true;
58
+ if (isPrivateAddress(host)) return true;
59
+ // IPv6 literals hold colons: only a plain name can be a single label.
60
+ return !host.includes(".") && !host.includes(":");
61
+ }
62
+
63
+ /** The hosts of the hosted memory presets (the UI's preset list). */
64
+ const PRESET_HOSTS: ReadonlyArray<[OnboardingMemoryProvider, (host: string) => boolean]> = [
65
+ ["openai", (host) => host === "api.openai.com"],
66
+ ["openrouter", (host) => host === "openrouter.ai"],
67
+ ["vercel", (host) => host === "ai-gateway.vercel.sh"],
68
+ [
69
+ "azure",
70
+ (host) =>
71
+ [".services.ai.azure.com", ".openai.azure.com", ".cognitiveservices.azure.com"].some(
72
+ (suffix) => host.endsWith(suffix),
73
+ ),
74
+ ],
75
+ ];
76
+
77
+ /** Where an embeddings endpoint runs, from its host. Unset means the hosted OpenAI default. */
78
+ function embeddingsProvider(baseUrl: string | undefined): OnboardingMemoryProvider {
79
+ if (isLocalEndpoint(baseUrl)) return "local";
80
+ if (!baseUrl?.trim()) return "openai";
81
+ let host: string;
82
+ try {
83
+ host = new URL(baseUrl.trim()).hostname.toLowerCase().replace(/\.$/, "");
84
+ } catch {
85
+ return "custom";
86
+ }
87
+ return PRESET_HOSTS.find(([, matches]) => matches(host))?.[0] ?? "custom";
88
+ }
89
+
90
+ /**
91
+ * The deployment environment's own embeddings setup, before a stored config
92
+ * replaced it: its base URL, model, and key. The key can also be a stored
93
+ * OPENAI_API_KEY, as for the setup the server runs.
94
+ */
95
+ function deploymentEmbeddings() {
96
+ return {
97
+ baseUrl: getDeploymentEnvValue("EMBEDDING_API_BASE_URL"),
98
+ model: getDeploymentEnvValue("EMBEDDING_MODEL") ?? "text-embedding-3-small",
99
+ apiKey: getDeploymentEnvValue("EMBEDDING_API_KEY") ?? process.env.OPENAI_API_KEY,
100
+ };
101
+ }
102
+
103
+ /** The local setup of the deployment environment, when it has a local endpoint and a key. */
104
+ function localDefault(): { model: string } | null {
105
+ const deployment = deploymentEmbeddings();
106
+ if (!isLocalEndpoint(deployment.baseUrl) || !deployment.apiKey) return null;
107
+ return { model: deployment.model };
108
+ }
109
+
34
110
  async function buildSignals(state: OnboardingState): Promise<OnboardingSignals> {
35
111
  const providers: OnboardingSignals["providers"] = [];
36
112
  for (const entry of await getAgentHarnessProviders()) {
@@ -51,12 +127,18 @@ async function buildSignals(state: OnboardingState): Promise<OnboardingSignals>
51
127
  const integrations = await getAutomationSetupStates();
52
128
  const agentCounts = await getReadyAgentCounts();
53
129
  const firstTask = state.firstTaskId ? await getTaskById(state.firstTaskId) : null;
130
+ const embeddingsOn = getEmbeddingProvider().isConfigured();
54
131
 
55
132
  return {
56
133
  providers,
57
134
  embeddings: {
58
- configured: getEmbeddingProvider().isConfigured(),
135
+ configured: embeddingsOn,
59
136
  dimensions: EMBEDDING_DIMENSIONS,
137
+ // The model id and the host class only: never the key or the URL.
138
+ model: embeddingsOn ? (process.env.EMBEDDING_MODEL ?? "text-embedding-3-small") : null,
139
+ local: embeddingsOn && isLocalEndpoint(process.env.EMBEDDING_API_BASE_URL),
140
+ provider: embeddingsOn ? embeddingsProvider(process.env.EMBEDDING_API_BASE_URL) : null,
141
+ localDefault: localDefault(),
60
142
  },
61
143
  integrations: {
62
144
  slack: integrations.slack === "configured",
@@ -71,6 +153,7 @@ async function buildSignals(state: OnboardingState): Promise<OnboardingSignals>
71
153
  workersOnline: agentCounts.workers,
72
154
  },
73
155
  firstTask: firstTask ? { id: firstTask.id, status: firstTask.status } : null,
156
+ files: { enabled: isCombEnabled() },
74
157
  };
75
158
  }
76
159
 
@@ -195,10 +278,31 @@ function memoryErrorMessage(errorClass: OnboardingErrorClass, status?: number):
195
278
  async function updateMemoryState(
196
279
  preset: OnboardingMemoryPreset,
197
280
  result: { ok: true } | { ok: false; errorClass: OnboardingErrorClass },
198
- saveConfig?: { baseUrl: string; model: string; apiKey?: string },
281
+ change?: MemoryConfigChange,
199
282
  ): Promise<void> {
200
- const configSaved = await updateOnboardingMemory(preset, result, saveConfig);
201
- if (configSaved) scheduleIntegrationsReload();
283
+ const configChanged = await updateOnboardingMemory(preset, result, change);
284
+ if (!configChanged) return;
285
+ if (change?.kind === "restore") {
286
+ // Apply the deployment setup before the response: the next GET shows it
287
+ // in use. Memory search stays on, so the reload starts no backfill.
288
+ await loadGlobalConfigsIntoEnv(true);
289
+ resetEmbeddingProvider();
290
+ }
291
+ scheduleIntegrationsReload();
292
+ }
293
+
294
+ /**
295
+ * The task prefixes of a saved setup. A prefix of the deployment environment
296
+ * belongs to its model (nomic needs `search_query: `): another model gets an
297
+ * empty stored prefix over it, and the same model gets the deployment prefix
298
+ * back (the stored row is deleted).
299
+ */
300
+ function prefixChanges(model: string): Extract<MemoryConfigChange, { kind: "save" }>["prefixes"] {
301
+ const sameModel = model === getDeploymentEnvValue("EMBEDDING_MODEL");
302
+ return EMBEDDING_PREFIX_KEYS.filter((key) => getDeploymentEnvValue(key)).map((key) => ({
303
+ key,
304
+ value: sameModel ? null : "",
305
+ }));
202
306
  }
203
307
 
204
308
  async function handleMemoryProbe(body: OnboardingMemoryRequest): Promise<OnboardingMemoryResponse> {
@@ -207,21 +311,39 @@ async function handleMemoryProbe(body: OnboardingMemoryRequest): Promise<Onboard
207
311
  body.preset === "openai" || body.preset === "openrouter" || body.preset === "vercel"
208
312
  ? MEMORY_PRESETS[body.preset]
209
313
  : undefined;
314
+ // `existing` tests the setup the server runs. With `restoreDefault` it
315
+ // tests the deployment environment's setup, which applies once the stored
316
+ // config is gone.
317
+ const restore = body.preset === "existing" && body.restoreDefault === true;
318
+ const deployment = restore ? deploymentEmbeddings() : null;
210
319
  const baseUrl =
211
320
  body.preset === "existing"
212
- ? process.env.EMBEDDING_API_BASE_URL
321
+ ? deployment
322
+ ? deployment.baseUrl
323
+ : process.env.EMBEDDING_API_BASE_URL
213
324
  : (body.baseUrl ??
214
325
  preset?.baseUrl ??
215
326
  process.env.EMBEDDING_API_BASE_URL ??
216
327
  "https://api.openai.com/v1");
217
328
  const model =
218
329
  body.preset === "existing"
219
- ? (process.env.EMBEDDING_MODEL ?? "text-embedding-3-small")
330
+ ? (deployment?.model ?? process.env.EMBEDDING_MODEL ?? "text-embedding-3-small")
220
331
  : (body.model ?? preset?.model ?? process.env.EMBEDDING_MODEL ?? "text-embedding-3-small");
221
332
 
333
+ // Preset `existing` tests the server's own EMBEDDING_API_BASE_URL. When it is
334
+ // the deployment environment's value, the URL is operator config, not request
335
+ // input, so a private endpoint (a local TEI at http://tei:80/v1) is allowed. A
336
+ // value stored through `/api/config` and every URL that can come from the
337
+ // request still pass the SSRF check.
338
+ const configuredEndpoint =
339
+ body.preset === "existing" &&
340
+ !!baseUrl &&
341
+ baseUrl === getDeploymentEnvValue("EMBEDDING_API_BASE_URL");
222
342
  let endpoint: URL;
223
343
  try {
224
- endpoint = assertUrlSafe(baseUrl ?? "", publicEndpointSsrfOptions());
344
+ endpoint = configuredEndpoint
345
+ ? new URL(baseUrl ?? "")
346
+ : assertUrlSafe(baseUrl ?? "", publicEndpointSsrfOptions());
225
347
  } catch {
226
348
  // Input rejections are not probe outcomes: the step state stays as is.
227
349
  return {
@@ -243,7 +365,9 @@ async function handleMemoryProbe(body: OnboardingMemoryRequest): Promise<Onboard
243
365
  !explicitKey && body.reuseKey && canReuseKey ? process.env[body.reuseKey] : undefined;
244
366
  const implicitKey =
245
367
  body.preset === "existing"
246
- ? (process.env.EMBEDDING_API_KEY ?? process.env.OPENAI_API_KEY)
368
+ ? deployment
369
+ ? deployment.apiKey
370
+ : (process.env.EMBEDDING_API_KEY ?? process.env.OPENAI_API_KEY)
247
371
  : undefined;
248
372
  // Test and save the same pool member.
249
373
  const apiKey = splitCredentialPool(explicitKey ?? reusedKey ?? implicitKey ?? "")[0];
@@ -290,11 +414,15 @@ async function handleMemoryProbe(body: OnboardingMemoryRequest): Promise<Onboard
290
414
  body.preset,
291
415
  { ok: true },
292
416
  body.preset === "existing"
293
- ? undefined
417
+ ? restore
418
+ ? { kind: "restore" }
419
+ : undefined
294
420
  : {
421
+ kind: "save",
295
422
  baseUrl: baseUrl ?? "https://api.openai.com/v1",
296
423
  model,
297
424
  apiKey: keyToSave,
425
+ prefixes: prefixChanges(model),
298
426
  },
299
427
  );
300
428
  return { ok: true, dimensions, latencyMs };
@@ -308,13 +308,45 @@ function getAppBaseUrl(): string {
308
308
  return getAppUrl(LOCAL_PAGE_APP_URL);
309
309
  }
310
310
 
311
+ // CSP host-source without a path: optional http(s) scheme, a host that may
312
+ // start with a `*.` wildcard label, and an optional port (digits or `*`).
313
+ // Bare `*`, keywords (`'none'`, `'self'`) and anything carrying `;` or a path
314
+ // are rejected so a bad env value cannot widen or break the policy.
315
+ const FRAME_ANCESTOR_SOURCE_RE =
316
+ /^(?:https?:\/\/)?(?:\*\.)?[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*(?::(?:\d{1,5}|\*))?$/i;
317
+
318
+ let cachedFrameAncestors: { raw: string; origins: string[] } | null = null;
319
+
320
+ /**
321
+ * Parse `PAGES_FRAME_ANCESTORS`: extra origins allowed to iframe PUBLIC pages
322
+ * (for example a marketing site embedding a chart). Comma or space separated.
323
+ * Invalid entries are dropped with one warning per distinct env value; the
324
+ * parse is cached on the raw string so a config reload picks up changes
325
+ * without re-warning on every request.
326
+ */
327
+ export function getPageFrameAncestors(): string[] {
328
+ const raw = process.env.PAGES_FRAME_ANCESTORS ?? "";
329
+ if (cachedFrameAncestors?.raw === raw) return cachedFrameAncestors.origins;
330
+ const origins: string[] = [];
331
+ for (const entry of raw.split(/[\s,]+/)) {
332
+ const value = entry.replace(/\/+$/, "");
333
+ if (!value) continue;
334
+ if (FRAME_ANCESTOR_SOURCE_RE.test(value)) origins.push(value);
335
+ else console.warn(`[pages] ignoring invalid PAGES_FRAME_ANCESTORS entry: ${entry}`);
336
+ }
337
+ cachedFrameAncestors = { raw, origins };
338
+ return origins;
339
+ }
340
+
311
341
  /**
312
342
  * Build the `Content-Security-Policy` for the served HTML. Allows inline
313
343
  * scripts (required for `BROWSER_SDK_JS`) but locks down everything else to
314
344
  * `'self'`. The SPA iframes the page in step-6 with `sandbox="allow-scripts
315
- * allow-forms"`; the CSP is a defence-in-depth layer.
345
+ * allow-forms"`; the CSP is a defence-in-depth layer. `PAGES_FRAME_ANCESTORS`
346
+ * applies only to public pages: authed and password pages stay embeddable by
347
+ * the app origins alone.
316
348
  */
317
- function buildCsp(): string {
349
+ function buildCsp(page: Page): string {
318
350
  // `frame-ancestors` lists every origin allowed to iframe `/p/:id`. We must
319
351
  // include the SPA origin(s). Configured app URLs may be comma-separated so
320
352
  // portless dev (`https://ui.swarm.localhost`), a Vite port (`http://localhost:5274`),
@@ -333,7 +365,8 @@ function buildCsp(): string {
333
365
  "https://*.localhost",
334
366
  "http://*.localhost",
335
367
  ];
336
- const ancestors = Array.from(new Set([...configured, ...devFallbacks]));
368
+ const embedders = page.authMode === "public" ? getPageFrameAncestors() : [];
369
+ const ancestors = Array.from(new Set([...configured, ...devFallbacks, ...embedders]));
337
370
  // Allow Tailwind Play CDN (`cdn.tailwindcss.com`) for scripts, Google
338
371
  // Fonts (`fonts.googleapis.com` stylesheets + `fonts.gstatic.com` font
339
372
  // files) for the swarm default typography, and same-origin /@swarm/api/*
@@ -609,7 +642,7 @@ export async function handlePagesPublic(
609
642
  const headers: Record<string, string | string[]> = {
610
643
  "Content-Type": "text/html; charset=utf-8",
611
644
  "Cache-Control": "no-store",
612
- "Content-Security-Policy": buildCsp(),
645
+ "Content-Security-Policy": buildCsp(page),
613
646
  "X-Content-Type-Options": "nosniff",
614
647
  };
615
648
  if (inlineSetCookie.length > 0) headers["Set-Cookie"] = inlineSetCookie;
@@ -642,7 +675,7 @@ export async function handlePagesPublic(
642
675
  const headers: Record<string, string | string[]> = {
643
676
  "Content-Type": "text/html; charset=utf-8",
644
677
  "Cache-Control": "no-store",
645
- "Content-Security-Policy": buildCsp(),
678
+ "Content-Security-Policy": buildCsp(page),
646
679
  // Defence-in-depth: prevent MIME sniffing and clickjacking outside the SPA.
647
680
  "X-Content-Type-Options": "nosniff",
648
681
  };