@desplega.ai/agent-swarm 2.4.3 → 2.4.5

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 (270) hide show
  1. package/dist/{acp-adapter-1885dpr7.js → acp-adapter-2wdkktv6.js} +1 -2
  2. package/dist/{actions-pn76n32q.js → actions-c40ebcm4.js} +18 -18
  3. package/dist/{amp-adapter-af84rx1d.js → amp-adapter-wb67m95z.js} +2 -2
  4. package/dist/{app-6sv8mhzm.js → app-353vre8g.js} +3 -3
  5. package/dist/{assistant-sc2tf825.js → assistant-sj1njh23.js} +21 -21
  6. package/dist/{boot-reembed-y3m0ydzj.js → boot-reembed-b9c3az3c.js} +1 -1
  7. package/dist/{boot-reembed-gvxjs2py.js → boot-reembed-qdkvxeem.js} +1 -1
  8. package/dist/{boot-scrub-sweep-1j9mk05f.js → boot-scrub-sweep-m3wh8eks.js} +2 -2
  9. package/dist/{boot-seeds-37sp626b.js → boot-seeds-j2273s54.js} +4 -4
  10. package/dist/{claude-adapter-rp0jtnwr.js → claude-adapter-m7q0a085.js} +2 -2
  11. package/dist/{claude-managed-adapter-h2y889ge.js → claude-managed-adapter-5ptb6tzs.js} +2 -2
  12. package/dist/{claude-managed-setup-rdt248k7.js → claude-managed-setup-smrqk5j5.js} +3 -3
  13. package/dist/{claude-sdk-session-53rcw5we.js → claude-sdk-session-38xnz21e.js} +8284 -8267
  14. package/dist/{cli-6eyncg0y.js → cli-018r4jq6.js} +34 -6
  15. package/dist/{cli-sds5zzs8.js → cli-1f3qzw6y.js} +78 -12
  16. package/dist/{cli-f0p0f6ja.js → cli-1sp861s4.js} +7 -7
  17. package/dist/{cli-dm8p2dpf.js → cli-1syn2xy1.js} +10 -10
  18. package/dist/{cli-tkf4x6ha.js → cli-37j04k05.js} +11 -11
  19. package/dist/{cli-v87dr6e3.js → cli-3a45zcgm.js} +1 -1
  20. package/dist/{cli-1n9gdg56.js → cli-43x64hv3.js} +4 -4
  21. package/dist/{cli-p0qerd2r.js → cli-4zfqgjc5.js} +2 -2
  22. package/dist/{cli-aa3tb29r.js → cli-7c9qs530.js} +1 -1
  23. package/dist/{cli-9qtjfd60.js → cli-848hxdwj.js} +1 -1
  24. package/dist/{cli-1xsraagq.js → cli-94gwyg7m.js} +8 -8
  25. package/dist/{cli-0dxxwfkt.js → cli-9hstj09h.js} +17 -14
  26. package/dist/{cli-1012xb7j.js → cli-9yqgm9nd.js} +22 -22
  27. package/dist/{cli-pygdsgh8.js → cli-a92r1127.js} +1 -1
  28. package/dist/{cli-8vverzf7.js → cli-ba8ne1es.js} +5 -0
  29. package/dist/{cli-bxrqvpg5.js → cli-be2161h1.js} +7 -7
  30. package/dist/{cli-npfe9t1s.js → cli-bnes4z6b.js} +2 -2
  31. package/dist/{cli-aypbg9r2.js → cli-cxxe61f6.js} +1 -1
  32. package/dist/{cli-vq9gqarq.js → cli-dk44dxec.js} +1 -1
  33. package/dist/{cli-836ehxmt.js → cli-eww120g8.js} +3 -2
  34. package/dist/{cli-a0kcty51.js → cli-f01h25yt.js} +1 -1
  35. package/dist/{cli-mzkwkbdy.js → cli-f0g3pgvf.js} +1 -1
  36. package/dist/{cli-1ma335fz.js → cli-ftc1g5tj.js} +2 -2
  37. package/dist/{cli-4yef8ghv.js → cli-fv5hepyy.js} +3 -3
  38. package/dist/{cli-58vwn100.js → cli-g35m2xwm.js} +4 -4
  39. package/dist/{cli-qje9k2nh.js → cli-g9ypd5en.js} +12 -2
  40. package/dist/{cli-gx0j2khs.js → cli-j99azg3g.js} +3 -3
  41. package/dist/{cli-er83cjbn.js → cli-k8g16gz3.js} +416 -116
  42. package/dist/{cli-qp2m4btv.js → cli-kpw04va0.js} +2 -2
  43. package/dist/{cli-8y99kfey.js → cli-ms85309d.js} +33 -15
  44. package/dist/{cli-kgnkyabk.js → cli-n72rg53p.js} +1555 -645
  45. package/dist/{cli-e4d78xsv.js → cli-p14m2cnp.js} +1 -1
  46. package/dist/{cli-1eszr343.js → cli-qfxc84gh.js} +1 -1
  47. package/dist/{cli-wk28hzqn.js → cli-qwhy2qma.js} +1 -1
  48. package/dist/{cli-73ezfhha.js → cli-st8aw1m6.js} +228 -11
  49. package/dist/{cli-zvbb4n0e.js → cli-tw4p90jb.js} +92 -192
  50. package/dist/{cli-038fga5k.js → cli-v0yrfmdh.js} +148 -183
  51. package/dist/{cli-6aen3ezb.js → cli-v8fjakwf.js} +1 -1
  52. package/dist/{cli-99jez72r.js → cli-w9etnnxk.js} +6 -1
  53. package/dist/{cli-vcgscaan.js → cli-wt0jf9hm.js} +217 -80
  54. package/dist/{cli-s9rr4gr2.js → cli-xpa64rek.js} +1065 -305
  55. package/dist/{cli-h0mgxann.js → cli-z7d29rw9.js} +2 -2
  56. package/dist/cli.js +45 -20
  57. package/dist/{codex-adapter-2zrmab9b.js → codex-adapter-tn6k5mkg.js} +1 -1
  58. package/dist/{codex-session-runner-kbkz62vc.js → codex-session-runner-7vjt7jbw.js} +1 -1
  59. package/dist/{commands-hd574th6.js → commands-t7rz9146.js} +2 -2
  60. package/dist/{controller-pz5mcfg4.js → controller-m2jasma5.js} +2 -2
  61. package/dist/{controllers-connect-5rgq2hfk.js → controllers-connect-9ftpex99.js} +19 -19
  62. package/dist/{cursor-adapter-mgaprq0j.js → cursor-adapter-mjv79e4z.js} +1 -1
  63. package/dist/{db-6nf1207q.js → db-6sfrn9g3.js} +1 -1
  64. package/dist/{db-h06dxp05.js → db-rn673r06.js} +3 -3
  65. package/dist/{dispatcher-zthpy4m1.js → dispatcher-c155xz0p.js} +17 -17
  66. package/dist/{dsh-adapter-64dhw0j7.js → dsh-adapter-3mmszc1x.js} +1 -1
  67. package/dist/{external-channel-x01t7ke6.js → external-channel-35k6tkv8.js} +2 -2
  68. package/dist/{grok-adapter-h21jv594.js → grok-adapter-tfrq7tg1.js} +2 -3
  69. package/dist/handlers-7gw64dxw.js +39 -0
  70. package/dist/{heartbeat-zzfksckz.js → heartbeat-dyg4npkp.js} +20 -20
  71. package/dist/{hook-z4zhzj1f.js → hook-drspbwpe.js} +1 -1
  72. package/dist/{hook-3xfqhh2s.js → hook-x26b31bg.js} +1 -1
  73. package/dist/{http-q813tmya.js → http-pk972xyw.js} +571 -294
  74. package/dist/{index-va3y63es.js → index-6srwvd5b.js} +1 -2
  75. package/dist/{index-x8xbyn1t.js → index-arytgj5q.js} +1 -1
  76. package/dist/{index-wbbs14c5.js → index-bgdyvcnv.js} +17 -17
  77. package/dist/{index-s98037v0.js → index-cbxkmfc5.js} +1 -2
  78. package/dist/{index-c4ykd0f1.js → index-dr94zbnh.js} +13348 -11611
  79. package/dist/{index-m0a4ydqj.js → index-gabbcdn2.js} +17 -17
  80. package/dist/{index-220tk9bw.js → index-jwxkkex2.js} +139 -76
  81. package/dist/index-tfb26zhy.js +41 -0
  82. package/dist/{keepalive-y5t1my7m.js → keepalive-7yrng7px.js} +3 -3
  83. package/dist/{lead-2xwtq66b.js → lead-7e02z839.js} +29 -30
  84. package/dist/{maintenance-ybfsaedv.js → maintenance-e6cemdqt.js} +1 -1
  85. package/dist/{node-3v3f08f7.js → node-de6akczr.js} +10 -9
  86. package/dist/{oauth-refresh-sweep-909jmn12.js → oauth-refresh-sweep-yw3xdbcj.js} +2 -2
  87. package/dist/{onboard-xn6fkkys.js → onboard-0a9y5yrc.js} +2 -2
  88. package/dist/{otel-impl-e6mn0z13.js → otel-impl-trjpkfb7.js} +1 -1
  89. package/dist/{pricing-refresh-ygd362ep.js → pricing-refresh-d2b5my0y.js} +3 -3
  90. package/dist/{rbac-roles-mb470p4e.js → rbac-roles-hey3yv9d.js} +2 -2
  91. package/dist/{rbac-roles-c9axvfnz.js → rbac-roles-nxh5ahwk.js} +2 -2
  92. package/dist/{registry-rxw10xzg.js → registry-ajhexe44.js} +1 -1
  93. package/dist/{render-v2-g0306nh1.js → render-v2-rr3p2atg.js} +3 -3
  94. package/dist/{resume-v315hnq6.js → resume-2efkf1s1.js} +5 -5
  95. package/dist/{session-access-2zg285a1.js → session-access-s2mttxpw.js} +6 -4
  96. package/dist/{setup-a4mtf02k.js → setup-vypma5yt.js} +1 -1
  97. package/dist/{work-objects-9fr0fgtn.js → work-objects-rvhckdkp.js} +20 -20
  98. package/dist/{worker-qae22jeb.js → worker-5snytpbd.js} +29 -30
  99. package/dist/worker-token-xf11yyds.js +136 -0
  100. package/openapi.json +303 -13
  101. package/package.json +8 -8
  102. package/src/apps/action-runner.ts +4 -0
  103. package/src/apps/catalog.generated.json +209 -4
  104. package/src/apps/page-validator.ts +21 -3
  105. package/src/be/chunking.ts +42 -9
  106. package/src/be/db/tasks/draft-text.ts +15 -0
  107. package/src/be/db/tasks/read.ts +14 -1
  108. package/src/be/db.ts +5 -1
  109. package/src/be/memory/index-content.ts +36 -17
  110. package/src/be/memory/providers/openai-embedding.ts +115 -61
  111. package/src/be/memory/providers/sqlite-store.ts +69 -23
  112. package/src/be/memory/reranker.ts +31 -1
  113. package/src/be/memory/search-candidates.ts +47 -0
  114. package/src/be/memory/types.ts +14 -0
  115. package/src/be/migrations/232_agent_session_tokens.sql +41 -0
  116. package/src/be/scripts/typecheck.ts +2 -0
  117. package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +116 -55
  118. package/src/be/seed-scripts/index.ts +2 -2
  119. package/src/be/swarm-config-guard.ts +40 -1
  120. package/src/be/task-attachment-store.ts +30 -0
  121. package/src/be/users.ts +169 -12
  122. package/src/cli.tsx +24 -0
  123. package/src/commands/worker-token.ts +164 -0
  124. package/src/extensions/catalog.generated.json +4 -4
  125. package/src/extensions/contract-types.generated.ts +1 -1
  126. package/src/extensions/contract.ts +7 -0
  127. package/src/fs/provider.ts +13 -1
  128. package/src/heartbeat/heartbeat.ts +31 -11
  129. package/src/http/apps.ts +43 -5
  130. package/src/http/codex-oauth-keep-warm.ts +27 -9
  131. package/src/http/config.ts +5 -3
  132. package/src/http/core.ts +2 -0
  133. package/src/http/fs.ts +54 -34
  134. package/src/http/mcp-body.ts +37 -0
  135. package/src/http/mcp-user.ts +3 -5
  136. package/src/http/mcp.ts +6 -7
  137. package/src/http/memory.ts +26 -15
  138. package/src/http/page-proxy.ts +61 -5
  139. package/src/http/pages-public.ts +27 -44
  140. package/src/http/pages.ts +4 -4
  141. package/src/http/script-runs.ts +2 -0
  142. package/src/http/session-access.ts +51 -2
  143. package/src/http/sessions.ts +249 -10
  144. package/src/http/tasks.ts +4 -32
  145. package/src/http/users.ts +12 -1
  146. package/src/integrations/chat-sdk/adapters.ts +7 -3
  147. package/src/integrations/chat-sdk/attachments.ts +90 -12
  148. package/src/integrations/chat-sdk/facts.ts +120 -0
  149. package/src/integrations/chat-sdk/inbound.ts +156 -8
  150. package/src/integrations/chat-sdk/limits.ts +24 -0
  151. package/src/integrations/chat-sdk/outbound-files.ts +186 -0
  152. package/src/integrations/chat-sdk/outbound.ts +234 -4
  153. package/src/integrations/chat-sdk/relay.ts +4 -1
  154. package/src/integrations/chat-sdk/router.ts +28 -2
  155. package/src/integrations/chat-sdk/steering.ts +9 -0
  156. package/src/integrations/chat-sdk/templates.ts +14 -0
  157. package/src/integrations/chat-sdk/thread-buffer.ts +4 -0
  158. package/src/mcp-limits.ts +20 -0
  159. package/src/pages/browser-sdk.ts +8 -3
  160. package/src/pages/feedback-overlay.ts +1 -1
  161. package/src/providers/acp-adapter.ts +33 -13
  162. package/src/providers/acp-swarm-events.ts +1 -1
  163. package/src/providers/amp-adapter.ts +1 -1
  164. package/src/providers/codex-adapter.ts +106 -119
  165. package/src/providers/codex-agents-md.ts +35 -229
  166. package/src/providers/codex-oauth/storage.ts +1 -1
  167. package/src/providers/cursor-adapter.ts +1 -1
  168. package/src/rbac/legacy-policy.ts +3 -1
  169. package/src/rbac/permissions.ts +4 -0
  170. package/src/realtime/transport.ts +1 -2
  171. package/src/script-workflows/workflow-ctx.ts +1 -0
  172. package/src/scripts-runtime/types/stdlib.d.ts +2 -0
  173. package/src/scripts-runtime/types/swarm-extension.d.ts +8 -0
  174. package/src/scripts-runtime/types/swarm-sdk.d.ts +2 -0
  175. package/src/server-user.ts +2 -0
  176. package/src/server.ts +2 -0
  177. package/src/slack/assistant.ts +2 -2
  178. package/src/slack/files.ts +2 -0
  179. package/src/slack/handlers.ts +4 -4
  180. package/src/slack/inbound-files.ts +39 -6
  181. package/src/slack/thread-buffer.ts +2 -1
  182. package/src/tasks/task-terminal-effects.ts +21 -17
  183. package/src/tests/acp-adapter.test.ts +10 -23
  184. package/src/tests/acp-fake-agent.ts +36 -0
  185. package/src/tests/acp-swarm-events.test.ts +1 -1
  186. package/src/tests/agent-session-tokens.test.ts +452 -0
  187. package/src/tests/apps-action-parent-task.test.ts +295 -0
  188. package/src/tests/apps-card-grid.test.ts +197 -0
  189. package/src/tests/apps-catalog-additions.test.ts +120 -0
  190. package/src/tests/apps-tree.test.ts +129 -0
  191. package/src/tests/chat-sdk-attachments-reply.test.ts +52 -0
  192. package/src/tests/chat-sdk-config.test.ts +11 -0
  193. package/src/tests/chat-sdk-facts.test.ts +23 -0
  194. package/src/tests/chat-sdk-inbound.test.ts +558 -4
  195. package/src/tests/chat-sdk-outbound-files.test.ts +652 -0
  196. package/src/tests/codex-adapter.test.ts +167 -242
  197. package/src/tests/codex-oauth-keep-warm.test.ts +85 -1
  198. package/src/tests/config-api-only-keys.test.ts +9 -0
  199. package/src/tests/config-session-auth.test.ts +14 -0
  200. package/src/tests/cors-allowlist.test.ts +1 -1
  201. package/src/tests/docs-index-footprint.test.js +2 -2
  202. package/src/tests/extensions-example-slack-reply-gate.test.ts +262 -91
  203. package/src/tests/grok-adapter.test.ts +3 -3
  204. package/src/tests/heartbeat-checklist.test.ts +33 -0
  205. package/src/tests/heartbeat-jev-gate-credentials.test.ts +44 -0
  206. package/src/tests/heartbeat-jev-gate.test.ts +265 -1
  207. package/src/tests/kv-page-proxy.test.ts +91 -5
  208. package/src/tests/launch-password-rejection.test.ts +2 -2
  209. package/src/tests/mcp-input-caps.test.ts +294 -0
  210. package/src/tests/memory-chunked-search-starvation.test.ts +200 -0
  211. package/src/tests/memory-embedding-input-cap.test.ts +114 -0
  212. package/src/tests/memory.test.ts +14 -0
  213. package/src/tests/migration-232-agent-session-tokens.test.ts +67 -0
  214. package/src/tests/page-guest-session.test.ts +19 -8
  215. package/src/tests/page-proxy-authed.test.ts +1 -1
  216. package/src/tests/page-proxy.test.ts +17 -10
  217. package/src/tests/page-session.test.ts +102 -1
  218. package/src/tests/pages-authed-mode.test.ts +82 -12
  219. package/src/tests/pages-password-mode.test.ts +5 -5
  220. package/src/tests/rbac-engine.test.ts +1 -0
  221. package/src/tests/realtime-transport.test.ts +13 -7
  222. package/src/tests/runtime-child-token-cleanup.test.ts +3 -3
  223. package/src/tests/script-connections.test.ts +2 -0
  224. package/src/tests/script-runs-http.test.ts +84 -0
  225. package/src/tests/scripts-http.test.ts +10 -1
  226. package/src/tests/session-route-posture.test.ts +2 -0
  227. package/src/tests/swarm-config-export-to-env.test.ts +22 -3
  228. package/src/tests/task-audio-transcription.test.ts +299 -0
  229. package/src/tests/task-completion-memory-chunking.test.ts +434 -0
  230. package/src/tests/task-terminal-secret-scrub.test.ts +6 -1
  231. package/src/tests/transcription.test.ts +299 -0
  232. package/src/tests/worker-token-cli.test.ts +76 -0
  233. package/src/tests/workflow-executors.test.ts +4 -2
  234. package/src/tests/workflow-system-one-decision-providers.test.ts +98 -1
  235. package/src/tests/workflow-system-one-decision.test.ts +2 -1
  236. package/src/tests/workflow-system-one-extraction.test.ts +805 -0
  237. package/src/tools/chat-access.ts +10 -0
  238. package/src/tools/chat-post.ts +31 -11
  239. package/src/tools/chat-reply.ts +32 -10
  240. package/src/tools/memory-search.ts +3 -1
  241. package/src/tools/system-one-decide.ts +17 -4
  242. package/src/types.ts +8 -1
  243. package/src/utils/config-env-export.ts +3 -1
  244. package/src/utils/page-session.ts +133 -20
  245. package/src/utils/request-auth-context.ts +2 -1
  246. package/src/utils/transcription.ts +246 -0
  247. package/src/workflows/definition.ts +7 -1
  248. package/src/workflows/engine.ts +18 -12
  249. package/src/workflows/executors/index.ts +1 -0
  250. package/src/workflows/executors/registry.ts +2 -0
  251. package/src/workflows/executors/system-one-decision.ts +84 -262
  252. package/src/workflows/executors/system-one-extract-wire.ts +57 -0
  253. package/src/workflows/executors/system-one-extraction.ts +830 -0
  254. package/src/workflows/executors/system-one-providers.ts +316 -114
  255. package/src/workflows/executors/system-one-transport.ts +264 -0
  256. package/templates/extensions/slack-reply-gate/README.md +24 -22
  257. package/templates/extensions/slack-reply-gate/hooks.ts +146 -95
  258. package/templates/extensions/slack-reply-gate/manifest.json +2 -2
  259. package/templates/official/lead/HEARTBEAT.md +1 -1
  260. package/templates/skills/apps/SKILL.md +31 -7
  261. package/templates/skills/apps/content.md +31 -7
  262. package/templates/skills/chat-sdk/content.md +6 -2
  263. package/templates/skills/code-quality/content.md +2 -1
  264. package/templates/skills/heartbeat-runbook/content.md +8 -8
  265. package/templates/skills/pages/content.md +6 -4
  266. package/templates/skills/swarm-scripts/SKILL.md +2 -2
  267. package/templates/skills/swarm-scripts/content.md +2 -2
  268. package/dist/cli-v1sjhn52.js +0 -7
  269. package/dist/handlers-2qjw434w.js +0 -39
  270. package/dist/index-d0g18bax.js +0 -41
