@desplega.ai/agent-swarm 2.7.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 (236) hide show
  1. package/dist/{acp-adapter-ma1js5n7.js → acp-adapter-8dn5tq56.js} +6 -6
  2. package/dist/{actions-1vhmtvg9.js → actions-3r6gw7dy.js} +18 -18
  3. package/dist/{amp-adapter-sxqqvmdk.js → amp-adapter-wjtq413p.js} +3 -3
  4. package/dist/{anthropic-messages-txs190dd.js → anthropic-messages-n4cby1rz.js} +4 -4
  5. package/dist/{app-mwx1d5a9.js → app-d53yhykk.js} +3 -3
  6. package/dist/{assistant-yx1p6g82.js → assistant-ksrawj3k.js} +20 -20
  7. package/dist/{boot-reembed-7h78vmvv.js → boot-reembed-fxfw17m2.js} +10 -3
  8. package/dist/{boot-reembed-5h37a2c2.js → boot-reembed-k5y5xbj0.js} +15 -8
  9. package/dist/{boot-scrub-sweep-8mawd4gw.js → boot-scrub-sweep-se41j9fd.js} +3 -3
  10. package/dist/{boot-seeds-ca84dhnn.js → boot-seeds-r3kwwv8b.js} +4 -4
  11. package/dist/{claude-adapter-wqt4hfkq.js → claude-adapter-ahmyf231.js} +7 -7
  12. package/dist/{claude-managed-adapter-6fjqtr2z.js → claude-managed-adapter-5knamegc.js} +3 -3
  13. package/dist/{claude-managed-setup-rqv98sea.js → claude-managed-setup-mdc7dy6b.js} +2 -2
  14. package/dist/{claude-sdk-session-f29f3b2x.js → claude-sdk-session-n5wen9zk.js} +8728 -8599
  15. package/dist/{cli-m5e802r7.js → cli-0dqtv2jg.js} +2 -2
  16. package/dist/{cli-e1595bgx.js → cli-0tww7dnz.js} +2 -2
  17. package/dist/{cli-2y7v133v.js → cli-0w4e4v58.js} +7 -7
  18. package/dist/{cli-d20qvpr2.js → cli-2sjm3t4r.js} +1 -1
  19. package/dist/{cli-gf2850q7.js → cli-3cywb44q.js} +18 -1
  20. package/dist/{cli-8wkb09q5.js → cli-3k9jej0c.js} +2 -2
  21. package/dist/{cli-twctnkng.js → cli-3v2cyqb8.js} +3 -3
  22. package/dist/{cli-he9gbdx0.js → cli-5qt9vzmq.js} +39 -20
  23. package/dist/{cli-a6ds2epe.js → cli-5rp1w4cr.js} +1 -1
  24. package/dist/{cli-6srv653c.js → cli-6vwjsetx.js} +4 -4
  25. package/dist/{cli-1hhbzkbb.js → cli-6w2yrrc0.js} +8 -8
  26. package/dist/{cli-f12mr20z.js → cli-75tdydgv.js} +1 -1
  27. package/dist/{cli-j3y0x2n4.js → cli-8feswf2v.js} +43 -33
  28. package/dist/{cli-07q03xm2.js → cli-8wd3mbyj.js} +1 -1
  29. package/dist/{cli-ymjpkxgn.js → cli-9csfthbw.js} +8 -6
  30. package/dist/{cli-f1e0ptjj.js → cli-b41p764a.js} +21 -2
  31. package/dist/{cli-kv7sbc2c.js → cli-c1t533hh.js} +3 -3
  32. package/dist/{cli-54sj64fc.js → cli-c28jxpcv.js} +1 -1
  33. package/dist/{cli-8mbc9za9.js → cli-cb50g9cd.js} +1 -1
  34. package/dist/{cli-c668vvcf.js → cli-d8gaphtr.js} +1 -1
  35. package/dist/{cli-86ehn4s5.js → cli-dgtfysne.js} +2 -2
  36. package/dist/{cli-sjctjr48.js → cli-dscnhn18.js} +1 -1
  37. package/dist/{cli-j0ppywdr.js → cli-dv11pgpv.js} +12 -12
  38. package/dist/{cli-1m9x8dr5.js → cli-ekptgk2m.js} +5 -5
  39. package/dist/{cli-9gsm2rzp.js → cli-f3mzy662.js} +17 -17
  40. package/dist/{cli-s0mgx214.js → cli-fk7k8s9x.js} +2 -2
  41. package/dist/{cli-vbhgkw9h.js → cli-h3e7bhvn.js} +3 -3
  42. package/dist/{cli-yd2nreqw.js → cli-hkxqdmnb.js} +2 -2
  43. package/dist/{cli-8wmj18cz.js → cli-hpkvgdad.js} +7 -4
  44. package/dist/{cli-wqr7cea4.js → cli-hq2v4rr5.js} +37 -9
  45. package/dist/{cli-sgqm2582.js → cli-k1zb7raw.js} +5 -5
  46. package/dist/{cli-4rvr7zfx.js → cli-k4awgvzm.js} +5 -0
  47. package/dist/{cli-ay40dnj3.js → cli-k7d3hvya.js} +2 -2
  48. package/dist/{cli-j9bpmykh.js → cli-kbtdftbw.js} +5 -5
  49. package/dist/{cli-x37r7qwf.js → cli-kj69b9tw.js} +25 -5
  50. package/dist/{cli-gsfk58nw.js → cli-kk9jy4nm.js} +1 -1
  51. package/dist/{cli-39aet655.js → cli-m52t4cta.js} +1 -1
  52. package/dist/{cli-h5jeg46p.js → cli-mdadewqp.js} +5 -5
  53. package/dist/{cli-a89zdqz9.js → cli-nf6k60sn.js} +1 -1
  54. package/dist/{cli-v6shg7na.js → cli-nzdf09js.js} +1 -1
  55. package/dist/{cli-rr7zx19j.js → cli-prcyzecg.js} +12 -12
  56. package/dist/{cli-6a9qfa8b.js → cli-pzafyqtk.js} +118 -48
  57. package/dist/{cli-48gxjmcz.js → cli-qvaa1gjp.js} +87 -24
  58. package/dist/{cli-qj442fws.js → cli-rg4b2g6j.js} +1 -1
  59. package/dist/{cli-aypd09nf.js → cli-rm66vqdz.js} +1 -1
  60. package/dist/{cli-9pshx6rd.js → cli-s504c3w7.js} +1 -1
  61. package/dist/{cli-ptfq202x.js → cli-swkx9jm2.js} +3 -3
  62. package/dist/{cli-m30k9777.js → cli-thsaq4pd.js} +16 -16
  63. package/dist/{cli-60sty27p.js → cli-twjnyvs8.js} +2 -2
  64. package/dist/{cli-rd5m70gj.js → cli-typdpe8g.js} +4 -4
  65. package/dist/{cli-yxbsmx56.js → cli-vg8pwmsd.js} +6 -6
  66. package/dist/{cli-s09j9vp5.js → cli-vhpckzvp.js} +1 -1
  67. package/dist/{cli-dv97kdzv.js → cli-wdxgsdz5.js} +2 -2
  68. package/dist/{cli-7a6p4e65.js → cli-wtjg6je3.js} +1 -1
  69. package/dist/{cli-b3yzyc57.js → cli-x3cngb1h.js} +44 -12
  70. package/dist/{cli-6mgdba42.js → cli-xbz6mcg4.js} +2 -2
  71. package/dist/{cli-2pvzmas1.js → cli-y4tdxsqb.js} +3 -3
  72. package/dist/{cli-f4dbh7a5.js → cli-yqrsp3ny.js} +7 -7
  73. package/dist/cli.js +27 -19
  74. package/dist/{codex-adapter-zk59x2zh.js → codex-adapter-7wepzq15.js} +5 -5
  75. package/dist/{codex-hook-xyjhvxwd.js → codex-hook-pbdh4xhx.js} +2 -2
  76. package/dist/{codex-session-runner-kh6s4j3n.js → codex-session-runner-02sb49e4.js} +5 -5
  77. package/dist/{commands-68et1qv2.js → commands-3rcspxct.js} +2 -2
  78. package/dist/{controller-fyrbnzg4.js → controller-8ar4avd6.js} +3 -3
  79. package/dist/{controllers-connect-nep9h2xv.js → controllers-connect-ajvh9h0v.js} +19 -19
  80. package/dist/{cursor-adapter-yss3pg7t.js → cursor-adapter-gd0sa7bh.js} +1 -1
  81. package/dist/{db-28gjyvxj.js → db-dv9j7ts5.js} +2 -2
  82. package/dist/{db-8ftwvzba.js → db-z26vgdvj.js} +3 -3
  83. package/dist/{dispatcher-hayexmkr.js → dispatcher-8m0htjq8.js} +17 -17
  84. package/dist/{dsh-adapter-46wawff9.js → dsh-adapter-y8gyk130.js} +1 -1
  85. package/dist/{e2b-fn3zacvt.js → e2b-74vg9b7e.js} +1 -1
  86. package/dist/{e2b-222hsajc.js → e2b-jyay8wzn.js} +1 -1
  87. package/dist/{easter-egg-3d-8d1e0h4m.js → easter-egg-3d-28f880xx.js} +1 -1
  88. package/dist/{execute-cjrfxfnm.js → execute-9seb79vh.js} +1 -1
  89. package/dist/{external-channel-xbmxcg09.js → external-channel-smz7jyth.js} +2 -2
  90. package/dist/{getMachineId-bsd-brn0xym3.js → getMachineId-bsd-whfre6gr.js} +3 -3
  91. package/dist/{getMachineId-darwin-t339sfxm.js → getMachineId-darwin-5zjynb19.js} +3 -3
  92. package/dist/{getMachineId-linux-5eq95d8b.js → getMachineId-linux-8mksrrsq.js} +3 -3
  93. package/dist/{getMachineId-unsupported-qavrtc7j.js → getMachineId-unsupported-fxtqsdt2.js} +3 -3
  94. package/dist/{getMachineId-win-q7fe1wx7.js → getMachineId-win-nzfmz857.js} +3 -3
  95. package/dist/{grok-adapter-ag4wks2a.js → grok-adapter-z0nnvtbn.js} +7 -7
  96. package/dist/handlers-pspryh6g.js +41 -0
  97. package/dist/{heartbeat-psfvcgwp.js → heartbeat-f928aqyp.js} +20 -20
  98. package/dist/{hook-cyq4mryn.js → hook-d7c605r4.js} +7 -7
  99. package/dist/{hook-8pz04697.js → hook-q9hv5xf9.js} +5 -5
  100. package/dist/{http-haj1nd6k.js → http-q6j4k989.js} +934 -180
  101. package/dist/{index-z5h22pes.js → index-5nb266jr.js} +17 -17
  102. package/dist/{index-ysd45kgc.js → index-8fxt5eah.js} +10 -10
  103. package/dist/{index-zgvqzjsw.js → index-cekzwhez.js} +5 -5
  104. package/dist/{index-zyq8ct52.js → index-ch0c1xff.js} +17 -17
  105. package/dist/{index-syvhnvtw.js → index-ebnjhxh1.js} +1 -1
  106. package/dist/{index-y1ajx04a.js → index-ed8dy8rn.js} +20 -20
  107. package/dist/{index-jwt2bj6f.js → index-ejqdkkqx.js} +11 -10
  108. package/dist/{index-dr94zbnh.js → index-farm1zsn.js} +32661 -29443
  109. package/dist/{index-q52f47pe.js → index-yn1yxg85.js} +1 -1
  110. package/dist/{keepalive-xdvsamvj.js → keepalive-zzc3hks1.js} +3 -3
  111. package/dist/{lead-hqg57kdp.js → lead-c0pjcaw4.js} +36 -36
  112. package/dist/{maintenance-4y2xzh3a.js → maintenance-kqsxzpzx.js} +1 -1
  113. package/dist/{oauth-refresh-sweep-jd4r7zbs.js → oauth-refresh-sweep-7qt0g55z.js} +2 -2
  114. package/dist/{onboard-18447q9q.js → onboard-k7x13fqg.js} +118 -5
  115. package/dist/{opencode-adapter-26b2frdf.js → opencode-adapter-tqvrtter.js} +5 -5
  116. package/dist/{otel-impl-f9tgr8yw.js → otel-impl-sqkf6k3b.js} +97 -97
  117. package/dist/{pi-mono-adapter-qn6qbga1.js → pi-mono-adapter-m2qnafxm.js} +63 -9
  118. package/dist/{pricing-refresh-dw2tgb53.js → pricing-refresh-wkpwhgvs.js} +3 -3
  119. package/dist/{rbac-roles-s4nwj2m3.js → rbac-roles-dpn0c0wj.js} +7 -4
  120. package/dist/{rbac-roles-aygm2760.js → rbac-roles-wk7xd6a6.js} +2 -2
  121. package/dist/{render-v2-yfvdfr0x.js → render-v2-ex2m4g2k.js} +3 -3
  122. package/dist/{resume-1bmvjbf3.js → resume-725g9b0b.js} +7 -7
  123. package/dist/{runtime-f7tpzedy.js → runtime-kajas15g.js} +1 -1
  124. package/dist/{session-access-r5garej6.js → session-access-0j2ezjje.js} +8 -4
  125. package/dist/{session-source-backfill-424wzv0r.js → session-source-backfill-62202awe.js} +2 -2
  126. package/dist/{setup-8tygvxkq.js → setup-9sj11ax6.js} +1 -1
  127. package/dist/{virtual-modules-xsw323b3.js → virtual-modules-c6fcq6zr.js} +2 -2
  128. package/dist/{work-objects-ggnghpgf.js → work-objects-0y07495y.js} +20 -20
  129. package/dist/{worker-sqjd1nqf.js → worker-d4q9sqbc.js} +36 -36
  130. package/openapi.json +1070 -9
  131. package/package.json +3 -3
  132. package/src/be/connector-codes.ts +65 -22
  133. package/src/be/memory/boot-reembed.ts +4 -1
  134. package/src/be/memory/constants.ts +6 -0
  135. package/src/be/memory/embedding-recovery.ts +48 -0
  136. package/src/be/memory/index-content.ts +5 -2
  137. package/src/be/memory/providers/openai-embedding.ts +65 -14
  138. package/src/be/memory/task-learning.ts +110 -8
  139. package/src/be/memory/types.ts +13 -2
  140. package/src/be/migrations/246_connector_code_kind.sql +11 -0
  141. package/src/be/onboarding.ts +125 -15
  142. package/src/be/rbac-roles.ts +4 -1
  143. package/src/be/scripts/embeddings.ts +2 -2
  144. package/src/be/seed/agent-fs-provision.ts +75 -14
  145. package/src/be/swarm-config-guard.ts +3 -0
  146. package/src/cli.tsx +9 -0
  147. package/src/comb/config.ts +6 -1
  148. package/src/commands/onboard/agent-fs-version.ts +5 -0
  149. package/src/commands/onboard/compose-generator.ts +112 -1
  150. package/src/commands/onboard/env-generator.ts +9 -0
  151. package/src/commands/onboard/tei-image.ts +12 -0
  152. package/src/commands/onboard/types.ts +7 -0
  153. package/src/commands/onboard.tsx +11 -1
  154. package/src/commands/runner.ts +14 -1
  155. package/src/http/agentfs-proxy.ts +369 -0
  156. package/src/http/all-routes.ts +1 -0
  157. package/src/http/codex-oauth-device.ts +8 -1
  158. package/src/http/fs.ts +64 -10
  159. package/src/http/inbox-state.ts +27 -1
  160. package/src/http/index.ts +4 -1
  161. package/src/http/memory.ts +7 -4
  162. package/src/http/onboarding.ts +139 -11
  163. package/src/http/session-access.ts +24 -0
  164. package/src/http/sessions.ts +10 -6
  165. package/src/http/tasks.ts +85 -11
  166. package/src/http/telemetry-ui.ts +1 -1
  167. package/src/http/user-roles.ts +4 -1
  168. package/src/http/users.ts +272 -3
  169. package/src/http/utils.ts +22 -3
  170. package/src/prompts/base-prompt.ts +24 -9
  171. package/src/prompts/session-templates.ts +26 -2
  172. package/src/providers/amp-adapter.ts +1 -1
  173. package/src/providers/codex-adapter.ts +6 -1
  174. package/src/providers/cursor-adapter.ts +1 -1
  175. package/src/providers/pi-mono-adapter.ts +58 -3
  176. package/src/rbac/legacy-policy.ts +1 -0
  177. package/src/rbac/permissions.ts +4 -0
  178. package/src/tests/agent-fs-provision-seeder.test.ts +261 -92
  179. package/src/tests/agentfs-proxy.test.ts +1106 -0
  180. package/src/tests/base-prompt.test.ts +69 -2
  181. package/src/tests/codex-adapter.test.ts +46 -8
  182. package/src/tests/codex-oauth-device.test.ts +12 -1
  183. package/src/tests/comb-config-guard.test.ts +20 -0
  184. package/src/tests/comb-links.test.ts +79 -1
  185. package/src/tests/comb-status.test.ts +44 -0
  186. package/src/tests/docs-index-footprint.test.js +3 -3
  187. package/src/tests/fixtures/onboard/baseline-states.ts +68 -0
  188. package/src/tests/fixtures/onboard/content.compose.yml.txt +190 -0
  189. package/src/tests/fixtures/onboard/content.env.txt +29 -0
  190. package/src/tests/fixtures/onboard/dev.compose.yml.txt +155 -0
  191. package/src/tests/fixtures/onboard/dev.env.txt +28 -0
  192. package/src/tests/fixtures/onboard/full-all.compose.yml.txt +582 -0
  193. package/src/tests/fixtures/onboard/full-all.env.txt +59 -0
  194. package/src/tests/fixtures/onboard/full.compose.yml.txt +435 -0
  195. package/src/tests/fixtures/onboard/full.env.txt +36 -0
  196. package/src/tests/fixtures/onboard/research.compose.yml.txt +155 -0
  197. package/src/tests/fixtures/onboard/research.env.txt +28 -0
  198. package/src/tests/fixtures/onboard/solo.compose.yml.txt +85 -0
  199. package/src/tests/fixtures/onboard/solo.env.txt +26 -0
  200. package/src/tests/http-log-scrubbing.test.ts +34 -1
  201. package/src/tests/http-task-write-ownership.test.ts +5 -1
  202. package/src/tests/invites-http.test.ts +464 -0
  203. package/src/tests/memory-embedding-input-cap.test.ts +122 -1
  204. package/src/tests/memory-embedding-recovery.test.ts +137 -0
  205. package/src/tests/onboard-compose.test.ts +142 -0
  206. package/src/tests/onboard-env.test.ts +24 -0
  207. package/src/tests/onboard-non-interactive.test.ts +29 -0
  208. package/src/tests/onboard-tei-image.test.ts +36 -0
  209. package/src/tests/onboarding.test.ts +451 -4
  210. package/src/tests/openai-embedding-provider.test.ts +105 -2
  211. package/src/tests/pi-mono-session-cost-on-abort.test.ts +270 -0
  212. package/src/tests/preload-env.test.ts +4 -0
  213. package/src/tests/preload.ts +4 -0
  214. package/src/tests/prompt-template-session.test.ts +19 -3
  215. package/src/tests/rbac-admission-e2e.test.ts +22 -6
  216. package/src/tests/rbac-admission.test.ts +342 -4
  217. package/src/tests/rbac-engine.test.ts +1 -0
  218. package/src/tests/rbac-roles.test.ts +9 -1
  219. package/src/tests/send-task-requester-gate.test.ts +64 -3
  220. package/src/tests/sessions-move.test.ts +39 -1
  221. package/src/tests/store-progress-learning.test.ts +95 -2
  222. package/src/tests/whoami-route.test.ts +41 -0
  223. package/src/tools/inject-learning.ts +1 -1
  224. package/src/tools/memory-edit.ts +1 -1
  225. package/src/tools/memory-search.ts +1 -1
  226. package/src/tools/store-progress.ts +5 -1
  227. package/src/tools/utils.ts +12 -1
  228. package/src/utils/constants.ts +39 -0
  229. package/src/utils/task-attachment-links.ts +15 -3
  230. package/templates/skills/swarm-docs/content.md +1 -1
  231. package/dist/handlers-tzmevqb5.js +0 -41
  232. package/dist/{azure-openai-responses-9kfwhmn7.js → azure-openai-responses-43asa23s.js} +0 -0
  233. package/dist/{node-vxx896fa.js → node-t28cc56b.js} +0 -0
  234. package/dist/{openai-codex-responses-79s8xnbh.js → openai-codex-responses-jhr3sjtm.js} +3 -3
  235. package/dist/{openai-completions-tdbyts0x.js → openai-completions-gs6yn37k.js} +3 -3
  236. /package/dist/{openai-responses-dm3t5vqp.js → openai-responses-jqm9vwwk.js} +0 -0
