@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
@@ -0,0 +1,369 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import { Readable } from "node:stream";
3
+ import { getApiKey } from "../utils/api-key";
4
+ import { isAgentFsProxyEnabled } from "../utils/constants";
5
+ import { resolveHttpRequestAuth } from "./auth";
6
+ import { clientIp, createIpRateLimiter } from "./ip-rate-limit";
7
+ import { route } from "./route-def";
8
+ import { jsonError, parseBody, RequestBodyTooLargeError } from "./utils";
9
+
10
+ /**
11
+ * Streaming pass-through to the agent-fs server at AGENT_FS_API_URL, active
12
+ * only when AGENT_FS_PROXY is on:
13
+ *
14
+ * - `/agentfs/*` -> agent-fs `/*` (the API Comb calls from the browser);
15
+ * - `/share/*` and `/site/*` -> agent-fs `/share/*` and `/site/*` unchanged.
16
+ * agent-fs share pages embed root-absolute `/share/<token>/raw` URLs and
17
+ * redirect to `/site/<token>/`, so they 404 behind the `/agentfs` prefix alone.
18
+ *
19
+ * agent-fs enforces its own `af_` bearer. The one swarm check is on
20
+ * `POST /agentfs/auth/register` (see `checkRegister`). Only an
21
+ * `Authorization: Bearer af_...` credential is forwarded (never a swarm API key
22
+ * or an `aswt_` token), plus an allowlist of other request headers (never
23
+ * Cookie or X-Agent-ID). On `/agentfs/*` the upstream CORS headers are dropped
24
+ * (the swarm's own setCorsHeaders applies); on `/share/*` and `/site/*` they
25
+ * are kept, because sandboxed site pages run in an opaque origin and need
26
+ * agent-fs's `Access-Control-Allow-Origin: *`. Both bodies are streamed.
27
+ */
28
+
29
+ const API_PREFIX = "/agentfs";
30
+ const PUBLIC_PREFIXES = ["/share/", "/site/"];
31
+
32
+ function upstreamBase(): string | null {
33
+ const v = process.env.AGENT_FS_API_URL?.trim().replace(/\/+$/, "");
34
+ return v || null;
35
+ }
36
+
37
+ /** Request headers forwarded as is. `authorization` is handled separately. */
38
+ const REQUEST_HEADER_ALLOWLIST = [
39
+ "content-type",
40
+ // Framing for streamed uploads: agent-fs checks it against its upload cap.
41
+ "content-length",
42
+ "accept",
43
+ "if-match",
44
+ "if-none-match",
45
+ "range",
46
+ "x-agent-fs-message",
47
+ "x-agent-fs-message-encoding",
48
+ ];
49
+ const AGENT_FS_BEARER = /^Bearer\s+af_\S+$/i;
50
+
51
+ /** Response headers copied from agent-fs (plus any `x-agent-fs-*`). */
52
+ const RESPONSE_HEADER_ALLOWLIST = new Set([
53
+ "content-type",
54
+ "content-length",
55
+ "content-disposition",
56
+ "content-range",
57
+ "accept-ranges",
58
+ "etag",
59
+ "last-modified",
60
+ "cache-control",
61
+ "location",
62
+ "retry-after",
63
+ // agent-fs serves user files (HTML included) with a sandbox CSP. Dropping
64
+ // these would run that content in the API's origin.
65
+ "content-security-policy",
66
+ "x-content-type-options",
67
+ "x-frame-options",
68
+ "referrer-policy",
69
+ "x-robots-tag",
70
+ "cross-origin-resource-policy",
71
+ ]);
72
+ const EXPOSED_HEADERS = ["X-Agent-FS-Version", "X-Agent-FS-Content-Hash", "Content-Disposition"];
73
+
74
+ const UNGATED =
75
+ "agent-fs enforces its own af_ auth; register needs a swarm credential; swarm credentials are never forwarded";
76
+ const OK_200 = {
77
+ description: "Upstream agent-fs response, streamed verbatim",
78
+ unstructured: "Body is streamed from agent-fs; shape and content-type vary per upstream route",
79
+ };
80
+
81
+ for (const method of ["get", "post", "put", "delete", "patch"] as const) {
82
+ route({
83
+ method,
84
+ path: "/agentfs/{path}",
85
+ pattern: ["agentfs", null],
86
+ exact: false,
87
+ summary: "Streaming pass-through proxy to agent-fs (enabled by AGENT_FS_PROXY)",
88
+ tags: ["Comb"],
89
+ responses: {
90
+ 200: OK_200,
91
+ 401: {
92
+ description:
93
+ "Returned by agent-fs when credentials are missing or invalid, or by the swarm when POST /agentfs/auth/register has no swarm credential",
94
+ },
95
+ 403: {
96
+ description:
97
+ "POST /agentfs/auth/register with a user token for an email that is not the user's",
98
+ },
99
+ 413: { description: "POST /agentfs/auth/register body over 16 KB" },
100
+ 429: { description: "Too many registrations from this IP (POST /agentfs/auth/register)" },
101
+ 502: { description: "agent-fs is unreachable" },
102
+ },
103
+ auth: { apiKey: false },
104
+ ...(method === "get" ? {} : { rbac: { ungated: UNGATED } }),
105
+ });
106
+ }
107
+
108
+ for (const prefix of ["share", "site"] as const) {
109
+ route({
110
+ method: "get",
111
+ path: `/${prefix}/{path}`,
112
+ pattern: [prefix, null],
113
+ exact: false,
114
+ summary: `Public agent-fs ${prefix} page, served through the API (enabled by AGENT_FS_PROXY)`,
115
+ tags: ["Comb"],
116
+ responses: {
117
+ 200: {
118
+ description: `agent-fs /${prefix} response, streamed verbatim`,
119
+ unstructured: "HTML page or raw file streamed from agent-fs; content-type varies",
120
+ },
121
+ 404: { description: "Unknown or expired link (from agent-fs), or the proxy is off" },
122
+ 502: { description: "agent-fs is unreachable" },
123
+ },
124
+ auth: { apiKey: false },
125
+ });
126
+ }
127
+
128
+ /** agent-fs `POST /auth/register` is public and unlimited upstream: 5 per minute per IP here. */
129
+ const registerRateLimiter = createIpRateLimiter({ capacity: 5, refillPerMs: 5 / 60_000 });
130
+
131
+ export function _resetAgentFsRegisterRateLimitForTests(): void {
132
+ registerRateLimiter.reset();
133
+ }
134
+
135
+ const REGISTER_BODY_MAX_BYTES = 16 * 1024;
136
+
137
+ /**
138
+ * agent-fs `POST /auth/register` does not check that the caller owns the
139
+ * email, and the swarm invites its users' emails to the shared drive. An
140
+ * account made first for a teammate's email would get that teammate's seat.
141
+ * So a registration through the proxy needs a swarm credential: the operator
142
+ * key may register any email, a user token only the user's own email or an
143
+ * alias. Returns the JSON body to send upstream, or null after it wrote the
144
+ * error response.
145
+ */
146
+ async function checkRegister(req: IncomingMessage, res: ServerResponse): Promise<string | null> {
147
+ const auth = await resolveHttpRequestAuth(req, getApiKey());
148
+ if (!auth || auth.page || (auth.kind !== "operator" && auth.kind !== "user")) {
149
+ jsonError(res, "Creating an agent-fs account here needs a swarm API key or user token", 401);
150
+ return null;
151
+ }
152
+ let body: unknown;
153
+ try {
154
+ body = await parseBody(req, REGISTER_BODY_MAX_BYTES);
155
+ } catch (err) {
156
+ if (err instanceof RequestBodyTooLargeError) jsonError(res, "Request body too large", 413);
157
+ else jsonError(res, "Invalid JSON body", 400);
158
+ return null;
159
+ }
160
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
161
+ jsonError(res, "Invalid JSON body", 400);
162
+ return null;
163
+ }
164
+ if (auth.kind === "user") {
165
+ const email = (body as { email?: unknown }).email;
166
+ const own = [auth.user.email, ...auth.user.emailAliases]
167
+ .filter((e): e is string => typeof e === "string")
168
+ .map((e) => e.trim().toLowerCase());
169
+ if (typeof email !== "string" || !own.includes(email.trim().toLowerCase())) {
170
+ jsonError(res, "You can create an agent-fs account only for your own email", 403);
171
+ return null;
172
+ }
173
+ }
174
+ // Re-encoded, so agent-fs reads the same email this check read.
175
+ return JSON.stringify(body);
176
+ }
177
+
178
+ /**
179
+ * The path as an upstream router would match it: percent-decoded, repeated
180
+ * slashes collapsed, trailing slash dropped. `/auth/%72egister` and
181
+ * `//auth/register/` must still hit the register limit.
182
+ */
183
+ function routedPath(pathOnly: string): string {
184
+ let decoded = pathOnly;
185
+ try {
186
+ decoded = decodeURIComponent(pathOnly);
187
+ } catch {
188
+ // A malformed escape: compare the raw bytes.
189
+ }
190
+ return decoded.replace(/\/{2,}/g, "/").replace(/\/+$/, "");
191
+ }
192
+
193
+ /**
194
+ * A path fetch() or the upstream router would rewrite: dot segments (raw or
195
+ * percent-encoded, also behind an encoded `%2F`) and backslashes, which the
196
+ * URL parser treats as `/`. Rejecting them keeps the register limit and the
197
+ * upstream on the same path: `/./auth/register` and `/auth\register` both
198
+ * reach upstream `/auth/register`.
199
+ */
200
+ function isNonCanonical(pathOnly: string): boolean {
201
+ let decoded = pathOnly;
202
+ try {
203
+ decoded = decodeURIComponent(pathOnly);
204
+ } catch {
205
+ // A malformed escape: check the raw bytes only.
206
+ }
207
+ for (const p of [pathOnly, decoded]) {
208
+ if (p.includes("\\")) return true;
209
+ if (p.split("/").some((s) => s === "." || s === "..")) return true;
210
+ }
211
+ // `.%2e` and the like, in case the decode above failed on a malformed escape.
212
+ return pathOnly.split("/").some((s) => /^(\.|%2e){1,2}$/i.test(s));
213
+ }
214
+
215
+ export async function handleAgentFsProxy(
216
+ req: IncomingMessage,
217
+ res: ServerResponse,
218
+ ): Promise<boolean> {
219
+ const url = req.url ?? "";
220
+ const isApi = url.startsWith(`${API_PREFIX}/`);
221
+ const isPublicPage = !isApi && PUBLIC_PREFIXES.some((p) => url.startsWith(p));
222
+ if (!isApi && !isPublicPage) return false;
223
+ const base = upstreamBase();
224
+ if (!base || !isAgentFsProxyEnabled()) return false;
225
+ const method = req.method ?? "GET";
226
+ const allowed = isApi ? ["GET", "POST", "PUT", "DELETE", "PATCH"] : ["GET"];
227
+ if (!allowed.includes(method)) return false;
228
+
229
+ // Keep the path and query bytes as sent, so an encoded `%2F` survives.
230
+ const upstreamPath = isApi ? url.slice(API_PREFIX.length) : url;
231
+ const queryStart = upstreamPath.indexOf("?");
232
+ const pathOnly = queryStart === -1 ? upstreamPath : upstreamPath.slice(0, queryStart);
233
+ if (isNonCanonical(pathOnly)) {
234
+ jsonError(res, "Invalid path", 400);
235
+ return true;
236
+ }
237
+ // Limit on the path fetch() will send, not on our own parse of the raw
238
+ // target: fetch drops a raw `#fragment`, so `/auth/register#x` reaches
239
+ // upstream `/auth/register`.
240
+ const upstreamUrl = `${base}${upstreamPath}`;
241
+ let sentPath: string;
242
+ try {
243
+ // Strip the base URL's own path prefix, if AGENT_FS_API_URL carries one.
244
+ const basePath = new URL(base).pathname.replace(/\/+$/, "");
245
+ sentPath = new URL(upstreamUrl).pathname.slice(basePath.length);
246
+ } catch {
247
+ jsonError(res, "Invalid path", 400);
248
+ return true;
249
+ }
250
+ const isRegister = isApi && method === "POST" && routedPath(sentPath) === "/auth/register";
251
+ if (isRegister && !registerRateLimiter.take(clientIp(req))) {
252
+ res.setHeader("Retry-After", "12");
253
+ jsonError(res, "rate_limited", 429);
254
+ return true;
255
+ }
256
+ const registerBody = isRegister ? await checkRegister(req, res) : null;
257
+ if (isRegister && registerBody === null) return true;
258
+
259
+ const headers = new Headers();
260
+ const authorization = req.headers.authorization?.trim();
261
+ // Only an agent-fs key goes upstream: never on register (the swarm credential
262
+ // authorized it), and never the swarm API key, even when it looks like one.
263
+ if (
264
+ registerBody === null &&
265
+ authorization &&
266
+ AGENT_FS_BEARER.test(authorization) &&
267
+ authorization.replace(/^bearer\s+/i, "") !== getApiKey()
268
+ ) {
269
+ headers.set("authorization", authorization);
270
+ }
271
+ for (const name of REQUEST_HEADER_ALLOWLIST) {
272
+ const v = req.headers[name];
273
+ if (typeof v === "string") headers.set(name, v);
274
+ }
275
+ if (registerBody !== null) {
276
+ // The body was read and re-encoded: fetch sets the new length.
277
+ headers.delete("content-length");
278
+ headers.set("content-type", "application/json");
279
+ }
280
+
281
+ const ac = new AbortController();
282
+ res.on("close", () => {
283
+ if (!res.writableEnded) ac.abort();
284
+ });
285
+
286
+ const hasBody = method !== "GET" && method !== "DELETE";
287
+ let upstream: Response;
288
+ try {
289
+ const init: RequestInit & { duplex?: "half" } = {
290
+ method,
291
+ headers,
292
+ redirect: "manual",
293
+ signal: ac.signal,
294
+ };
295
+ if (registerBody !== null) {
296
+ init.body = registerBody;
297
+ } else if (hasBody) {
298
+ // Stream the request body; Bun and undici need duplex "half" for that.
299
+ init.body = Readable.toWeb(req) as RequestInit["body"];
300
+ init.duplex = "half";
301
+ }
302
+ upstream = await fetch(upstreamUrl, init);
303
+ } catch {
304
+ if (!res.headersSent && !res.destroyed) jsonError(res, "agent-fs upstream unavailable", 502);
305
+ else res.destroy();
306
+ return true;
307
+ }
308
+
309
+ if (isPublicPage) {
310
+ // agent-fs owns CORS on public pages: drop the swarm's echo before copying its headers.
311
+ for (const name of res.getHeaderNames()) {
312
+ if (name.startsWith("access-control-")) res.removeHeader(name);
313
+ }
314
+ }
315
+ const encoded = upstream.headers.has("content-encoding");
316
+ upstream.headers.forEach((value, name) => {
317
+ const n = name.toLowerCase();
318
+ const keep =
319
+ RESPONSE_HEADER_ALLOWLIST.has(n) ||
320
+ n.startsWith("x-agent-fs-") ||
321
+ (isPublicPage && n.startsWith("access-control-"));
322
+ if (!keep) return;
323
+ // fetch() already decoded the body, so the encoded length no longer applies.
324
+ if (encoded && n === "content-length") return;
325
+ res.setHeader(name, value);
326
+ });
327
+ if (isApi) {
328
+ // Let allowlisted browser origins read the agent-fs response headers (the base
329
+ // list in setCorsHeaders only covers Content-Type, Content-Length, ETag, Location).
330
+ const expose = res.getHeader("Access-Control-Expose-Headers");
331
+ if (typeof expose === "string" && expose !== "*") {
332
+ res.setHeader("Access-Control-Expose-Headers", `${expose}, ${EXPOSED_HEADERS.join(", ")}`);
333
+ }
334
+ }
335
+ res.writeHead(upstream.status);
336
+ if (!upstream.body) {
337
+ res.end();
338
+ return true;
339
+ }
340
+ await pump(upstream.body, res);
341
+ return true;
342
+ }
343
+
344
+ /** Copy the upstream body to `res`, waiting for `drain` so a slow client never buffers the file. */
345
+ async function pump(body: ReadableStream<Uint8Array>, res: ServerResponse): Promise<void> {
346
+ const reader = body.getReader();
347
+ const closed = new Promise<void>((resolve) => {
348
+ if (res.destroyed) resolve();
349
+ else res.once("close", () => resolve());
350
+ });
351
+ try {
352
+ for (;;) {
353
+ if (res.destroyed) break;
354
+ const { done, value } = await reader.read();
355
+ if (done) {
356
+ res.end();
357
+ break;
358
+ }
359
+ if (!res.write(value)) {
360
+ await Promise.race([new Promise((resolve) => res.once("drain", resolve)), closed]);
361
+ }
362
+ }
363
+ } catch {
364
+ res.destroy();
365
+ } finally {
366
+ reader.cancel().catch(() => {});
367
+ }
368
+ await closed;
369
+ }
@@ -42,6 +42,7 @@ import "./oauth-callback";
42
42
  import "./oauth-generic";
