@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,8 +1,12 @@
1
1
  import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, mock, test } from "bun:test";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { closeDb, createAgent, getDbClient, initDb, upsertSwarmConfig } from "../be/db";
2
4
  import * as contractRuntime from "../extensions/contract-runtime";
5
+ import { registerSystemOneDecideTool } from "../tools/system-one-decide";
3
6
 
4
- // Unit tests for templates/extensions/slack-reply-gate/hooks.ts. Every Jev and Slack
5
- // call goes to a mocked fetch; nothing reaches the network.
7
+ // Unit tests for templates/extensions/slack-reply-gate/hooks.ts. Decisions go to a
8
+ // stubbed `ctx.swarm.systemOne_decide` and Slack calls to a mocked fetch; nothing
9
+ // reaches the network. The last block runs the real system-one-decide tool behind it.
6
10
 
7
11
  // `swarm-extension` is a bare import the extension loader shims to contract-runtime.
8
12
  mock.module("swarm-extension", () => contractRuntime);
@@ -11,11 +15,14 @@ type Handler = (event: any, ctx: any) => Promise<any>;
11
15
  let handler: Handler;
12
16
  let mod: any;
13
17
 
14
- // The hook reads its secrets from the API process env. Each test sets its own; the
15
- // runner's values are saved here and restored after the file.
18
+ // The hook reads credential presence and the Slack token from the API process env.
19
+ // Each test sets its own; the runner's values are saved here and restored after the file.
16
20
  const ENV_KEYS = [
17
21
  "TYPESAFE_API_KEY",
18
22
  "OPENROUTER_API_KEY",
23
+ "CLOUDFLARE_API_TOKEN",
24
+ "CLOUDFLARE_API_KEY",
25
+ "CLOUDFLARE_ACCOUNT_ID",
19
26
  "SLACK_BOT_TOKEN",
20
27
  ...["ACCEPTED", "BUFFERED", "NOW", "STEERED"].map((k) => `SLACK_REACTION_${k}`),
21
28
  ];
@@ -54,8 +61,25 @@ const slackOk = (method: string, body: any): Response => {
54
61
  };
55
62
  let slackReply: SlackReply = slackOk;
56
63
 