@@ -134,17 +134,21 @@ afterAll(async () => {
134
134
  describe("proxy allowlist for a guest session", () => {
135
135
  test("allows the page's own record and its KV", async () => {
136
136
  const own = await fetch(`${BASE}/@swarm/api/pages/${pageId}`, {
137
- headers: { Cookie: `page_session=${guestCookie}` },
137
+ headers: { Cookie: `page_session_${pageId}=${guestCookie}`, "X-Swarm-Page-Id": pageId },
138
138
  });
139
139
  expect(own.status).toBe(200);
140
140
  const put = await fetch(`${BASE}/@swarm/api/kv/probe`, {
141
141
  method: "PUT",
142
- headers: { Cookie: `page_session=${guestCookie}`, "Content-Type": "application/json" },
142
+ headers: {
143
+ Cookie: `page_session_${pageId}=${guestCookie}`,
144
+ "X-Swarm-Page-Id": pageId,
145
+ "Content-Type": "application/json",
146
+ },
143
147
  body: JSON.stringify({ value: "v" }),
144
148
  });
145
149
  expect(put.status).toBe(200);
146
150
  const get = await fetch(`${BASE}/@swarm/api/kv/probe`, {
147
- headers: { Cookie: `page_session=${guestCookie}` },
151
+ headers: { Cookie: `page_session_${pageId}=${guestCookie}`, "X-Swarm-Page-Id": pageId },
148
152
  });
149
153
  expect(get.status).toBe(200);
150
154
  });
@@ -162,7 +166,11 @@ describe("proxy allowlist for a guest session", () => {
162
166
  test(`refuses ${method} ${path} with 403`, async () => {
163
167
  const res = await fetch(`${BASE}/@swarm/api/${path}`, {
164
168
  method,
165
- headers: { Cookie: `page_session=${guestCookie}`, "Content-Type": "application/json" },
169
+ headers: {
170
+ Cookie: `page_session_${pageId}=${guestCookie}`,
171
+ "X-Swarm-Page-Id": pageId,
172
+ "Content-Type": "application/json",
173
+ },
166
174
  body: method === "POST" ? "{}" : undefined,
167
175
  });
168
176
  expect(res.status).toBe(403);
@@ -206,13 +214,16 @@ describe("a password-page guest is isolated from another page", () => {
206
214
 
207
215
  const unlock = await fetch(`${BASE}/p/${pageA}?key=open-sesame`);
208
216
  expect(unlock.status).toBe(200);
209
- const cookieA = /page_session=([^;]+)/.exec(unlock.headers.get("set-cookie") ?? "")?.[1];
217
+ const cookieA = /page_session_[A-Za-z0-9_-]+=([^;]+)/.exec(
218
+ unlock.headers.get("set-cookie") ?? "",
219
+ )?.[1];
210
220
  expect(cookieA).toBeTruthy();
211
221
  const asGuest = (path: string, init: RequestInit = {}) =>
212
222
  fetch(`${BASE}/@swarm/api/${path}`, {
213
223
  ...init,
214
224
  headers: {
215
- Cookie: `page_session=${cookieA}`,
225
+ Cookie: `page_session_${pageA}=${cookieA}`,
226
+ "X-Swarm-Page-Id": pageA,
216
227
  "Content-Type": "application/json",
217
228
  "X-Page-Id": pageB,
218
229
  "X-Agent-ID": agentId,
@@ -377,12 +388,12 @@ describe("server-side gates deny a guest and keep the operator and user", () =>
377
388
  test("a user session through the proxy and an operator-launched session still work", async () => {
378
389
  for (const cookie of [userCookie, operatorCookie]) {
379
390
  const res = await fetch(`${BASE}/@swarm/api/tasks`, {
380
- headers: { Cookie: `page_session=${cookie}` },
391
+ headers: { Cookie: `page_session_${pageId}=${cookie}`, "X-Swarm-Page-Id": pageId },
381
392
  });
382
393
  expect(res.status).toBe(200);
383
394
  }
384
395
  const whoami = await fetch(`${BASE}/@swarm/api/whoami`, {
385
- headers: { Cookie: `page_session=${userCookie}` },
396
+ headers: { Cookie: `page_session_${pageId}=${userCookie}`, "X-Swarm-Page-Id": pageId },
386
397
  });
387
398
  expect(((await whoami.json()) as { kind: string }).kind).toBe("user");
388
399
  });
@@ -122,7 +122,7 @@ describe("/@swarm/api/* proxy — authed-mode page", () => {
122
122
  expect(launch.status).toBe(204);
123
123
  const setCookie = launch.headers.get("set-cookie");
124
124
  expect(setCookie).toBeTruthy();
125
- const cookieValue = /page_session=([^;]+)/.exec(setCookie!)?.[1];
125
+ const cookieValue = /page_session_[A-Za-z0-9_-]+=([^;]+)/.exec(setCookie!)?.[1];
126
126
  expect(cookieValue).toBeTruthy();
127
127
 
128
128
  // Drive the proxy with the cookie — should resolve to the page owner's
@@ -182,11 +182,13 @@ type BrowserSdk = {
182
182
  };
183
183
  };
184
184
 
185
- function browserSdk(cookie: string, spoofedAgentId?: string): BrowserSdk {
185
+ function browserSdk(pageId: string, cookie: string, spoofedAgentId?: string): BrowserSdk {
186
186
  const window: { swarmSdk?: BrowserSdk } = {};
187
187
  const pageFetch = (input: string, init?: RequestInit) => {
188
188
  const headers = new Headers(init?.headers);
189
- headers.set("Cookie", `page_session=${cookie}`);
189
+ // The SDK must name its own page; the test only supplies the cookie jar.
190
+ if (headers.get("X-Swarm-Page-Id") !== pageId) throw new Error("SDK did not name its page");
191
+ headers.set("Cookie", `page_session_${pageId}=${cookie}`);
190
192
  if (spoofedAgentId) headers.set("X-Agent-ID", spoofedAgentId);
191
193
  return fetch(new URL(input, BASE), { ...init, headers });
192
194
  };
@@ -195,6 +197,7 @@ function browserSdk(cookie: string, spoofedAgentId?: string): BrowserSdk {
195
197
  encodeURIComponent,
196
198
  fetch: pageFetch,
197
199
  URLSearchParams,
200
+ location: { pathname: `/p/${pageId}` },
198
201
  window,
199
202
  });
200
203
  if (!window.swarmSdk) throw new Error("browser SDK did not initialize");
@@ -207,7 +210,9 @@ async function launchPageCookie(id: string, bearer = API_KEY): Promise<string> {
207
210
  headers: { Authorization: `Bearer ${bearer}` },
208
211
  });
209
212
  expect(launch.status).toBe(204);
210
- const cookie = /page_session=([^;]+)/.exec(launch.headers.get("set-cookie") ?? "")?.[1];
213
+ const cookie = /page_session_[A-Za-z0-9_-]+=([^;]+)/.exec(
214
+ launch.headers.get("set-cookie") ?? "",
215
+ )?.[1];
211
216
  if (!cookie) throw new Error("failed to mint page session cookie");
212
217
  return cookie;
213
218
  }
@@ -243,14 +248,14 @@ describe("/api/pages/:id/launch", () => {
243
248
  expect(res.status).toBe(204);
244
249
  const cookie = res.headers.get("set-cookie");
245
250
  expect(cookie).toBeTruthy();
246
- expect(cookie!).toContain("page_session=");
251
+ expect(cookie!).toContain("page_session_");
247
252
  expect(cookie!).toContain("HttpOnly");
248
253
  expect(cookie!).toContain("Path=/");
249
254
  expect(cookie!).toContain("Max-Age=3600");
250
255
  // In dev (NODE_ENV != production) the cookie should be SameSite=Lax sans Secure.
251
256
  expect(cookie!).toContain("SameSite=Lax");
252
257
  expect(cookie!).not.toMatch(/\bSecure\b/);
253
- const cookieValue = /page_session=([^;]+)/.exec(cookie!)?.[1];
258
+ const cookieValue = /page_session_[A-Za-z0-9_-]+=([^;]+)/.exec(cookie!)?.[1];
254
259
  const payload = await verifyPageSession(cookieValue);
255
260
  expect(payload?.uid).toBeUndefined();
256
261
  expect(payload?.name).toMatch(/^guest-/);
@@ -294,7 +299,9 @@ describe("/@swarm/api/* proxy", () => {
294
299
  headers: { Authorization: `Bearer ${viewer.token}` },
295
300
  });
296
301
  expect(launch.status).toBe(204);
297
- const cookieValue = /page_session=([^;]+)/.exec(launch.headers.get("set-cookie") ?? "")?.[1];
302
+ const cookieValue = /page_session_[A-Za-z0-9_-]+=([^;]+)/.exec(
303
+ launch.headers.get("set-cookie") ?? "",
304
+ )?.[1];
298
305
  expect(cookieValue).toBeTruthy();
299
306
  const payload = await verifyPageSession(cookieValue);
300
307
  expect(payload?.uid).toBe(viewer.id);
@@ -322,7 +329,7 @@ describe("/@swarm/api/* proxy", () => {
322
329
  const setCookie = launch.headers.get("set-cookie");
323
330
  expect(setCookie).toBeTruthy();
324
331
 
325
- const cookieValue = /page_session=([^;]+)/.exec(setCookie!)?.[1];
332
+ const cookieValue = /page_session_[A-Za-z0-9_-]+=([^;]+)/.exec(setCookie!)?.[1];
326
333
  expect(cookieValue).toBeTruthy();
327
334
 
328
335
  const res = await fetch(`${BASE}/@swarm/api/agents/${agentId}`, {
@@ -410,7 +417,7 @@ describe("/@swarm/api/* page execution context", () => {
410
417
 
411
418
  // Execute the shipped browser SDK with a forged client identity. The
412
419
  // proxy must ignore that header and derive memory scope from the page.
413
- const sdk = browserSdk(cookie, otherAgentId);
420
+ const sdk = browserSdk(pageId, cookie, otherAgentId);
414
421
  const search = await sdk.memory.search({ query: searchToken, scope: "agent", limit: 10 });
415
422
  expect(search.results.map((memory) => memory.id)).toContain(ownerMemoryId);
416
423
  expect(search.results.map((memory) => memory.id)).not.toContain(otherMemoryId);
@@ -432,7 +439,7 @@ describe("/@swarm/api/* page execution context", () => {
432
439
  const viewer = await createUserToken(`Page Viewer ${randomUUID().slice(0, 8)}`);
433
440
  const pageId = await createPage();
434
441
  const cookie = await launchPageCookie(pageId, viewer.token);
435
- const sdk = browserSdk(cookie);
442
+ const sdk = browserSdk(pageId, cookie);
436
443
 
437
444
  const search = await sdk.memory.search({ query: searchToken, scope: "agent", limit: 5 });
438
445
  expect(search.results.map((memory) => memory.id)).toContain(ownerMemoryId);
@@ -506,7 +513,7 @@ describe("/@swarm/api/* proxy — suffix normalization", () => {
506
513
  headers: { Authorization: `Bearer ${API_KEY}`, "X-Agent-ID": agentId },
507
514
  });
508
515
  const setCookie = launch.headers.get("set-cookie");
509
- const cookieValue = /page_session=([^;]+)/.exec(setCookie!)?.[1];
516
+ const cookieValue = /page_session_[A-Za-z0-9_-]+=([^;]+)/.exec(setCookie!)?.[1];
510
517
  if (!cookieValue) throw new Error("failed to mint test cookie");
511
518
  return cookieValue;
512
519
  }
@@ -3,7 +3,15 @@ import { existsSync, mkdtempSync, rmSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import path from "node:path";
5
5
  import type { PageSessionPayload } from "../utils/page-session";
6
- import { parseCookieHeader, signPageSession, verifyPageSession } from "../utils/page-session";
6
+ import {
7
+ extractPageSession,
8
+ issuePageSessionCookie,
9
+ MAX_PAGE_SESSION_COOKIES,
10
+ pageSessionSetCookies,
11
+ parseCookieHeader,
12
+ signPageSession,
13
+ verifyPageSession,
14
+ } from "../utils/page-session";
7
15
 
8
16
  const ORIGINAL_SECRET = process.env.PAGE_SESSION_SECRET;
9
17
  const ORIGINAL_API_KEY = process.env.API_KEY;
@@ -266,3 +274,96 @@ describe("parseCookieHeader", () => {
266
274
  expect(parseCookieHeader("xxpage_session=evil; other=ok", "page_session")).toBeUndefined();
267
275
  });
268
276
  });
277
+
278
+ describe("per-page session cookies", () => {
279
+ const exp = () => Math.floor(Date.now() / 1000) + 3600;
280
+ const req = (cookie: string) => ({ headers: { cookie } });
281
+
282
+ test("issuance names the cookie after the page", async () => {
283
+ const header = await issuePageSessionCookie("pagea", { dev: true });
284
+ expect(header.startsWith("page_session_pagea=")).toBe(true);
285
+ });
286
+
287
+ test("each page reads its own cookie from a shared jar", async () => {
288
+ const a = await signPageSession({ pageId: "pagea", exp: exp() });
289
+ const b = await signPageSession({ pageId: "pageb", exp: exp() });
290
+ const jar = req(`page_session_pagea=${a}; page_session_pageb=${b}`);
291
+ expect((await extractPageSession(jar, "pagea"))?.token).toBe(a);
292
+ expect((await extractPageSession(jar, "pageb"))?.token).toBe(b);
293
+ });
294
+
295
+ test("the legacy page_session cookie is accepted only for the page it names", async () => {
296
+ const a = await signPageSession({ pageId: "pagea", exp: exp() });
297
+ expect((await extractPageSession(req(`page_session=${a}`), "pagea"))?.payload.pageId).toBe(
298
+ "pagea",
299
+ );
300
+ expect(await extractPageSession(req(`page_session=${a}`), "pageb")).toBeNull();
301
+ });
302
+
303
+ test("a token for another page under this page's name is ignored", async () => {
304
+ const a = await signPageSession({ pageId: "pagea", exp: exp() });
305
+ expect(await extractPageSession(req(`page_session_pageb=${a}`), "pageb")).toBeNull();
306
+ });
307
+
308
+ test("an id that is not cookie-name safe is refused", async () => {
309
+ const a = await signPageSession({ pageId: "a=b", exp: exp() });
310
+ expect(await extractPageSession(req(`page_session=${a}`), "a=b")).toBeNull();
311
+ });
312
+ });
313
+
314
+ describe("pageSessionSetCookies", () => {
315
+ const now = () => Math.floor(Date.now() / 1000);
316
+ const cookieFor = async (pageId: string, exp = now() + 3600) =>
317
+ `page_session_${pageId}=${await signPageSession({ pageId, exp })}`;
318
+ const evicted = (lines: string[]) =>
319
+ lines.slice(1).map((line) => {
320
+ expect(line).toContain("Max-Age=0");
321
+ return line.split("=")[0];
322
+ });
323
+
324
+ test("issues only the new session when the jar is under the cap", async () => {
325
+ const jar = await cookieFor("pagea");
326
+ const lines = await pageSessionSetCookies({ headers: { cookie: jar } }, "pageb", { dev: true });
327
+ expect(lines).toHaveLength(1);
328
+ expect(lines[0]!.startsWith("page_session_pageb=")).toBe(true);
329
+ });
330
+
331
+ test("evicts the oldest sessions beyond the cap", async () => {
332
+ const ids = Array.from({ length: MAX_PAGE_SESSION_COOKIES + 4 }, (_, i) => `page${i}`);
333
+ // page0 expires soonest, so it is the oldest launch.
334
+ const jar = (await Promise.all(ids.map((id, i) => cookieFor(id, now() + 1000 + i)))).join("; ");
335
+ const lines = await pageSessionSetCookies({ headers: { cookie: jar } }, "fresh", { dev: true });
336
+ expect(evicted(lines).sort()).toEqual(
337
+ ["page0", "page1", "page2", "page3", "page4"].map((id) => `page_session_${id}`).sort(),
338
+ );
339
+ });
340
+
341
+ test("breaks same-second ties toward the cookie listed later", async () => {
342
+ const exp = now() + 3600;
343
+ const ids = Array.from({ length: MAX_PAGE_SESSION_COOKIES }, (_, i) => `tie${i}`);
344
+ const jar = (await Promise.all(ids.map((id) => cookieFor(id, exp)))).join("; ");
345
+ const lines = await pageSessionSetCookies({ headers: { cookie: jar } }, "fresh", { dev: true });
346
+ expect(evicted(lines)).toEqual(["page_session_tie0"]);
347
+ });
348
+
349
+ test("evicts expired, forged and misnamed sessions but keeps legacy and own cookies", async () => {
350
+ const jar = [
351
+ await cookieFor("expired", now() - 10),
352
+ `page_session_forged=${await forgeToken({ pageId: "forged", exp: now() + 3600 }, "x")}`,
353
+ `page_session_misnamed=${await signPageSession({ pageId: "other", exp: now() + 3600 })}`,
354
+ `page_session=${await signPageSession({ pageId: "legacy", exp: now() + 3600 })}`,
355
+ await cookieFor("fresh"),
356
+ await cookieFor("kept"),
357
+ ].join("; ");
358
+ const lines = await pageSessionSetCookies({ headers: { cookie: jar } }, "fresh", {
359
+ dev: false,
360
+ });
361
+ expect(evicted(lines).sort()).toEqual([
362
+ "page_session_expired",
363
+ "page_session_forged",
364
+ "page_session_misnamed",
365
+ ]);
366
+ // Prod eviction cookies carry the same SameSite=None; Secure attributes.
367
+ for (const line of lines) expect(line).toContain("SameSite=None; Secure");
368
+ });
369
+ });
@@ -5,8 +5,9 @@
5
5
  * 2. `GET /p/:id` without cookie → 401.
6
6
  * 3. `POST /api/pages/:id/launch` (bearer) → 204 + Set-Cookie.
7
7
  * 4. `GET /p/:id` with cookie → 200 + SDK injected.
8
- * 5. `GET /p/:id` with cookie scoped to a DIFFERENT page id → 403.
9
- * 6. `GET /p/:id.json` with cookie → 200 + JSON metadata.
8
+ * 5. A session signed for a DIFFERENT page id is ignored → 401.
9
+ * 6. Two launched pages both load from one cookie jar.
10
+ * 7. `GET /p/:id.json` with cookie → 200 + JSON metadata.
10
11
  *
11
12
  * Uses the same in-process handler wiring as `pages-public-html.test.ts` so
12
13
  * we don't have to boot the full http server. The proxy in
@@ -26,7 +27,7 @@ import { closeDb, initDb } from "../be/db";
26
27
  import { handlePages } from "../http/pages";
27
28
  import { handlePagesPublic } from "../http/pages-public";
28
29
  import { getPathSegments, parseQueryParams } from "../http/utils";
29
- import { signPageSession } from "../utils/page-session";
30
+ import { MAX_PAGE_SESSION_COOKIES, signPageSession } from "../utils/page-session";
30
31
  import { listenOnFreePort } from "./test-net";
31
32
 
32
33
  const TEST_DB_PATH = "./test-pages-authed-mode.sqlite";
@@ -47,7 +48,7 @@ function createTestServer(): Server {
47
48
  /** Pull the cookie value out of a Set-Cookie header line. */
48
49
  function extractCookieValue(setCookie: string | null): string {
49
50
  expect(setCookie).toBeTruthy();
50
- const match = /page_session=([^;]+)/.exec(setCookie!);
51
+ const match = /page_session_[A-Za-z0-9_-]+=([^;]+)/.exec(setCookie!);
51
52
  expect(match).toBeTruthy();
52
53
  return match![1]!;
53
54
  }
@@ -132,7 +133,7 @@ describe("GET /p/:id — authed mode cookie gate (step-4)", () => {
132
133
  expect(text).toContain("class SwarmSDK");
133
134
  });
134
135
 
135
- test("cookie scoped to a different page id → 403", async () => {
136
+ test("a session signed for another page is ignored → 401", async () => {
136
137
  const idA = await createAuthedPage("page-a");
137
138
  const idB = await createAuthedPage("page-b");
138
139
 
@@ -140,13 +141,82 @@ describe("GET /p/:id — authed mode cookie gate (step-4)", () => {
140
141
  const exp = Math.floor(Date.now() / 1000) + 3600;
141
142
  const tokenForA = await signPageSession({ pageId: idA, exp });
142
143
 
143
- // … and try to use it for page B.
144
- const res = await fetch(`${BASE}/p/${idB}`, {
145
- headers: { Cookie: `page_session=${tokenForA}` },
146
- });
147
- expect(res.status).toBe(403);
148
- const body = (await res.json()) as { error: string };
149
- expect(body.error).toContain("different page id");
144
+ // … and try to use it for page B, under the legacy name and under B's own name.
145
+ for (const cookie of [`page_session=${tokenForA}`, `page_session_${idB}=${tokenForA}`]) {
146
+ const res = await fetch(`${BASE}/p/${idB}`, { headers: { Cookie: cookie } });
147
+ expect(res.status).toBe(401);
148
+ }
149
+ });
150
+
151
+ test("two launched pages both load from one cookie jar", async () => {
152
+ const idA = await createAuthedPage("tab-a");
153
+ const idB = await createAuthedPage("tab-b");
154
+ const jar: string[] = [];
155
+ for (const id of [idA, idB]) {
156
+ const launch = await fetch(`${BASE}/api/pages/${id}/launch`, {
157
+ method: "POST",
158
+ headers: { "X-Agent-ID": agentId },
159
+ });
160
+ expect(launch.status).toBe(204);
161
+ const setCookie = launch.headers.get("set-cookie")!;
162
+ expect(setCookie.startsWith(`page_session_${id}=`)).toBe(true);
163
+ jar.push(setCookie.split(";")[0]!);
164
+ }
165
+ const cookie = jar.join("; ");
166
+ for (const id of [idA, idB]) {
167
+ const res = await fetch(`${BASE}/p/${id}`, { headers: { Cookie: cookie } });
168
+ expect(res.status).toBe(200);
169
+ }
170
+ });
171
+
172
+ test("90 launches from one browser keep the Cookie header bounded", async () => {
173
+ // A browser jar: Set-Cookie with Max-Age=0 deletes, anything else upserts.
174
+ const jar = new Map<string, string>();
175
+ const cookieHeader = () => [...jar].map(([name, value]) => `${name}=${value}`).join("; ");
176
+ const ids: string[] = [];
177
+ for (let i = 0; i < 90; i++) {
178
+ const id = await createAuthedPage(`overflow-${i}`);
179
+ ids.push(id);
180
+ const launch = await fetch(`${BASE}/api/pages/${id}/launch`, {
181
+ method: "POST",
182
+ headers: { "X-Agent-ID": agentId, Cookie: cookieHeader() },
183
+ });
184
+ expect(launch.status).toBe(204);
185
+ for (const line of launch.headers.getSetCookie()) {
186
+ const [pair] = line.split(";");
187
+ const eq = pair!.indexOf("=");
188
+ const name = pair!.slice(0, eq);
189
+ if (/Max-Age=0(;|$)/.test(line)) jar.delete(name);
190
+ else jar.set(name, pair!.slice(eq + 1));
191
+ }
192
+ }
193
+
194
+ expect(jar.size).toBe(MAX_PAGE_SESSION_COOKIES);
195
+ expect(cookieHeader().length).toBeLessThan(8 * 1024);
196
+ // The newest launches keep their sessions; the oldest were evicted.
197
+ for (const id of ids.slice(-MAX_PAGE_SESSION_COOKIES)) {
198
+ const res = await fetch(`${BASE}/p/${id}`, { headers: { Cookie: cookieHeader() } });
199
+ expect(res.status).toBe(200);
200
+ }
201
+ expect(jar.has(`page_session_${ids[0]}`)).toBe(false);
202
+
203
+ // Control: the same sessions, operator-shaped as the dashboard mints them,
204
+ // overflow the 16 KB header limit when nothing evicts them.
205
+ const unbounded = (
206
+ await Promise.all(
207
+ ids.map(async (id) => {
208
+ const token = await signPageSession({
209
+ pageId: id,
210
+ exp: Math.floor(Date.now() / 1000) + 3600,
211
+ name: "guest-0123abcd",
212
+ op: true,
213
+ });
214
+ return `page_session_${id}=${token}`;
215
+ }),
216
+ )
217
+ ).join("; ");
218
+ const overflow = await fetch(`${BASE}/p/${ids.at(-1)}`, { headers: { Cookie: unbounded } });
219
+ expect(overflow.status).toBe(431);
150
220
  });
151
221
 
152
222
  test("/p/:id.json with cookie → 200 + JSON metadata", async () => {
@@ -47,7 +47,7 @@ function createTestServer(): Server {
47
47
 
48
48
  function extractCookieValue(setCookie: string | null): string {
49
49
  expect(setCookie).toBeTruthy();
50
- const match = /page_session=([^;]+)/.exec(setCookie!);
50
+ const match = /page_session_[A-Za-z0-9_-]+=([^;]+)/.exec(setCookie!);
51
51
  expect(match).toBeTruthy();
52
52
  return match![1]!;
53
53
  }
@@ -140,7 +140,7 @@ describe("GET /p/:id — password mode (step-5)", () => {
140
140
  expect(res.status).toBe(200);
141
141
  expect(res.headers.get("content-type")?.toLowerCase()).toContain("text/html");
142
142
  const cookieHeader = res.headers.get("set-cookie");
143
- expect(cookieHeader).toContain("page_session=");
143
+ expect(cookieHeader).toContain("page_session_");
144
144
  const text = await res.text();
145
145
  expect(text).toContain("<h1>open sesame</h1>");
146
146
  // BROWSER_SDK_JS sentinel.
@@ -158,7 +158,7 @@ describe("GET /p/:id — password mode (step-5)", () => {
158
158
  headers: { Authorization: `Basic ${basic}` },
159
159
  });
160
160
  expect(res.status).toBe(200);
161
- expect(res.headers.get("set-cookie")).toContain("page_session=");
161
+ expect(res.headers.get("set-cookie")).toContain("page_session_");
162
162
  const text = await res.text();
163
163
  expect(text).toContain("<h1>via basic</h1>");
164
164
  });
@@ -222,7 +222,7 @@ describe("GET /p/:id — password mode (step-5)", () => {
222
222
  const id = await createPasswordPage("json-key", "swordfish");
223
223
  const res = await fetch(`${BASE}/p/${id}.json?key=swordfish`);
224
224
  expect(res.status).toBe(200);
225
- expect(res.headers.get("set-cookie")).toContain("page_session=");
225
+ expect(res.headers.get("set-cookie")).toContain("page_session_");
226
226
  });
227
227
 
228
228
  test("cross-page cookie reuse → 401 + password prompt (cookie is silently ignored for password mode)", async () => {
@@ -252,7 +252,7 @@ describe("GET /p/:id — password mode (step-5)", () => {
252
252
  headers: { Cookie: `page_session=${cookieForA}` },
253
253
  });
254
254
  expect(recover.status).toBe(200);
255
- expect(recover.headers.get("set-cookie")).toContain("page_session=");
255
+ expect(recover.headers.get("set-cookie")).toContain("page_session_");
256
256
  });
257
257
 
258
258
  test("malformed Basic header (no colon) → 401 (no crash)", async () => {
@@ -105,6 +105,7 @@ const OPERATOR_OR_USER_VERBS: PermissionVerb[] = [
105
105
  "approval.respond",
106
106
  "runtime_controller.write",
107
107
  "runtime_pool.write",
108
+ "agent.token.manage",
108
109
  ];
109
110
 
110
111
  const LEAD_OR_OPERATOR_VERBS: PermissionVerb[] = ["models.catalog.write"];
@@ -75,9 +75,11 @@ async function createPage(
75
75
  async function launchPage(pageId: string, agentId: string): Promise<string> {
76
76
  const response = await api("POST", `/api/pages/${pageId}/launch`, { agentId });
77
77
  expect(response.status).toBe(204);
78
- const cookie = response.headers.get("set-cookie")?.match(/page_session=([^;]+)/)?.[1];
78
+ const cookie = response.headers
79
+ .get("set-cookie")
80
+ ?.match(/page_session_[A-Za-z0-9_-]+=([^;]+)/)?.[1];
79
81
  expect(cookie).toBeTruthy();
80
- return `page_session=${cookie}`;
82
+ return `page_session_${pageId}=${cookie}`;
81
83
  }
82
84
 
83
85
  async function createUserToken(name: string): Promise<{ id: string; token: string }> {
@@ -107,17 +109,21 @@ async function launchPageAsUser(pageId: string, token: string): Promise<string>
107
109
  headers: { Authorization: `Bearer ${token}` },
108
110
  });
109
111
  expect(response.status).toBe(204);
110
- const cookie = response.headers.get("set-cookie")?.match(/page_session=([^;]+)/)?.[1];
112
+ const cookie = response.headers
113
+ .get("set-cookie")
114
+ ?.match(/page_session_[A-Za-z0-9_-]+=([^;]+)/)?.[1];
111
115
  expect(cookie).toBeTruthy();
112
- return `page_session=${cookie}`;
116
+ return `page_session_${pageId}=${cookie}`;
113
117
  }
114
118
 
115
119
  async function unlockPasswordPage(pageId: string, password: string): Promise<string> {
116
120
  const response = await fetch(`${baseUrl}/p/${pageId}?key=${encodeURIComponent(password)}`);
117
121
  expect(response.status).toBe(200);
118
- const cookie = response.headers.get("set-cookie")?.match(/page_session=([^;]+)/)?.[1];
122
+ const cookie = response.headers
123
+ .get("set-cookie")
124
+ ?.match(/page_session_[A-Za-z0-9_-]+=([^;]+)/)?.[1];
119
125
  expect(cookie).toBeTruthy();
120
- return `page_session=${cookie}`;
126
+ return `page_session_${pageId}=${cookie}`;
121
127
  }
122
128
 
123
129
  function openSocket(
@@ -443,7 +449,7 @@ describe("realtime transport", () => {
443
449
  const passwordPage = await createPage(agentId, "password", "secret");
444
450
  expect(await socketAccepted({ Origin: baseUrl }, pageSocketUrl(passwordPage))).toBe(false);
445
451
  const passwordCookie = await unlockPasswordPage(passwordPage, "secret");
446
- expect(passwordCookie).toContain("page_session=");
452
+ expect(passwordCookie).toContain("page_session_");
447
453
  });
448
454
 
449
455
  test("persists room state across an API restart and cascades page deletion", async () => {
@@ -9,6 +9,7 @@ import { handleSessions } from "../http/sessions";
9
9
  import { getPathSegments, parseQueryParams } from "../http/utils";
10
10
  import { ACPAdapter } from "../providers/acp-adapter";
11
11
  import { setRequestAuth } from "../utils/request-auth-context";
12
+ import { ACP_SDK_PATH, serveFakeAcpAgent } from "./acp-fake-agent";
12
13
  import { listenOnFreePort } from "./test-net";
13
14
  import { CHILD_PROCESS_TEST_BUDGET_MS } from "./test-proc";
14
15
 
@@ -69,12 +70,11 @@ for (const setupFails of [false, true]) {
69
70
  const api = await startApi(resolveRevoke);
70
71
  const script = join(tmpdir(), `runtime-child-${crypto.randomUUID()}.ts`);
71
72
  const capture = `${script}.auth`;
72
- const sdk = join(process.cwd(), "node_modules/@agentclientprotocol/sdk/dist/acp.js");
73
73
  await Bun.write(
74
74
  script,
75
75
  `
76
76
  import { Readable, Writable } from "node:stream";
77
- import { AgentSideConnection, PROTOCOL_VERSION, ndJsonStream } from ${JSON.stringify(sdk)};
77
+ import { PROTOCOL_VERSION, agent, methods, ndJsonStream } from ${JSON.stringify(ACP_SDK_PATH)};
78
78
  class Agent {
79
79
  async initialize() {
80
80
  return { protocolVersion: PROTOCOL_VERSION, agentCapabilities: { loadSession: false } };
@@ -89,7 +89,7 @@ class Agent {
89
89
  async prompt() { return { stopReason: "end_turn" }; }
90
90
  async cancel() {}
91
91
  }
92
- new AgentSideConnection(() => new Agent(), ndJsonStream(Writable.toWeb(process.stdout), Readable.toWeb(process.stdin)));
92
+ ${serveFakeAcpAgent("Agent")}
93
93
  `,
94
94
  );
95
95
  try {
@@ -636,6 +636,8 @@ describe("script connections", () => {
636
636
  markMigrationApplied(database, "227_inbox_item_state_last_read_at.sql");
637
637
  // 231 indexes agent_tasks columns that this fixture's id-only agent_tasks lacks.
638
638
  markMigrationApplied(database, "231_slack_tasks_last_updated_index.sql");
639
+ // 232 rebuilds session_tokens with the 207 columns, which this fixture skipped.
640
+ markMigrationApplied(database, "232_agent_session_tokens.sql");
639
641
 
640
642
  const id = crypto.randomUUID();
641
643
  const now = new Date().toISOString();