43
43
  import "./onboarding";
44
44
  import "./page-proxy";
45
+ import "./agentfs-proxy";
45
46
  import "./pages";
46
47
  import "./pages-public";
47
48
  import "./prompt-templates";
@@ -1,9 +1,10 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import { z } from "zod";
3
3
  import { decryptSecret, encryptSecret, getEncryptionKey } from "../be/crypto";
4
- import { getDbClient, upsertSwarmConfig } from "../be/db";
4
+ import { getDbClient, recordKeyValidation, upsertSwarmConfig } from "../be/db";
5
5
  import { getCodexDeviceFlow, upsertCodexDeviceFlow } from "../be/db/codex-oauth-device-flows";
6
6
  import { updateOnboardingAiFromCodexDevice } from "../be/onboarding";
7
+ import { deriveCodexKeySuffix } from "../providers/codex-oauth/auth-json";
7
8
  import {
8
9
  CODEX_DEVICE_VERIFICATION_URL,
9
10
  DeviceCodeNotEnabledError,
@@ -214,6 +215,12 @@ async function completeFlow(
214
215
  isSecret: true,
215
216
  description: `Codex ChatGPT OAuth credentials slot ${slot} (stored by dashboard device login)`,
216
217
  });
218
+ // OpenAI just issued this token. Record the account as checked, so the
219
+ // account list says "Signed in" now: a worker's Codex check is a presence
220
+ // check and reports no per-account result.
221
+ const keySuffix = deriveCodexKeySuffix(credentials.access, credentials.accountId);
222
+ const checked = [{ keyIndex: slot, keySuffix, ok: true }];
223
+ await recordKeyValidation("CODEX_OAUTH", checked, "global", null, { retireAbsent: false });
217
224
  await updateOnboardingAiFromCodexDevice({ status: "complete" });
218
225
  state.status = "complete";
219
226
  state.slot = slot;
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));
@@ -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),
@@ -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);