@desplega.ai/agent-swarm 2.4.4 → 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 (221) hide show
  1. package/dist/{acp-adapter-thq04adh.js → acp-adapter-2wdkktv6.js} +1 -1
  2. package/dist/{actions-j6ywh6x5.js → actions-c40ebcm4.js} +18 -18
  3. package/dist/{amp-adapter-k8b5xmj8.js → amp-adapter-wb67m95z.js} +2 -2
  4. package/dist/{app-rb7vc02s.js → app-353vre8g.js} +3 -3
  5. package/dist/{assistant-ftta1zjf.js → assistant-sj1njh23.js} +21 -21
  6. package/dist/{boot-reembed-srp6v6af.js → boot-reembed-b9c3az3c.js} +1 -1
  7. package/dist/{boot-reembed-xns7h5h9.js → boot-reembed-qdkvxeem.js} +1 -1
  8. package/dist/{boot-scrub-sweep-3qqrh2mz.js → boot-scrub-sweep-m3wh8eks.js} +2 -2
  9. package/dist/{boot-seeds-9ks8hce7.js → boot-seeds-j2273s54.js} +4 -4
  10. package/dist/{claude-adapter-k5ksyres.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-n5a5eqks.js → claude-managed-setup-smrqk5j5.js} +3 -3
  13. package/dist/{claude-sdk-session-2j3j4kwn.js → claude-sdk-session-38xnz21e.js} +8284 -8267
  14. package/dist/{cli-cynvc9fj.js → cli-018r4jq6.js} +5 -5
  15. package/dist/{cli-9fhkkpj5.js → cli-1f3qzw6y.js} +1 -1
  16. package/dist/{cli-dqb4n629.js → cli-1sp861s4.js} +7 -7
  17. package/dist/{cli-gqvh8d8a.js → cli-1syn2xy1.js} +10 -10
  18. package/dist/{cli-qkhc5aqy.js → cli-37j04k05.js} +11 -11
  19. package/dist/{cli-1d70crsy.js → cli-3a45zcgm.js} +1 -1
  20. package/dist/{cli-t7cyef5g.js → cli-43x64hv3.js} +4 -4
  21. package/dist/{cli-hj3debgw.js → cli-4zfqgjc5.js} +2 -2
  22. package/dist/{cli-55qmbp6z.js → cli-7c9qs530.js} +1 -1
  23. package/dist/{cli-9qtjfd60.js → cli-848hxdwj.js} +1 -1
  24. package/dist/{cli-pccfmtb5.js → cli-94gwyg7m.js} +7 -7
  25. package/dist/{cli-51y09c6p.js → cli-9hstj09h.js} +5 -5
  26. package/dist/{cli-zrwpctp4.js → cli-9yqgm9nd.js} +22 -22
  27. package/dist/{cli-esxerzg7.js → cli-a92r1127.js} +1 -1
  28. package/dist/{cli-b8k7epj1.js → cli-be2161h1.js} +7 -7
  29. package/dist/{cli-xc27fwzg.js → cli-bnes4z6b.js} +2 -2
  30. package/dist/{cli-zrk6jxv8.js → cli-cxxe61f6.js} +1 -1
  31. package/dist/{cli-amyhm3gj.js → cli-dk44dxec.js} +1 -1
  32. package/dist/{cli-nc35pxb7.js → cli-eww120g8.js} +1 -0
  33. package/dist/{cli-a0kcty51.js → cli-f01h25yt.js} +1 -1
  34. package/dist/{cli-82d3nkc3.js → cli-f0g3pgvf.js} +1 -1
  35. package/dist/{cli-1ephgwnq.js → cli-ftc1g5tj.js} +2 -2
  36. package/dist/{cli-2v045xyf.js → cli-fv5hepyy.js} +3 -3
  37. package/dist/{cli-bnz0r3kc.js → cli-g35m2xwm.js} +4 -4
  38. package/dist/{cli-qje9k2nh.js → cli-g9ypd5en.js} +12 -2
  39. package/dist/{cli-w6nadkh4.js → cli-j99azg3g.js} +3 -3
  40. package/dist/{cli-er83cjbn.js → cli-k8g16gz3.js} +416 -116
  41. package/dist/{cli-4dg55wsk.js → cli-kpw04va0.js} +1 -1
  42. package/dist/{cli-3cgngyd8.js → cli-ms85309d.js} +33 -15
  43. package/dist/{cli-v3yr2gbg.js → cli-n72rg53p.js} +1471 -635
  44. package/dist/{cli-37yb7307.js → cli-p14m2cnp.js} +1 -1
  45. package/dist/{cli-2zqpkvtt.js → cli-qfxc84gh.js} +1 -1
  46. package/dist/{cli-44159k28.js → cli-qwhy2qma.js} +1 -1
  47. package/dist/{cli-2y91wnbh.js → cli-st8aw1m6.js} +221 -9
  48. package/dist/{cli-vq97g1jq.js → cli-tw4p90jb.js} +92 -192
  49. package/dist/{cli-te6b5wvg.js → cli-v0yrfmdh.js} +1 -1
  50. package/dist/{cli-xj0re5tm.js → cli-v8fjakwf.js} +1 -1
  51. package/dist/{cli-p20r8w42.js → cli-wt0jf9hm.js} +215 -80
  52. package/dist/{cli-q6r7t299.js → cli-xpa64rek.js} +33 -11
  53. package/dist/{cli-vdm20wej.js → cli-z7d29rw9.js} +2 -2
  54. package/dist/cli.js +19 -19
  55. package/dist/{codex-adapter-968xwfy7.js → codex-adapter-tn6k5mkg.js} +1 -1
  56. package/dist/{codex-session-runner-ht4z99fa.js → codex-session-runner-7vjt7jbw.js} +1 -1
  57. package/dist/{commands-pcymaeg9.js → commands-t7rz9146.js} +2 -2
  58. package/dist/{controller-fpjyk46r.js → controller-m2jasma5.js} +2 -2
  59. package/dist/{controllers-connect-mwzx1fh6.js → controllers-connect-9ftpex99.js} +19 -19
  60. package/dist/{cursor-adapter-cp4s2e2f.js → cursor-adapter-mjv79e4z.js} +1 -1
  61. package/dist/{db-xsjg9jqq.js → db-6sfrn9g3.js} +1 -1
  62. package/dist/{db-sgd12bt9.js → db-rn673r06.js} +3 -3
  63. package/dist/{dispatcher-x0jfw2c9.js → dispatcher-c155xz0p.js} +17 -17
  64. package/dist/{dsh-adapter-jrq7ma7h.js → dsh-adapter-3mmszc1x.js} +1 -1
  65. package/dist/{external-channel-8njj4h3b.js → external-channel-35k6tkv8.js} +2 -2
  66. package/dist/{grok-adapter-kkr97v2f.js → grok-adapter-tfrq7tg1.js} +2 -2
  67. package/dist/handlers-7gw64dxw.js +39 -0
  68. package/dist/{heartbeat-wd03zhn0.js → heartbeat-dyg4npkp.js} +20 -20
  69. package/dist/{hook-f0vghj42.js → hook-drspbwpe.js} +1 -1
  70. package/dist/{hook-xzj2ts2p.js → hook-x26b31bg.js} +1 -1
  71. package/dist/{http-4hwxv62p.js → http-pk972xyw.js} +355 -216
  72. package/dist/{index-ww2vznkp.js → index-arytgj5q.js} +1 -1
  73. package/dist/{index-hg0x2cdv.js → index-bgdyvcnv.js} +17 -17
  74. package/dist/{index-c4ykd0f1.js → index-dr94zbnh.js} +13348 -11611
  75. package/dist/{index-gdh8jrdm.js → index-gabbcdn2.js} +17 -17
  76. package/dist/{index-97q4xn03.js → index-jwxkkex2.js} +129 -75
  77. package/dist/index-tfb26zhy.js +41 -0
  78. package/dist/{keepalive-hrh7xnr9.js → keepalive-7yrng7px.js} +3 -3
  79. package/dist/{lead-ee8vazz2.js → lead-7e02z839.js} +29 -29
  80. package/dist/{maintenance-e8wnwnqx.js → maintenance-e6cemdqt.js} +1 -1
  81. package/dist/{node-3v3f08f7.js → node-de6akczr.js} +10 -9
  82. package/dist/{oauth-refresh-sweep-zm0yj4sj.js → oauth-refresh-sweep-yw3xdbcj.js} +2 -2
  83. package/dist/{onboard-dtwcq603.js → onboard-0a9y5yrc.js} +2 -2
  84. package/dist/{otel-impl-w3fbhpn0.js → otel-impl-trjpkfb7.js} +1 -1
  85. package/dist/{pricing-refresh-j38dehfs.js → pricing-refresh-d2b5my0y.js} +3 -3
  86. package/dist/{rbac-roles-ejxx9b35.js → rbac-roles-hey3yv9d.js} +2 -2
  87. package/dist/{rbac-roles-ae8yttq4.js → rbac-roles-nxh5ahwk.js} +1 -1
  88. package/dist/{registry-rxw10xzg.js → registry-ajhexe44.js} +1 -1
  89. package/dist/{render-v2-n16n0phe.js → render-v2-rr3p2atg.js} +3 -3
  90. package/dist/{resume-z0fgjc38.js → resume-2efkf1s1.js} +5 -5
  91. package/dist/{session-access-cv3mq6mc.js → session-access-s2mttxpw.js} +4 -4
  92. package/dist/{setup-mafhj3yt.js → setup-vypma5yt.js} +1 -1
  93. package/dist/{work-objects-8b86mgjp.js → work-objects-rvhckdkp.js} +20 -20
  94. package/dist/{worker-16s75hm5.js → worker-5snytpbd.js} +29 -29
  95. package/openapi.json +9 -2
  96. package/package.json +7 -7
  97. package/src/apps/catalog.generated.json +206 -4
  98. package/src/apps/page-validator.ts +13 -1
  99. package/src/be/chunking.ts +42 -9
  100. package/src/be/db/tasks/draft-text.ts +15 -0
  101. package/src/be/db/tasks/read.ts +14 -1
  102. package/src/be/db.ts +5 -1
  103. package/src/be/memory/index-content.ts +36 -17
  104. package/src/be/memory/providers/openai-embedding.ts +115 -61
  105. package/src/be/memory/providers/sqlite-store.ts +69 -23
  106. package/src/be/memory/reranker.ts +31 -1
  107. package/src/be/memory/search-candidates.ts +47 -0
  108. package/src/be/memory/types.ts +14 -0
  109. package/src/be/scripts/typecheck.ts +2 -0
  110. package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +106 -54
  111. package/src/be/seed-scripts/index.ts +2 -2
  112. package/src/be/swarm-config-guard.ts +24 -1
  113. package/src/be/task-attachment-store.ts +30 -0
  114. package/src/extensions/catalog.generated.json +4 -4
  115. package/src/extensions/contract-types.generated.ts +1 -1
  116. package/src/extensions/contract.ts +7 -0
  117. package/src/fs/provider.ts +13 -1
  118. package/src/heartbeat/heartbeat.ts +31 -11
  119. package/src/http/fs.ts +54 -34
  120. package/src/http/mcp-body.ts +37 -0
  121. package/src/http/mcp-user.ts +3 -5
  122. package/src/http/mcp.ts +6 -7
  123. package/src/http/memory.ts +26 -15
  124. package/src/http/page-proxy.ts +61 -5
  125. package/src/http/pages-public.ts +27 -44
  126. package/src/http/pages.ts +4 -4
  127. package/src/http/script-runs.ts +2 -0
  128. package/src/http/users.ts +12 -1
  129. package/src/integrations/chat-sdk/adapters.ts +7 -3
  130. package/src/integrations/chat-sdk/attachments.ts +90 -12
  131. package/src/integrations/chat-sdk/facts.ts +120 -0
  132. package/src/integrations/chat-sdk/inbound.ts +156 -8
  133. package/src/integrations/chat-sdk/limits.ts +24 -0
  134. package/src/integrations/chat-sdk/outbound-files.ts +186 -0
  135. package/src/integrations/chat-sdk/outbound.ts +234 -4
  136. package/src/integrations/chat-sdk/relay.ts +4 -1
  137. package/src/integrations/chat-sdk/router.ts +28 -2
  138. package/src/integrations/chat-sdk/steering.ts +9 -0
  139. package/src/integrations/chat-sdk/templates.ts +14 -0
  140. package/src/integrations/chat-sdk/thread-buffer.ts +4 -0
  141. package/src/mcp-limits.ts +20 -0
  142. package/src/pages/browser-sdk.ts +8 -3
  143. package/src/pages/feedback-overlay.ts +1 -1
  144. package/src/providers/amp-adapter.ts +1 -1
  145. package/src/providers/codex-adapter.ts +106 -119
  146. package/src/providers/codex-agents-md.ts +35 -229
  147. package/src/providers/cursor-adapter.ts +1 -1
  148. package/src/realtime/transport.ts +1 -2
  149. package/src/script-workflows/workflow-ctx.ts +1 -0
  150. package/src/scripts-runtime/types/stdlib.d.ts +2 -0
  151. package/src/scripts-runtime/types/swarm-extension.d.ts +8 -0
  152. package/src/scripts-runtime/types/swarm-sdk.d.ts +2 -0
  153. package/src/server-user.ts +2 -0
  154. package/src/server.ts +2 -0
  155. package/src/slack/assistant.ts +2 -2
  156. package/src/slack/files.ts +2 -0
  157. package/src/slack/handlers.ts +4 -4
  158. package/src/slack/inbound-files.ts +39 -6
  159. package/src/slack/thread-buffer.ts +2 -1
  160. package/src/tasks/task-terminal-effects.ts +21 -17
  161. package/src/tests/apps-card-grid.test.ts +197 -0
  162. package/src/tests/apps-catalog-additions.test.ts +120 -0
  163. package/src/tests/apps-tree.test.ts +129 -0
  164. package/src/tests/chat-sdk-attachments-reply.test.ts +52 -0
  165. package/src/tests/chat-sdk-config.test.ts +11 -0
  166. package/src/tests/chat-sdk-facts.test.ts +23 -0
  167. package/src/tests/chat-sdk-inbound.test.ts +558 -4
  168. package/src/tests/chat-sdk-outbound-files.test.ts +652 -0
  169. package/src/tests/codex-adapter.test.ts +167 -242
  170. package/src/tests/config-api-only-keys.test.ts +9 -0
  171. package/src/tests/cors-allowlist.test.ts +1 -1
  172. package/src/tests/extensions-example-slack-reply-gate.test.ts +262 -91
  173. package/src/tests/heartbeat-checklist.test.ts +33 -0
  174. package/src/tests/heartbeat-jev-gate-credentials.test.ts +44 -0
  175. package/src/tests/heartbeat-jev-gate.test.ts +242 -1
  176. package/src/tests/kv-page-proxy.test.ts +91 -5
  177. package/src/tests/launch-password-rejection.test.ts +2 -2
  178. package/src/tests/mcp-input-caps.test.ts +294 -0
  179. package/src/tests/memory-chunked-search-starvation.test.ts +200 -0
  180. package/src/tests/memory-embedding-input-cap.test.ts +114 -0
  181. package/src/tests/memory.test.ts +14 -0
  182. package/src/tests/page-guest-session.test.ts +19 -8
  183. package/src/tests/page-proxy-authed.test.ts +1 -1
  184. package/src/tests/page-proxy.test.ts +17 -10
  185. package/src/tests/page-session.test.ts +102 -1
  186. package/src/tests/pages-authed-mode.test.ts +82 -12
  187. package/src/tests/pages-password-mode.test.ts +5 -5
  188. package/src/tests/realtime-transport.test.ts +13 -7
  189. package/src/tests/script-runs-http.test.ts +84 -0
  190. package/src/tests/scripts-http.test.ts +10 -1
  191. package/src/tests/swarm-config-export-to-env.test.ts +22 -3
  192. package/src/tests/task-audio-transcription.test.ts +299 -0
  193. package/src/tests/task-completion-memory-chunking.test.ts +434 -0
  194. package/src/tests/task-terminal-secret-scrub.test.ts +6 -1
  195. package/src/tests/transcription.test.ts +299 -0
  196. package/src/tests/workflow-system-one-decision-providers.test.ts +98 -1
  197. package/src/tests/workflow-system-one-decision.test.ts +2 -1
  198. package/src/tools/chat-access.ts +10 -0
  199. package/src/tools/chat-post.ts +31 -11
  200. package/src/tools/chat-reply.ts +32 -10
  201. package/src/tools/memory-search.ts +3 -1
  202. package/src/tools/system-one-decide.ts +17 -4
  203. package/src/types.ts +6 -0
  204. package/src/utils/config-env-export.ts +3 -1
  205. package/src/utils/page-session.ts +133 -20
  206. package/src/utils/transcription.ts +246 -0
  207. package/src/workflows/executors/system-one-decision.ts +28 -7
  208. package/src/workflows/executors/system-one-providers.ts +39 -4
  209. package/templates/extensions/slack-reply-gate/README.md +24 -22
  210. package/templates/extensions/slack-reply-gate/hooks.ts +146 -95
  211. package/templates/extensions/slack-reply-gate/manifest.json +2 -2
  212. package/templates/skills/apps/SKILL.md +28 -6
  213. package/templates/skills/apps/content.md +28 -6
  214. package/templates/skills/chat-sdk/content.md +6 -2
  215. package/templates/skills/code-quality/content.md +2 -1
  216. package/templates/skills/heartbeat-runbook/content.md +5 -5
  217. package/templates/skills/pages/content.md +6 -4
  218. package/templates/skills/swarm-scripts/SKILL.md +2 -2
  219. package/templates/skills/swarm-scripts/content.md +2 -2
  220. package/dist/handlers-0pvee9nw.js +0 -39
  221. package/dist/index-thm0y6a8.js +0 -41