57
- /** Mocked Jev client: answer every call with `reply(body)`. Slack calls go to `slackReply`. */
58
- const mockJev = (reply: (body: any) => Response | Promise<Response>) => {
64
+ /** What the MCP bridge returns for a tool call: the tool's structured content in `data`. */
65
+ type BridgeResult = { success: boolean; status: number; data: Record<string, unknown> };
66
+ const served = (data: Record<string, unknown>): BridgeResult => ({
67
+ success: true,
68
+ status: 200,
69
+ data: { success: true, message: "Decision", ...data },
70
+ });
71
+ const refused = (message: string): BridgeResult => ({
72
+ success: true,
73
+ status: 200,
74
+ data: { success: false, message },
75
+ });
76
+
77
+ type DecideReply = (args: any) => BridgeResult | Promise<BridgeResult>;
78
+ let decideReply: DecideReply = () => refused("no decision stub");
79
+
80
+ /** Stub `systemOne_decide` with `reply(args)`. Slack calls go to `slackReply` through fetch. */
81
+ const mockJev = (reply: DecideReply) => {
82
+ decideReply = reply;
59
83
  globalThis.fetch = (async (url: any, init: any) => {
60
84
  const body = JSON.parse(String(init?.body ?? "{}"));
61
85
  const href = String(url);
@@ -64,8 +88,7 @@ const mockJev = (reply: (body: any) => Response | Promise<Response>) => {
64
88
  slackCalls.push({ method, body, auth: String(init?.headers?.Authorization ?? "") });
65
89
  return slackReply(method, body);
66
90
  }
67
- jevCalls.push({ url: href, body, signal: init?.signal });
68
- return reply(body);
91
+ throw new Error(`unexpected fetch: ${href}`);
69
92
  }) as typeof fetch;
70
93
  };
71
94
  const jevChoice = (
@@ -73,27 +96,29 @@ const jevChoice = (
73
96
  confidence = 0.9,
74
97
  probabilities: Record<string, number> | null = { [choice]: confidence, other: 1 - confidence },
75
98
  ) =>
76
- mockJev(
77
- () =>
78
- new Response(
79
- JSON.stringify({
80
- model: "jev-1.13.0",
81
- answers: {
82
- reason: {
83
- type: "choice",
84
- choice,
85
- confidence,
86
- ...(probabilities ? { probabilities } : {}),
87
- },
88
- },
89
- }),
90
- { status: 200 },
91
- ),
99
+ mockJev(() =>
100
+ served({
101
+ model: "jev-1.13.0",
102
+ answers: {
103
+ reason: {
104
+ type: "choice",
105
+ choice,
106
+ confidence,
107
+ ...(probabilities ? { probabilities } : {}),
108
+ },
109
+ },
110
+ }),
92
111
  );
93
112
 
94
113
  const makeCtx = (
95
114
  config: Record<string, unknown> = {},
96
- opts: { key?: string; openrouterKey?: string; slackToken?: string } = {},
115
+ opts: {
116
+ key?: string;
117
+ openrouterKey?: string;
118
+ cloudflareToken?: string;
119
+ cloudflareAccount?: string;
120
+ slackToken?: string;
121
+ } = {},
97
122
  ) => {
98
123
  const kv: Record<string, unknown> = {};
99
124
  const counters: Record<string, number> = {};
@@ -103,8 +128,14 @@ const makeCtx = (
103
128
  };
104
129
  setEnv("TYPESAFE_API_KEY", opts.key ?? "tsk-test");
105
130
  setEnv("OPENROUTER_API_KEY", opts.openrouterKey ?? "");
131
+ setEnv("CLOUDFLARE_API_TOKEN", opts.cloudflareToken ?? "");
132
+ setEnv("CLOUDFLARE_ACCOUNT_ID", opts.cloudflareAccount ?? "");
106
133
  setEnv("SLACK_BOT_TOKEN", opts.slackToken ?? "xoxb-test");
107
- const swarm = {
134
+ const swarm: Record<string, (args: any) => Promise<unknown>> = {
135
+ systemOne_decide: async (args: any) => {
136
+ jevCalls.push({ provider: args.provider, body: args });
137
+ return decideReply(args);
138
+ },
108
139
  kv_set: async ({ key, value }: any) => {
109
140
  kv[key] = value;
110
141
  return { success: true };
@@ -154,6 +185,7 @@ beforeEach(() => {
154
185
  jevCalls = [];
155
186
  slackCalls = [];
156
187
  slackReply = slackOk;
188
+ decideReply = () => refused("no decision stub");
157
189
  mod.resetKeyCache();
158
190
  });
159
191
  afterEach(async () => {
@@ -325,107 +357,112 @@ describe("decide", () => {
325
357
  });
326
358
  });
327
359
 
328
- describe("Jev transport", () => {
360
+ describe("provider", () => {
329
361
  const cfg = (extra: Record<string, unknown> = {}) => mod.config.parse(extra);
362
+ const CF_ACCOUNT = "0123456789abcdef0123456789abcdef";
330
363
 
331
- test("auto prefers TypeSafe when both keys are set", () => {
332
- makeCtx({}, { key: "tsk-test", openrouterKey: "or-test" });
333
- expect(mod.resolveTransport(cfg())).toEqual({
334
- provider: "typesafe",
335
- endpoint: "https://api.typesafe.ai/v1/systemone",
336
- key: "tsk-test",
337
- model: "jev-latest",
338
- });
364
+ test("auto prefers TypeSafe when every credential is set", () => {
365
+ makeCtx(
366
+ {},
367
+ {
368
+ key: "tsk-test",
369
+ openrouterKey: "or-test",
370
+ cloudflareToken: "cf",
371
+ cloudflareAccount: CF_ACCOUNT,
372
+ },
373
+ );
374
+ expect(mod.resolveProvider(cfg())).toEqual({ provider: "typesafe", model: "jev-latest" });
339
375
  });
340
376
 
341
- test("auto falls back to OpenRouter when only its key is set", () => {
342
- makeCtx({}, { key: "", openrouterKey: "or-test" });
343
- expect(mod.resolveTransport(cfg())).toEqual({
377
+ test("auto falls back to OpenRouter, then to Clef", () => {
378
+ makeCtx(
379
+ {},
380
+ { key: "", openrouterKey: "or-test", cloudflareToken: "cf", cloudflareAccount: CF_ACCOUNT },
381
+ );
382
+ expect(mod.resolveProvider(cfg())).toEqual({
344
383
  provider: "openrouter",
345
- endpoint: "https://openrouter.ai/api/alpha/decisions",
346
- key: "or-test",
347
384
  model: "typesafe/jev-1.13",
348
385
  });
386
+ mod.resetKeyCache();
387
+ makeCtx({}, { key: "", cloudflareToken: "cf", cloudflareAccount: CF_ACCOUNT });
388
+ expect(mod.resolveProvider(cfg())).toEqual({ provider: "cloudflare", model: "clef" });
349
389
  });
350
390
 
351
- test("an explicit provider wins over auto order and needs its own key", () => {
352
- makeCtx({}, { key: "tsk-test", openrouterKey: "or-test" });
353
- expect(mod.resolveTransport(cfg({ provider: "openrouter" })).provider).toBe("openrouter");
391
+ test("auto accepts CLOUDFLARE_API_KEY and needs the account id", () => {
392
+ makeCtx({}, { key: "", cloudflareAccount: CF_ACCOUNT });
393
+ process.env.CLOUDFLARE_API_KEY = "cf-key";
394
+ expect(mod.resolveProvider(cfg()).provider).toBe("cloudflare");
354
395
  mod.resetKeyCache();
355
- makeCtx({}, { key: "", openrouterKey: "or-test" });
356
- expect(() => mod.resolveTransport(cfg({ provider: "typesafe" }))).toThrow(
357
- "no TYPESAFE_API_KEY",
358
- );
396
+ makeCtx({}, { key: "", cloudflareToken: "cf" });
397
+ delete process.env.CLOUDFLARE_API_KEY;
398
+ expect(() => mod.resolveProvider(cfg())).toThrow("CLOUDFLARE_ACCOUNT_ID");
399
+ });
400
+
401
+ test("an explicit provider is used as is; the server checks its credential", () => {
402
+ makeCtx({}, { key: "tsk-test", openrouterKey: "or-test" });
403
+ expect(mod.resolveProvider(cfg({ provider: "openrouter" })).provider).toBe("openrouter");
404
+ expect(
405
+ mod.resolveProvider(cfg({ provider: "cloudflare", cloudflareModel: "clef-flash" })),
406
+ ).toEqual({
407
+ provider: "cloudflare",
408
+ model: "clef-flash",
409
+ });
410
+ // A host with no model setting sends none: the server uses the host's default.
411
+ expect(mod.resolveProvider(cfg({ provider: "laya" }))).toEqual({
412
+ provider: "laya",
413
+ model: undefined,
414
+ });
359
415
  });
360
416
 
361
- test("auto with no key names both secrets", () => {
417
+ test("auto with no credential names every secret", () => {
362
418
  makeCtx({}, { key: "", openrouterKey: "" });
363
- expect(() => mod.resolveTransport(cfg())).toThrow("no TYPESAFE_API_KEY or OPENROUTER_API_KEY");
364
- });
365
-
366
- test("an OpenRouter-only swarm classifies through the Decisions endpoint", async () => {
367
- // Shape captured from a live OpenRouter Decisions call to typesafe/jev-1.13.
368
- mockJev(() =>
369
- Response.json({
370
- model: "typesafe/jev-1.13-20260917",
371
- answers: {
372
- reason: {
373
- type: "choice",
374
- choice: "reaction_only",
375
- probabilities: { direct_ask: 0, reaction_only: 0.95, banter_between_others: 0.05 },
376
- confidence: 0.93,
377
- },
378
- },
379
- usage: { input_tokens: 430, output_tokens: 53, cost: 0.00001806 },
380
- id: "gen-dec-1",
381
- provider: "TypeSafe",
382
- }),
419
+ expect(() => mod.resolveProvider(cfg())).toThrow(
420
+ "no TYPESAFE_API_KEY, OPENROUTER_API_KEY, or CLOUDFLARE_API_TOKEN with CLOUDFLARE_ACCOUNT_ID",
383
421
  );
422
+ });
423
+
424
+ test("the decision goes through systemOne_decide, never a direct fetch", async () => {
425
+ jevChoice("reaction_only", 0.95);
384
426
  const { ctx, kv } = makeCtx({ mode: "enforce" }, { key: "", openrouterKey: "or-test" });
385
427
  const result = await handler(slackEvent(["lol nice"]), ctx);
386
- expect(result).toEqual({ action: "block", reason: expect.stringContaining("p(skip) 1.00") });
428
+ expect(result).toEqual({ action: "block", reason: expect.stringContaining("reaction_only") });
387
429
  expect(jevCalls).toHaveLength(1);
388
- expect(jevCalls[0].url).toBe("https://openrouter.ai/api/alpha/decisions");
389
- expect(jevCalls[0].body.model).toBe("typesafe/jev-1.13");
430
+ expect(jevCalls[0].body).toMatchObject({
431
+ provider: "openrouter",
432
+ model: "typesafe/jev-1.13",
433
+ timeoutMs: 3000,
434
+ maxRetries: 0,
435
+ });
390
436
  expect(Object.keys(jevCalls[0].body.questions.reason.criteria)).toEqual(
391
437
  Object.keys(mod.REASONS),
392
438
  );
439
+ expect(JSON.stringify(jevCalls[0].body)).not.toContain("or-test");
393
440
  expect(kv.last).toMatchObject({ provider: "openrouter", reason: "reaction_only" });
394
441
  });
395
442
 
396
- test("an OpenRouter HTTP error fails open and names the provider", async () => {
397
- mockJev(() => Response.json({ error: { message: "no credits" } }, { status: 402 }));
443
+ test("a provider error relayed by the server fails open and names the provider", async () => {
444
+ mockJev(() => refused("OpenRouter returned HTTP 402"));
398
445
  const { ctx, kv } = makeCtx({ mode: "enforce" }, { key: "", openrouterKey: "or-test" });
399
446
  expect(await handler(slackEvent(["lol"]), ctx)).toBeUndefined();
400
447
  expect(kv.last).toMatchObject({
401
448
  action: "fail-open",
402
- error: expect.stringContaining("openrouter 402"),
449
+ error: expect.stringContaining("openrouter: OpenRouter returned HTTP 402"),
403
450
  });
404
451
  });
405
452
  });
406
453
 
407
454
  describe("fail open", () => {
408
455
  const cases: Array<[string, () => void, Record<string, unknown>]> = [
409
- ["HTTP 500", () => mockJev(() => new Response("boom", { status: 500 })), {}],
410
- ["network error", () => mockJev(() => Promise.reject(new Error("ECONNRESET"))), {}],
456
+ ["provider error", () => mockJev(() => refused("TypeSafe returned HTTP 500")), {}],
411
457
  [
412
- "malformed answer",
413
- () => mockJev(() => new Response(JSON.stringify({ answers: {} }), { status: 200 })),
458
+ "bridge error",
459
+ () => mockJev(() => ({ success: false, status: 500, data: { error: "boom" } })),
414
460
  {},
415
461
  ],
462
+ ["network error", () => mockJev(() => Promise.reject(new Error("ECONNRESET"))), {}],
463
+ ["malformed answer", () => mockJev(() => served({ answers: {} })), {}],
416
464
  ["unknown label", () => jevChoice("definitely_skip"), {}],
417
- [
418
- "timeout",
419
- () =>
420
- mockJev(
421
- (_body) =>
422
- new Promise<Response>((_resolve, reject) => {
423
- const signal = jevCalls.at(-1)?.signal as AbortSignal;
424
- signal.addEventListener("abort", () => reject(new Error("aborted")));
425
- }),
426
- ),
427
- { timeoutMs: 50 },
428
- ],
465
+ ["timeout", () => mockJev(() => new Promise<BridgeResult>(() => {})), { timeoutMs: 50 }],
429
466
  ];
430
467
  for (const [label, arrange, cfg] of cases) {
431
468
  test(`${label} -> continue, error recorded`, async () => {
@@ -440,7 +477,7 @@ describe("fail open", () => {
440
477
  });
441
478
  }
442
479
 
443
- test("missing API key -> continue without calling Jev", async () => {
480
+ test("missing API key under auto -> continue without a decision call", async () => {
444
481
  jevChoice("reaction_only");
445
482
  const { ctx, counters } = makeCtx({ mode: "enforce" }, { key: "" });
446
483
  const result = await handler(slackEvent(["lol"]), ctx);
@@ -754,3 +791,137 @@ describe("mute on drop (enforce)", () => {
754
791
  expect(slackCalls).toHaveLength(0);
755
792
  });
756
793
  });
794
+
795
+ describe("on the real system-one-decide tool", () => {
796
+ const CF_ACCOUNT = "0123456789abcdef0123456789abcdef";
797
+ const CF_TOKEN = "cf-reply-gate-example-token";
798
+ const CLEF_URL = `https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT}/ai/run/@cf/cloudflare/clef`;
799
+ let agentId: string;
800
+ let workersAi: Array<{ url: string; authorization: string | null; body: any }> = [];
801
+
802
+ beforeAll(async () => {
803
+ initDb(":memory:");
804
+ agentId = (await createAgent({ name: "reply-gate-ext", isLead: false, status: "idle" })).id;
805
+ await upsertSwarmConfig({ scope: "global", key: "CLOUDFLARE_ACCOUNT_ID", value: CF_ACCOUNT });
806
+ });
807
+ afterAll(() => closeDb());
808
+
809
+ /** `ctx.swarm.systemOne_decide` as the MCP bridge serves it, backed by the registered tool. */
810
+ const viaTool = (ctx: any) => {
811
+ const server = new McpServer({ name: "reply-gate-decide", version: "1" });
812
+ registerSystemOneDecideTool(server);
813
+ const tool = (server as any)._registeredTools["system-one-decide"];
814
+ ctx.swarm.systemOne_decide = async (args: any) => {
815
+ jevCalls.push({ provider: args.provider, body: args });
816
+ const result = await tool.handler(args, {
817
+ sessionId: "extension",
818
+ requestInfo: { headers: { "x-agent-id": agentId } },
819
+ });
820
+ return { success: true, status: 200, data: result.structuredContent };
821
+ };
822
+ };
823
+
824
+ /** Workers AI answering in its REST envelope; Slack calls go to `slackReply`. */
825
+ const mockWorkersAi = (choice: string, p: number) => {
826
+ workersAi = [];
827
+ globalThis.fetch = (async (input: any, init: any) => {
828
+ const url = String(input);
829
+ const body = JSON.parse(String(init?.body ?? "{}"));
830
+ if (url.startsWith("https://slack.com/api/")) {
831
+ const method = url.slice("https://slack.com/api/".length);
832
+ slackCalls.push({ method, body, auth: String(init?.headers?.authorization ?? "") });
833
+ return slackReply(method, body);
834
+ }
835
+ workersAi.push({ url, authorization: new Headers(init?.headers).get("authorization"), body });
836
+ const labels = Object.keys(mod.REASONS);
837
+ const rest = (1 - p) / (labels.length - 1);
838
+ const probabilities = Object.fromEntries(labels.map((l) => [l, l === choice ? p : rest]));
839
+ return Response.json({
840
+ result: {
841
+ model: "clef",
842
+ answers: { reason: { type: "choice", choice, probabilities, confidence: p } },
843
+ usage: { input_tokens: 412, output_tokens: 0 },
844
+ },
845
+ success: true,
846
+ errors: [],
847
+ messages: [],
848
+ });
849
+ }) as typeof fetch;
850
+ };
851
+
852
+ test("Clef classifies on the server; the hook never holds the token", async () => {
853
+ await upsertSwarmConfig({
854
+ scope: "global",
855
+ key: "CLOUDFLARE_API_TOKEN",
856
+ value: CF_TOKEN,
857
+ isSecret: true,
858
+ });
859
+ mockWorkersAi("reaction_only", 0.9);
860
+ const { ctx, kv } = makeCtx(
861
+ { mode: "enforce" },
862
+ { key: "", cloudflareToken: "present", cloudflareAccount: CF_ACCOUNT },
863
+ );
864
+ viaTool(ctx);
865
+
866
+ const result = await handler(slackEvent(["lol nice"]), ctx);
867
+ expect(result).toEqual({ action: "block", reason: expect.stringContaining("reaction_only") });
868
+ expect(workersAi).toHaveLength(1);
869
+ expect(workersAi[0]).toMatchObject({ url: CLEF_URL, authorization: `Bearer ${CF_TOKEN}` });
870
+ expect(workersAi[0]!.body.model).toBe("clef");
871
+ expect(kv.last).toMatchObject({
872
+ provider: "cloudflare",
873
+ reason: "reaction_only",
874
+ action: "skipped",
875
+ });
876
+ expect(JSON.stringify(kv)).not.toContain(CF_TOKEN);
877
+ });
878
+
879
+ test("auto skips OpenRouter behind a gateway, which the server refuses, and reaches Clef", async () => {
880
+ await upsertSwarmConfig({
881
+ scope: "global",
882
+ key: "CLOUDFLARE_API_TOKEN",
883
+ value: CF_TOKEN,
884
+ isSecret: true,
885
+ });
886
+ const prevBase = process.env.OPENROUTER_BASE_URL;
887
+ process.env.OPENROUTER_BASE_URL = "https://llm-gateway.example.com/v1";
888
+ try {
889
+ mockWorkersAi("reaction_only", 0.9);
890
+ const { ctx, kv } = makeCtx(
891
+ { mode: "enforce" },
892
+ {
893
+ key: "",
894
+ openrouterKey: "or-gateway-token",
895
+ cloudflareToken: "present",
896
+ cloudflareAccount: CF_ACCOUNT,
897
+ },
898
+ );
899
+ viaTool(ctx);
900
+
901
+ const result = await handler(slackEvent(["lol nice"]), ctx);
902
+ expect(result).toEqual({ action: "block", reason: expect.stringContaining("reaction_only") });
903
+ expect(jevCalls.map((c) => c.provider)).toEqual(["openrouter", "cloudflare"]);
904
+ expect(workersAi).toHaveLength(1);
905
+ expect(workersAi[0]!.url).toBe(CLEF_URL);
906
+ expect(kv.last).toMatchObject({ provider: "cloudflare", action: "skipped" });
907
+ } finally {
908
+ if (prevBase === undefined) delete process.env.OPENROUTER_BASE_URL;
909
+ else process.env.OPENROUTER_BASE_URL = prevBase;
910
+ }
911
+ });
912
+
913
+ test("a token missing on the server fails open, as before", async () => {
914
+ await getDbClient().run("DELETE FROM swarm_config WHERE key = 'CLOUDFLARE_API_TOKEN'");
915
+ mockWorkersAi("reaction_only", 0.9);
916
+ const { ctx, kv, counters } = makeCtx({ mode: "enforce", provider: "cloudflare" }, { key: "" });
917
+ viaTool(ctx);
918
+
919
+ expect(await handler(slackEvent(["lol"]), ctx)).toBeUndefined();
920
+ expect(workersAi).toHaveLength(0);
921
+ expect(counters.error).toBe(1);
922
+ expect(kv.last).toMatchObject({
923
+ action: "fail-open",
924
+ error: expect.stringContaining("CLOUDFLARE_API_TOKEN is not configured"),
925
+ });
926
+ });
927
+ });
@@ -532,6 +532,39 @@ describe("Heartbeat Checklist", () => {
532
532
  expect(calls.map((call) => call.scriptName)).toEqual(["heartbeat-jev-gate"]);
533
533
  });
534
534
 
535
+ test("unset script with a Cloudflare token and account id runs heartbeat-jev-gate", async () => {
536
+ await setGlobal("CLOUDFLARE_API_TOKEN", "cf-test-token");
537
+ await setGlobal("CLOUDFLARE_ACCOUNT_ID", "0123456789abcdef0123456789abcdef");
538
+ useGate({ quiet: true });
539
+ await seedLead();
540
+
541
+ await checkHeartbeatChecklist();
542
+
543
+ expect(calls.map((call) => call.scriptName)).toEqual(["heartbeat-jev-gate"]);
544
+ });
545
+
546
+ test("CLOUDFLARE_API_KEY counts as the Cloudflare token", async () => {
547
+ await setGlobal("CLOUDFLARE_API_KEY", "cf-test-key");
548
+ await setGlobal("CLOUDFLARE_ACCOUNT_ID", "0123456789abcdef0123456789abcdef");
549
+ process.env.HEARTBEAT_JEV_GATE_PROVIDER = "cloudflare";
550
+ useGate({ quiet: true });
551
+ await seedLead();
552
+
553
+ await checkHeartbeatChecklist();
554
+
555
+ expect(calls.map((call) => call.scriptName)).toEqual(["heartbeat-jev-gate"]);
556
+ });
557
+
558
+ test("a Cloudflare token without an account id leaves the gate off", async () => {
559
+ await setGlobal("CLOUDFLARE_API_TOKEN", "cf-test-token");
560
+ useGate({ quiet: true });
561
+ await seedLead();
562
+
563
+ await checkHeartbeatChecklist();
564
+
565
+ expect(calls).toHaveLength(0);
566
+ });
567
+
535
568
  test("unset script and no Jev key: gate stays off", async () => {
536
569
  await setGlobal("ANTHROPIC_API_KEY", "unrelated");
537
570
  useGate({ quiet: true });
@@ -165,4 +165,48 @@ describe("system-one-decide keeps the key on the server", () => {
165
165
  ]);
166
166
  expect(JSON.stringify(result)).not.toContain(TYPESAFE_KEY);
167
167
  });
168
+
169
+ test("with no provider it uses SYSTEM_ONE_DEFAULT_PROVIDER, typesafe when unset", async () => {
170
+ const { provider: _omit, ...noProvider } = request("typesafe");
171
+ const unset = recordAuthorization();
172
+ expect((await tool().handler(noProvider, extra())).isError).toBeFalsy();
173
+ expect(unset.map((s) => s.url)).toEqual(["https://api.typesafe.ai/v1/systemone"]);
174
+
175
+ const account = "0123456789abcdef0123456789abcdef";
176
+ await upsertSwarmConfig({
177
+ scope: "global",
178
+ key: "SYSTEM_ONE_DEFAULT_PROVIDER",
179
+ value: "cloudflare",
180
+ });
181
+ await upsertSwarmConfig({ scope: "global", key: "CLOUDFLARE_ACCOUNT_ID", value: account });
182
+ await upsertSwarmConfig({
183
+ scope: "global",
184
+ key: "CLOUDFLARE_API_TOKEN",
185
+ value: "cf-default-provider-token",
186
+ isSecret: true,
187
+ });
188
+ const sent: string[] = [];
189
+ globalThis.fetch = (async (input: RequestInfo | URL) => {
190
+ sent.push(input instanceof Request ? input.url : String(input));
191
+ return Response.json({
192
+ result: {
193
+ model: "clef",
194
+ answers: { act: { type: "noul", noul: 0.7 } },
195
+ usage: { input_tokens: 12, output_tokens: 0 },
196
+ },
197
+ success: true,
198
+ errors: [],
199
+ messages: [],
200
+ });
201
+ }) as typeof fetch;
202
+ const result = await tool().handler(noProvider, extra());
203
+ expect(result.isError).toBeFalsy();
204
+ expect(result.structuredContent).toMatchObject({
205
+ model: "clef",
206
+ answers: { act: { noul: 0.7 } },
207
+ });
208
+ expect(sent).toEqual([
209
+ `https://api.cloudflare.com/client/v4/accounts/${account}/ai/run/@cf/cloudflare/clef`,
210
+ ]);
211
+ });
168
212
  });