@@ -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 };
@@ -88,6 +88,30 @@ export async function hasSessionWrite(
88
88
  return hasSessionAccess(access, "write");
89
89
  }
90
90
 
91
+ export const NOT_YOUR_TASK_DENIED =
92
+ "You can only change tasks you requested, or sessions shared with you to edit.";
93
+
94
+ /**
95
+ * True when a user token without a grant-all role (RBAC on) changes a task it
96
+ * does not own: neither the task nor its session root names the user as
97
+ * requester, and the session is not shared with the user for writing. The
98
+ * session wall reads an ownerless root (Slack, schedules, operator key) as
99
+ * everyone's; for such a user it is not theirs to change. Operator, agent and
100
+ * admin callers are never denied here.
101
+ */
102
+ export async function deniesNonOwnerTaskWrite(
103
+ req: IncomingMessage,
104
+ task: { id: string; requestedByUserId?: string | null },
105
+ ): Promise<boolean> {
106
+ const auth = getRequestAuth(req);
107
+ if (auth?.kind !== "user" || !isRbacEnabled()) return false;
108
+ if (task.requestedByUserId === auth.userId) return false;
109
+ if ((await getUserGrant(auth.userId)).grantsAll) return false;
110
+ const root = await getSessionRoot(task.id);
111
+ if (!root?.requestedByUserId) return true;
112
+ return !hasSessionAccess(await sessionAccessForRoot(auth.userId, root), "write");
113
+ }
114
+
91
115
  /**
92
116
  * The parent gate shared by `POST /api/tasks` and `POST /api/apps/{id}/actions/{name}`.
93
117
  * The child inherits the parent's thread context, so an agent caller must own or
@@ -77,7 +77,13 @@ import { isRuntimePoolsEnabled } from "../utils/runtime-pools-flag";
77
77
  import { canMutateTaskNamespace } from "./assets";
78
78
  import { agentFirstPrincipal } from "./request-principal";
79
79
  import { route } from "./route-def";
80
- import { resolveSessionActor, type SessionActor, sessionAccessFor } from "./session-access";
80
+ import {
81
+ deniesNonOwnerTaskWrite,
82
+ NOT_YOUR_TASK_DENIED,
83
+ resolveSessionActor,
84
+ type SessionActor,
85
+ sessionAccessFor,
86
+ } from "./session-access";
81
87
  import { jsonError } from "./utils";
82
88
 
83
89
  // ─── Response Schemas ────────────────────────────────────────────────────────
@@ -660,7 +666,7 @@ const triageSessionListRoute = route({
660
666
  401: { description: "Unauthorized" },
661
667
  },
662
668
  auth: { apiKey: true },
663
- rbac: { ungated: "per-user inbox UI state, same posture as PATCH /api/inbox-state" },
669
+ rbac: { permission: "favorite.write.own" },
664
670
  });
665
671
 
666
672
  const SESSION_MOVE_LIMIT = 200;
@@ -671,10 +677,7 @@ const SessionMoveResultSchema = z.object({
671
677
  moved: z.array(z.string()),
672
678
  skipped: z.array(z.object({ rootTaskId: z.string(), reason: z.string() })),
673
679
  });
674
- const sessionMoveRbac = {
675
- ungated:
676
- "per-session access, per-task task.fs.mutate, and asset-key write checks in the handler, as PATCH /api/assets/task/{id}/key",
677
- };
680
+ const sessionMoveRbac = { permission: "task.fs.mutate" } as const;
678
681
  const sessionMoveResponses = {
679
682
  400: { description: "Invalid namespace or request" },
680
683
  403: { description: "Personal destination not authorized" },
@@ -759,6 +762,7 @@ async function checkSessionMove(
759
762
  const access = await sessionAccessFor(actor, root.id);
760
763
  if (access === "none") return { reason: SESSION_MOVE_UNAVAILABLE };
761
764
  if (!hasSessionAccess(access, "write")) return { reason: "Session write access required" };
765
+ if (await deniesNonOwnerTaskWrite(req, root)) return { reason: NOT_YOUR_TASK_DENIED };
762
766
  }
763
767
  if (root.parentTaskId) return { reason: "Task is not a session root" };
764
768
  const sourceError = sourceMoveError(options.destination ?? null, root);
package/src/http/tasks.ts CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  getAgentById,
12
12
  getAllTasks,
13
13
  getDbClient,
14
+ getInProgressTasksByContextKey,
14
15
  getLeadAgent,
15
16
  getLogsByTaskId,
16
17
  getPausedTasksForAgent,
@@ -39,6 +40,7 @@ import {
39
40
  } from "../be/db";
40
41
  import { modelProfileRefError, taskProfileError } from "../be/db/model-profiles";
41
42
  import { explicitModelErrorForAgent } from "../be/model-validation";
43
+ import { getUserGrant } from "../be/rbac-roles";
42
44
  import { hasSessionAccess, type SessionAccess } from "../be/session-shares";
43
45
  import {
44
46
  getTaskSteeringFields,
@@ -49,7 +51,7 @@ import {
49
51
  } from "../be/steering";
50
52
  import { getTaskCitations, TaskCitationSchema } from "../be/task-citations";
51
53
  import { findUserById } from "../be/users";
52
- import { can, type RbacPrincipal, type RbacResource } from "../rbac";
54
+ import { can, isRbacEnabled, type RbacPrincipal, type RbacResource } from "../rbac";
53
55
  import { chatThreadOfContextKey } from "../tasks/context-key";
54
56
  import { TaskCreationBlockedError } from "../tasks/errors";
55
57
  import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
@@ -87,7 +89,9 @@ import { buildTaskRequestedBy, TaskRequestedBySchema } from "./requested-by";
87
89
  import { bodyTask, route } from "./route-def";
88
90
  import {
89
91
  canReadSession,
92
+ deniesNonOwnerTaskWrite,
90
93
  denyParentAttach,
94
+ NOT_YOUR_TASK_DENIED,
91
95
  readableByFor,
92
96
  resolveSessionActor,
93
97
  SESSION_READ_DENIED,
@@ -279,6 +283,7 @@ const createTask = route({
279
283
  path: "/api/tasks",
280
284
  pattern: ["api", "tasks"],
281
285
  session: bodyTask("parentTaskId"),
286
+ rbac: { permission: "task.create.own" },
282
287
  summary: "Create a new task",
283
288
  tags: ["Tasks"],
284
289
  body: z
@@ -411,6 +416,7 @@ const cancelTaskRoute = route({
411
416
  path: "/api/tasks/{id}/cancel",
412
417
  pattern: ["api", "tasks", null, "cancel"],
413
418
  session: { taskParam: "id" },
419
+ rbac: { permission: "task.cancel.own" },
414
420
  summary: "Cancel a pending or in-progress task",
415
421
  tags: ["Tasks"],
416
422
  params: z.object({ id: z.string() }),
@@ -687,6 +693,7 @@ const pauseTaskRoute = route({
687
693
  path: "/api/tasks/{id}/pause",
688
694
  pattern: ["api", "tasks", null, "pause"],
689
695
  session: { taskParam: "id" },
696
+ rbac: { permission: "task.action.own" },
690
697
  summary: "Pause an in-progress task",
691
698
  tags: ["Tasks"],
692
699
  params: z.object({ id: z.string() }),
@@ -703,6 +710,7 @@ const resumeTaskRoute = route({
703
710
  path: "/api/tasks/{id}/resume",
704
711
  pattern: ["api", "tasks", null, "resume"],
705
712
  session: { taskParam: "id" },
713
+ rbac: { permission: "task.action.own" },
706
714
  summary: "Resume a paused task",
707
715
  tags: ["Tasks"],
708
716
  params: z.object({ id: z.string() }),
@@ -777,9 +785,7 @@ const updateTaskTitleRoute = route({
777
785
  404: { description: "Task not found" },
778
786
  },
779
787
  auth: { apiKey: true },
780
- rbac: {
781
- ungated: "mirrors the pre-RBAC PATCH /api/tasks/{id}/vcs sibling route: bearer auth only",
782
- },
788
+ rbac: { permission: "task.action.own" },
783
789
  });
784
790
 
785
791
  async function canSteerTask(
@@ -879,6 +885,40 @@ async function resolveTaskWritePrincipal(
879
885
  return { kind: "agent", agentId, isLead: agent?.isLead === true };
880
886
  }
881
887
 
888
+ /**
889
+ * May this caller bind a new task to a chat thread? The operator may. An agent needs the
890
+ * lead's chat post permission. A user needs admin, or a role with `integration.chat.post`.
891
+ */
892
+ async function mayBindChatThread(principal: RbacPrincipal): Promise<boolean> {
893
+ if (principal.kind === "operator") return true;
894
+ if (principal.kind === "user") {
895
+ if (!isRbacEnabled()) return true;
896
+ const grant = await getUserGrant(principal.userId);
897
+ return grant.grantsAll || grant.verbs.has("integration.chat.post");
898
+ }
899
+ return can({
900
+ principal,
901
+ verb: "integration.chat.post",
902
+ resource: { kind: "none" },
903
+ source: "http",
904
+ }).allow;
905
+ }
906
+
907
+ /**
908
+ * Sibling awareness copies the open tasks under a contextKey into the new task, and may pick
909
+ * one as its parent. A non-admin user may name a context only when they may write each of
910
+ * its open tasks.
911
+ */
912
+ async function deniesForeignContext(req: IncomingMessage, contextKey: string): Promise<boolean> {
913
+ const auth = getRequestAuth(req);
914
+ if (auth?.kind !== "user" || !isRbacEnabled()) return false;
915
+ if ((await getUserGrant(auth.userId)).grantsAll) return false;
916
+ for (const task of await getInProgressTasksByContextKey(contextKey)) {
917
+ if (await deniesNonOwnerTaskWrite(req, task)) return true;
918
+ }
919
+ return false;
920
+ }
921
+
882
922
  /**
883
923
  * Requester for a new task. Prefer trusted server-side identity: an
884
924
  * authenticated request user, or the caller's own ownership-gated task context
@@ -1070,17 +1110,27 @@ export async function handleTasks(
1070
1110
  return true;
1071
1111
  }
1072
1112
  // A chat-sdk contextKey binds the task to a chat thread that its assignee and creator
1073
- // may then read and post to. An agent may only name a thread other than the parent's
1074
- // with the lead's chat post permission.
1113
+ // may then read and post to. Only a caller with chat post permission may name a thread
1114
+ // other than the parent's.
1075
1115
  if (
1076
- principal.kind === "agent" &&
1077
1116
  parsed.body.contextKey &&
1078
1117
  parsed.body.contextKey !== parentTask?.contextKey &&
1079
1118
  chatThreadOfContextKey(parsed.body.contextKey) &&
1080
- !can({ principal, verb: "integration.chat.post", resource: { kind: "none" }, source: "http" })
1081
- .allow
1119
+ !(await mayBindChatThread(principal))
1082
1120
  ) {
1083
- jsonError(res, "Permission denied: only a lead can bind a task to a chat thread.", 403);
1121
+ jsonError(
1122
+ res,
1123
+ "Permission denied: only a lead or an admin can bind a task to a chat thread.",
1124
+ 403,
1125
+ );
1126
+ return true;
1127
+ }
1128
+ if (
1129
+ parsed.body.contextKey &&
1130
+ parsed.body.contextKey !== parentTask?.contextKey &&
1131
+ (await deniesForeignContext(req, parsed.body.contextKey))
1132
+ ) {
1133
+ jsonError(res, "You don't have access to the tasks in this context.", 403);
1084
1134
  return true;
1085
1135
  }
1086
1136
  let assetKey: string | undefined;
@@ -1240,7 +1290,12 @@ export async function handleTasks(
1240
1290
  return true;
1241
1291
  }
1242
1292
 
1243
- // Humans may cancel any task. Agents get the MCP cancel-task policy:
1293
+ if (await deniesNonOwnerTaskWrite(req, task)) {
1294
+ jsonError(res, NOT_YOUR_TASK_DENIED, 403);
1295
+ return true;
1296
+ }
1297
+
1298
+ // Admins may cancel any task. Agents get the MCP cancel-task policy:
1244
1299
  // lead or task creator.
1245
1300
  const principal = await resolveTaskWritePrincipal(req, myAgentId);
1246
1301
  if (
@@ -1882,6 +1937,11 @@ export async function handleTasks(
1882
1937
  return true;
1883
1938
  }
1884
1939
 
1940
+ if (await deniesNonOwnerTaskWrite(req, task)) {
1941
+ jsonError(res, NOT_YOUR_TASK_DENIED, 403);
1942
+ return true;
1943
+ }
1944
+
1885
1945
  if (myAgentId && task.agentId !== myAgentId) {
1886
1946
  jsonError(res, "Task belongs to another agent", 403);
1887
1947
  return true;
@@ -1945,6 +2005,15 @@ export async function handleTasks(
1945
2005
  if (updateTaskTitleRoute.match(req.method, pathSegments)) {
1946
2006
  const parsed = await updateTaskTitleRoute.parse(req, res, pathSegments, queryParams);
1947
2007
  if (!parsed) return true;
2008
+ const existing = await getTaskById(parsed.params.id);
2009
+ if (!existing) {
2010
+ jsonError(res, "Task not found", 404);
2011
+ return true;
2012
+ }
2013
+ if (await deniesNonOwnerTaskWrite(req, existing)) {
2014
+ jsonError(res, NOT_YOUR_TASK_DENIED, 403);
2015
+ return true;
2016
+ }
1948
2017
  const task = await updateTaskTitle(parsed.params.id, parsed.body.title);
1949
2018
  if (!task) {
1950
2019
  jsonError(res, "Task not found", 404);
@@ -1964,6 +2033,11 @@ export async function handleTasks(
1964
2033
  return true;
1965
2034
  }
1966
2035
 
2036
+ if (await deniesNonOwnerTaskWrite(req, task)) {
2037
+ jsonError(res, NOT_YOUR_TASK_DENIED, 403);
2038
+ return true;
2039
+ }
2040
+
1967
2041
  if (myAgentId && task.agentId !== myAgentId) {
1968
2042
  jsonError(res, "Task belongs to another agent", 403);
1969
2043
  return true;
@@ -32,7 +32,7 @@ const uiEvent = route({
32
32
  401: { description: "Unauthorized" },
33
33
  403: { description: "Guest page sessions cannot send events" },
34
34
  },
35
- rbac: { ungated: "any authenticated dashboard user; event name allowlisted" },
35
+ rbac: { permission: "favorite.write.own" },
36
36
  });
37
37
 
38
38
  const enableTelemetry = route({
@@ -80,7 +80,10 @@ async function composeRoles(userId: string) {
80
80
  }
81
81
 
82
82
  /** Operator or user only; agents never change human roles, even with the shared key. */
83
- async function ensureRoleManager(req: IncomingMessage, res: ServerResponse): Promise<boolean> {
83
+ export async function ensureRoleManager(
84
+ req: IncomingMessage,
85
+ res: ServerResponse,
86
+ ): Promise<boolean> {
84
87
  if (rejectGuest(req, res)) return false;
85
88
  const auth = getRequestAuth(req);
86
89
  if (auth?.kind !== "operator" && auth?.kind !== "user") {