@@ -1,6 +1,7 @@
1
1
  import { Database } from "bun:sqlite";
2
2
  import { afterAll, beforeAll, describe, expect, test } from "bun:test";
3
3
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import * as db from "../be/db";
4
5
  import {
5
6
  closeDb,
6
7
  createAgent,
@@ -8,9 +9,16 @@ import {
8
9
  getApprovalRequestById,
9
10
  initDb,
10
11
  startTask,
12
+ upsertSwarmConfig,
11
13
  } from "../be/db";
12
14
  import gate from "../be/seed-scripts/catalog/heartbeat-jev-gate";
13
15
  import { registerRequestHumanInputTool } from "../tools/request-human-input";
16
+ import { registerSystemOneDecideTool } from "../tools/system-one-decide";
17
+ import type { ExecutorDependencies } from "../workflows/executors/base";
18
+ import {
19
+ SystemOneDecisionConfigSchema,
20
+ SystemOneDecisionExecutor,
21
+ } from "../workflows/executors/system-one-decision";
14
22
 
15
23
  const LEAD = "11111111-2222-4333-8444-555555555555";
16
24
  const NOW = Date.parse("2026-10-15T14:00:00.000Z");
@@ -176,6 +184,51 @@ const jevFails = (message: string) => async () => ({
176
184
  status: 200,
177
185
  data: { success: false, message },
178
186
  });
187
+ const CF_ACCOUNT = "0123456789abcdef0123456789abcdef";
188
+
189
+ /**
190
+ * Stands in for `ctx.swarm.systemOne_decide` with the real executor behind it, as
191
+ * the tool runs it: Workers AI answers in its REST envelope, and the executor
192
+ * unwraps it. `urls` records where each request went.
193
+ */
194
+ function clefDecide(p: number) {
195
+ const urls: string[] = [];
196
+ const executor = new SystemOneDecisionExecutor({ db } as unknown as ExecutorDependencies, {
197
+ env: {},
198
+ getApiKey: async (id) => (id === "cloudflare" ? "cf-example-token" : null),
199
+ getServerUrl: async (id) => (id === "cloudflare" ? CF_ACCOUNT : null),
200
+ fetch: (async (input: string | URL | Request) => {
201
+ urls.push(String(input));
202
+ return Response.json({
203
+ result: {
204
+ model: "clef",
205
+ answers: { act: { type: "noul", noul: p } },
206
+ usage: { input_tokens: 739, output_tokens: 0 },
207
+ },
208
+ success: true,
209
+ errors: [],
210
+ messages: [],
211
+ });
212
+ }) as typeof fetch,
213
+ });
214
+ const decide = async (args: Record<string, any>) => {
215
+ const returns = Object.fromEntries(
216
+ Object.entries(args.questions).map(([id, q]: [string, any]) => [id, { type: q.type }]),
217
+ );
218
+ const decided = await executor.decide(
219
+ SystemOneDecisionConfigSchema.parse({ ...args, returns }),
220
+ );
221
+ return {
222
+ success: true,
223
+ status: 200,
224
+ data: decided.ok
225
+ ? { success: true, ...decided.output }
226
+ : { success: false, message: decided.error },
227
+ };
228
+ };
229
+ return { urls, decide };
230
+ }
231
+
179
232
  const run = (ctx: unknown, args: Record<string, unknown> = {}) =>
180
233
  gate({ leadAgentId: LEAD, ...args }, ctx) as Promise<any>;
181
234
 
@@ -677,6 +730,101 @@ describe("heartbeat-jev-gate with a provider", () => {
677
730
  });
678
731
  });
679
732
 
733
+ describe("heartbeat-jev-gate on Clef", () => {
734
+ const cloudflareOnly = { CLOUDFLARE_API_TOKEN: "x", CLOUDFLARE_ACCOUNT_ID: CF_ACCOUNT };
735
+
736
+ test("runs on Clef when only the Cloudflare token and account id are set", async () => {
737
+ const clef = clefDecide(0.15);
738
+ const { ctx, calls } = fakeCtx({ configs: cloudflareOnly, jev: clef.decide });
739
+ const verdict = await run(ctx);
740
+ // 0.15 is quiet only under Clef's own measured threshold, not Jev's 0.5 neighbourhood.
741
+ expect(verdict).toEqual({
742
+ quiet: true,
743
+ reason: "clef p(needs action)=0.150 < 0.2 via cloudflare",
744
+ });
745
+ expect(calls.decides[0]).toMatchObject({ provider: "cloudflare", model: "clef" });
746
+ expect(clef.urls).toEqual([
747
+ `https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT}/ai/run/@cf/cloudflare/clef`,
748
+ ]);
749
+ expect(calls.prompts).toHaveLength(0);
750
+ });
751
+
752
+ test("at or above 0.2 Clef wakes the Lead, and the summary names Clef", async () => {
753
+ const { ctx } = fakeCtx({ configs: cloudflareOnly, jev: clefDecide(0.25).decide });
754
+ const verdict = await run(ctx);
755
+ expect(verdict.quiet).toBe(false);
756
+ expect(verdict.reason).toBe("clef p(needs action)=0.250 >= 0.2 via cloudflare");
757
+ expect(verdict.summary).toStartWith(
758
+ "Clef p(needs action) = 0.250 (threshold 0.2, cloudflare).",
759
+ );
760
+ });
761
+
762
+ test("CLOUDFLARE_API_KEY is accepted in place of CLOUDFLARE_API_TOKEN", async () => {
763
+ const { ctx } = fakeCtx({
764
+ configs: { CLOUDFLARE_API_KEY: "x", CLOUDFLARE_ACCOUNT_ID: CF_ACCOUNT },
765
+ jev: clefDecide(0.1).decide,
766
+ });
767
+ expect((await run(ctx)).reason).toContain("via cloudflare");
768
+ });
769
+
770
+ test("auto prefers Jev when a Jev key is also set", async () => {
771
+ const { ctx, calls } = fakeCtx({
772
+ configs: { ...cloudflareOnly, OPENROUTER_API_KEY: "x" },
773
+ jev: jevSays(0.1),
774
+ });
775
+ expect((await run(ctx)).reason).toContain("via openrouter");
776
+ expect(calls.decides[0]!.provider).toBe("openrouter");
777
+ });
778
+
779
+ test("an explicit cloudflare provider wins over a Jev key", async () => {
780
+ const { ctx, calls } = fakeCtx({
781
+ configs: {
782
+ ...cloudflareOnly,
783
+ TYPESAFE_API_KEY: "x",
784
+ HEARTBEAT_JEV_GATE_PROVIDER: "cloudflare",
785
+ },
786
+ jev: clefDecide(0.1).decide,
787
+ });
788
+ expect((await run(ctx)).reason).toContain("via cloudflare");
789
+ expect(calls.decides[0]!.provider).toBe("cloudflare");
790
+ });
791
+
792
+ test("an unknown provider value falls back to auto, even an Object prototype name", async () => {
793
+ for (const pref of ["constructor", "toString", "jev"]) {
794
+ const { ctx } = fakeCtx({
795
+ configs: { TYPESAFE_API_KEY: "x", HEARTBEAT_JEV_GATE_PROVIDER: pref },
796
+ jev: jevSays(0.1),
797
+ });
798
+ expect((await run(ctx)).reason).toContain("via typesafe");
799
+ }
800
+ });
801
+
802
+ test("a configured threshold applies to Clef too", async () => {
803
+ const { ctx } = fakeCtx({
804
+ configs: { ...cloudflareOnly, HEARTBEAT_JEV_GATE_THRESHOLD: "0.5" },
805
+ jev: clefDecide(0.3).decide,
806
+ });
807
+ const verdict = await run(ctx, { dryRun: true });
808
+ expect(verdict.quiet).toBe(true);
809
+ expect(verdict.threshold).toBe(0.5);
810
+ });
811
+
812
+ test("a token without an account id is not a usable provider", async () => {
813
+ const { ctx, calls } = fakeCtx({ configs: { CLOUDFLARE_API_TOKEN: "x" } });
814
+ const verdict = await run(ctx, { dryRun: true });
815
+ expect(verdict.reason).toStartWith("jev-gate inactive:");
816
+ expect(calls.decides).toHaveLength(0);
817
+ });
818
+
819
+ test("a Clef error throws with the provider named, so the heartbeat fails open", async () => {
820
+ const { ctx } = fakeCtx({
821
+ configs: cloudflareOnly,
822
+ jev: jevFails("CLOUDFLARE_API_TOKEN was rejected by Cloudflare Workers AI (HTTP 401)."),
823
+ });
824
+ await expect(run(ctx)).rejects.toThrow("clef cloudflare: CLOUDFLARE_API_TOKEN was rejected");
825
+ });
826
+ });
827
+
680
828
  describe("heartbeat-jev-gate without a provider", () => {
681
829
  const inactive = (verdict: any) => {
682
830
  expect(verdict.quiet).toBe(false);
@@ -697,6 +845,8 @@ describe("heartbeat-jev-gate without a provider", () => {
697
845
  expect(prompt.questions[0].type).toBe("approval");
698
846
  expect(prompt.questions[0].description).toContain("TYPESAFE_API_KEY");
699
847
  expect(prompt.questions[0].description).toContain("OPENROUTER_API_KEY");
848
+ expect(prompt.questions[0].description).toContain("CLOUDFLARE_API_TOKEN");
849
+ expect(prompt.questions[0].description).toContain("CLOUDFLARE_ACCOUNT_ID");
700
850
  expect(prompt.questions[0].description).toContain("$0.00004");
701
851
  expect(kv.get("setup")).toMatchObject({ status: "asked", requestId: "req-1" });
702
852
 
@@ -755,13 +905,104 @@ describe("heartbeat-jev-gate without a provider", () => {
755
905
  const verdict = await run(ctx, { dryRun: true });
756
906
  inactive(verdict);
757
907
  expect(verdict.dryRun).toBe(true);
758
- expect(verdict.prompt.title).toContain("Jev");
908
+ expect(verdict.prompt.title).toContain("decision model");
759
909
  expect(verdict.state.agents).toBe("- Online: 6 (4 idle, 2 busy), Offline: 5");
760
910
  expect(calls.prompts).toHaveLength(0);
761
911
  expect(calls.kvSets).toBe(0);
762
912
  });
763
913
  });
764
914
 
915
+ describe("heartbeat-jev-gate on the real system-one-decide tool", () => {
916
+ let agentId: string;
917
+ let prevBase: string | undefined;
918
+ const realFetch = globalThis.fetch;
919
+ beforeAll(async () => {
920
+ initDb(":memory:");
921
+ agentId = (await createAgent({ name: "jev-gate-decide", isLead: true, status: "idle" })).id;
922
+ await upsertSwarmConfig({ scope: "global", key: "CLOUDFLARE_ACCOUNT_ID", value: CF_ACCOUNT });
923
+ await upsertSwarmConfig({
924
+ scope: "global",
925
+ key: "CLOUDFLARE_API_TOKEN",
926
+ value: "cf-example-token",
927
+ isSecret: true,
928
+ });
929
+ prevBase = process.env.OPENROUTER_BASE_URL;
930
+ });
931
+ afterAll(() => {
932
+ globalThis.fetch = realFetch;
933
+ if (prevBase === undefined) delete process.env.OPENROUTER_BASE_URL;
934
+ else process.env.OPENROUTER_BASE_URL = prevBase;
935
+ closeDb();
936
+ });
937
+
938
+ /** `ctx.swarm.systemOne_decide` as the MCP bridge serves it, backed by the registered tool. */
939
+ const viaTool = () => {
940
+ const server = new McpServer({ name: "jev-gate-decide", version: "1" });
941
+ registerSystemOneDecideTool(server);
942
+ const tool = (server as any)._registeredTools["system-one-decide"];
943
+ return async (args: Record<string, any>) => {
944
+ const result = await tool.handler(args, {
945
+ sessionId: "script",
946
+ requestInfo: { headers: { "x-agent-id": agentId } },
947
+ });
948
+ return { success: true, status: 200, data: result.structuredContent };
949
+ };
950
+ };
951
+
952
+ test("auto skips OpenRouter behind a gateway, which the server refuses, and runs on Clef", async () => {
953
+ process.env.OPENROUTER_BASE_URL = "https://llm-gateway.example.com/v1";
954
+ const urls: string[] = [];
955
+ globalThis.fetch = (async (input: string | URL | Request) => {
956
+ urls.push(String(input));
957
+ return Response.json({
958
+ result: {
959
+ model: "clef",
960
+ answers: { act: { type: "noul", noul: 0.1 } },
961
+ usage: { input_tokens: 739, output_tokens: 0 },
962
+ },
963
+ success: true,
964
+ errors: [],
965
+ messages: [],
966
+ });
967
+ }) as typeof fetch;
968
+ const { ctx, calls } = fakeCtx({
969
+ configs: {
970
+ OPENROUTER_API_KEY: "x",
971
+ CLOUDFLARE_API_TOKEN: "x",
972
+ CLOUDFLARE_ACCOUNT_ID: CF_ACCOUNT,
973
+ },
974
+ jev: viaTool(),
975
+ });
976
+
977
+ const verdict = await run(ctx, { dryRun: true });
978
+ expect(verdict).toMatchObject({ quiet: true, provider: "cloudflare", threshold: 0.2 });
979
+ expect(verdict.skipped).toEqual([
980
+ expect.stringContaining("openrouter: OPENROUTER_BASE_URL points at a gateway"),
981
+ ]);
982
+ expect(calls.decides.map((d) => d.provider)).toEqual(["openrouter", "cloudflare"]);
983
+ expect(urls).toEqual([
984
+ `https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT}/ai/run/@cf/cloudflare/clef`,
985
+ ]);
986
+ });
987
+
988
+ test("an explicit openrouter choice behind a gateway still throws", async () => {
989
+ process.env.OPENROUTER_BASE_URL = "https://llm-gateway.example.com/v1";
990
+ const { ctx, calls } = fakeCtx({
991
+ configs: {
992
+ OPENROUTER_API_KEY: "x",
993
+ CLOUDFLARE_API_TOKEN: "x",
994
+ CLOUDFLARE_ACCOUNT_ID: CF_ACCOUNT,
995
+ HEARTBEAT_JEV_GATE_PROVIDER: "openrouter",
996
+ },
997
+ jev: viaTool(),
998
+ });
999
+ await expect(run(ctx)).rejects.toThrow(
1000
+ "jev openrouter: OPENROUTER_BASE_URL points at a gateway",
1001
+ );
1002
+ expect(calls.decides).toHaveLength(1);
1003
+ });
1004
+ });
1005
+
765
1006
  describe("request-human-input detached", () => {
766
1007
  beforeAll(() => initDb(":memory:"));
767
1008
  afterAll(() => closeDb());
@@ -11,6 +11,9 @@
11
11
  * 3. Reading from the agent's `task:agent:*` namespace via the cookie is
12
12
  * ALSO forced to the page namespace (the page can't see the agent's
13
13
  * scratchpad).
14
+ * 4. Per-page cookies: two pages open in one browser each proxy as
15
+ * themselves, a header naming another page is refused, and the legacy
16
+ * `page_session` cookie still works for its own page.
14
17
  */
15
18
  import { afterAll, beforeAll, describe, expect, test } from "bun:test";
16
19
  import { randomUUID } from "node:crypto";
@@ -123,11 +126,94 @@ async function launchPage(pageId: string): Promise<string> {
123
126
  expect(res.status).toBe(204);
124
127
  const setCookie = res.headers.get("set-cookie");
125
128
  expect(setCookie).toBeTruthy();
126
- const cookieValue = /page_session=([^;]+)/.exec(setCookie!)?.[1];
129
+ const cookieValue = /page_session_[A-Za-z0-9_-]+=([^;]+)/.exec(setCookie!)?.[1];
127
130
  expect(cookieValue).toBeTruthy();
128
131
  return cookieValue!;
129
132
  }
130
133
 
134
+ /** What the browser SDK sends from `/p/<id>`: its own cookie plus its page id. */
135
+ function pageSessionHeaders(id: string, cookie: string): Record<string, string> {
136
+ return { Cookie: `page_session_${id}=${cookie}`, "X-Swarm-Page-Id": id };
137
+ }
138
+
139
+ async function putClick(headers: Record<string, string>): Promise<Response> {
140
+ return fetch(`${BASE}/@swarm/api/kv/clicks`, {
141
+ method: "PUT",
142
+ headers: { "Content-Type": "application/json", ...headers },
143
+ body: JSON.stringify({ value: 1, valueType: "integer" }),
144
+ });
145
+ }
146
+
147
+ async function namespaceOf(res: Response): Promise<string> {
148
+ expect(res.status).toBe(200);
149
+ return ((await res.json()) as { namespace: string }).namespace;
150
+ }
151
+
152
+ describe("page proxy → kv with several pages open in one browser", () => {
153
+ test("two pages launched in one cookie jar each proxy as themselves, concurrently", async () => {
154
+ const idA = await createPage();
155
+ const idB = await createPage();
156
+ const jar = `page_session_${idA}=${await launchPage(idA)}; page_session_${idB}=${await launchPage(idB)}`;
157
+
158
+ const [resA, resB] = await Promise.all([
159
+ putClick({ Cookie: jar, "X-Swarm-Page-Id": idA }),
160
+ putClick({ Cookie: jar, "X-Swarm-Page-Id": idB }),
161
+ ]);
162
+ expect(await namespaceOf(resA)).toBe(`task:page:${idA}`);
163
+ expect(await namespaceOf(resB)).toBe(`task:page:${idB}`);
164
+ });
165
+
166
+ test("a header naming page B with only page A's session is rejected", async () => {
167
+ const idA = await createPage();
168
+ const idB = await createPage();
169
+ const cookieA = await launchPage(idA);
170
+
171
+ for (const cookie of [
172
+ `page_session_${idA}=${cookieA}`,
173
+ `page_session_${idB}=${cookieA}`,
174
+ `page_session=${cookieA}`,
175
+ ]) {
176
+ const res = await putClick({ Cookie: cookie, "X-Swarm-Page-Id": idB });
177
+ expect(res.status).toBe(401);
178
+ }
179
+ });
180
+
181
+ test("a header that cannot be a page id is rejected", async () => {
182
+ const id = await createPage();
183
+ const cookie = await launchPage(id);
184
+ const res = await putClick({
185
+ Cookie: `page_session_${id}=${cookie}; page_session=${cookie}`,
186
+ "X-Swarm-Page-Id": "../x",
187
+ });
188
+ expect(res.status).toBe(401);
189
+ });
190
+
191
+ test("the legacy page_session cookie still works for its own page", async () => {
192
+ const id = await createPage();
193
+ const cookie = await launchPage(id);
194
+ // With the page named, and from a client that names no page at all.
195
+ const named = await putClick({ Cookie: `page_session=${cookie}`, "X-Swarm-Page-Id": id });
196
+ expect(await namespaceOf(named)).toBe(`task:page:${id}`);
197
+ const unnamed = await putClick({ Cookie: `page_session=${cookie}` });
198
+ expect(await namespaceOf(unnamed)).toBe(`task:page:${id}`);
199
+ });
200
+
201
+ test("a same-host /p/<id> Referer selects the page when no header is sent", async () => {
202
+ const idA = await createPage();
203
+ const idB = await createPage();
204
+ const jar = `page_session_${idA}=${await launchPage(idA)}; page_session_${idB}=${await launchPage(idB)}`;
205
+
206
+ const res = await putClick({ Cookie: jar, Referer: `${BASE}/p/${idB}` });
207
+ expect(await namespaceOf(res)).toBe(`task:page:${idB}`);
208
+
209
+ // A cross-site Referer picks nothing, and no per-page cookie is used unnamed.
210
+ const crossSite = await putClick({ Cookie: jar, Referer: `https://evil.example/p/${idB}` });
211
+ expect(crossSite.status).toBe(401);
212
+ const unnamed = await putClick({ Cookie: jar });
213
+ expect(unnamed.status).toBe(401);
214
+ });
215
+ });
216
+
131
217
  describe("page proxy → kv", () => {
132
218
  test("writes via the proxy land in task:page:<id> automatically", async () => {
133
219
  const id = await createPage();
@@ -137,7 +223,7 @@ describe("page proxy → kv", () => {
137
223
  method: "PUT",
138
224
  headers: {
139
225
  "Content-Type": "application/json",
140
- Cookie: `page_session=${cookie}`,
226
+ ...pageSessionHeaders(id, cookie),
141
227
  },
142
228
  body: JSON.stringify({ value: 1, valueType: "integer" }),
143
229
  });
@@ -161,13 +247,13 @@ describe("page proxy → kv", () => {
161
247
  const cookie = await launchPage(id);
162
248
  const r1 = await fetch(`${BASE}/@swarm/api/kv/votes/incr`, {
163
249
  method: "POST",
164
- headers: { "Content-Type": "application/json", Cookie: `page_session=${cookie}` },
250
+ headers: { "Content-Type": "application/json", ...pageSessionHeaders(id, cookie) },
165
251
  body: JSON.stringify({ by: 2 }),
166
252
  });
167
253
  expect(r1.status).toBe(200);
168
254
  const r2 = await fetch(`${BASE}/@swarm/api/kv/votes/incr`, {
169
255
  method: "POST",
170
- headers: { "Content-Type": "application/json", Cookie: `page_session=${cookie}` },
256
+ headers: { "Content-Type": "application/json", ...pageSessionHeaders(id, cookie) },
171
257
  body: JSON.stringify({}),
172
258
  });
173
259
  const v = (await r2.json()) as { value: number; namespace: string };
@@ -184,7 +270,7 @@ describe("page proxy → kv", () => {
184
270
  const fakeNs = `task:agent:${agentId}`;
185
271
  const put = await fetch(`${BASE}/@swarm/api/kv/_/${encodeURIComponent(fakeNs)}/escape`, {
186
272
  method: "PUT",
187
- headers: { "Content-Type": "application/json", Cookie: `page_session=${cookie}` },
273
+ headers: { "Content-Type": "application/json", ...pageSessionHeaders(id, cookie) },
188
274
  body: JSON.stringify({ value: "leaked", valueType: "string" }),
189
275
  });
190
276
  // The kv handler treats X-Page-Id as the highest-priority namespace
@@ -112,7 +112,7 @@ describe("POST /api/pages/:id/launch — password mode rejection (step-4)", () =
112
112
  headers: { "X-Agent-ID": agentId },
113
113
  });
114
114
  expect(launch.status).toBe(204);
115
- expect(launch.headers.get("set-cookie")).toContain("page_session=");
115
+ expect(launch.headers.get("set-cookie")).toContain("page_session_");
116
116
  });
117
117
 
118
118
  test("public page → launch still issues a cookie (uniform path)", async () => {
@@ -135,6 +135,6 @@ describe("POST /api/pages/:id/launch — password mode rejection (step-4)", () =
135
135
  headers: { "X-Agent-ID": agentId },
136
136
  });
137
137
  expect(launch.status).toBe(204);
138
- expect(launch.headers.get("set-cookie")).toContain("page_session=");
138
+ expect(launch.headers.get("set-cookie")).toContain("page_session_");
139
139
  });
140
140
  });