@desplega.ai/agent-swarm 2.0.0 → 2.1.0

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 (238) hide show
  1. package/dist/{acp-adapter-sj2g0m84.js → acp-adapter-kr4abrsk.js} +10 -10
  2. package/dist/{actions-fegk44gr.js → actions-eh4g6w2r.js} +27 -27
  3. package/dist/{amp-adapter-ad8ft89x.js → amp-adapter-7h7wdt8q.js} +5 -5
  4. package/dist/{app-kfvgbvzy.js → app-7k8s9w5p.js} +9 -9
  5. package/dist/{assistant-z7zdy9ja.js → assistant-wswt2y7p.js} +28 -28
  6. package/dist/{boot-reembed-j8xa181e.js → boot-reembed-9apm9qw6.js} +7 -7
  7. package/dist/{boot-reembed-5c2z0mbe.js → boot-reembed-whzbdthj.js} +7 -7
  8. package/dist/{boot-scrub-sweep-hnkn3rgt.js → boot-scrub-sweep-ndzmvmgt.js} +8 -8
  9. package/dist/{boot-seeds-kwjq4d2z.js → boot-seeds-e9sta0yt.js} +13 -11
  10. package/dist/{claude-adapter-9vmvpvgd.js → claude-adapter-4mqd3r6r.js} +11 -11
  11. package/dist/{claude-managed-adapter-k9qae05j.js → claude-managed-adapter-9wswb52m.js} +3 -3
  12. package/dist/{claude-managed-setup-q54zx5q2.js → claude-managed-setup-tpe1effx.js} +4 -4
  13. package/dist/{claude-sdk-session-9jye6cxr.js → claude-sdk-session-q49ghmh9.js} +10340 -10197
  14. package/dist/{cli-ke896fcf.js → cli-01xvj1rq.js} +10 -10
  15. package/dist/{cli-tr4sxp68.js → cli-035vekcw.js} +2 -2
  16. package/dist/{cli-z5jjpaxw.js → cli-0pd8hzhc.js} +3 -3
  17. package/dist/{cli-n5vwg842.js → cli-0veb92a3.js} +6 -1
  18. package/dist/{cli-wayw93nt.js → cli-10jesr25.js} +3 -3
  19. package/dist/{cli-se8w6wny.js → cli-1y7yywmj.js} +1 -1
  20. package/dist/{cli-v1yb0rqb.js → cli-20krdhz5.js} +47 -19
  21. package/dist/{cli-d5f9ed5c.js → cli-29rja109.js} +39 -20
  22. package/dist/{cli-apx87jne.js → cli-2pwa0q4d.js} +15 -4
  23. package/dist/{cli-ee9gtz70.js → cli-4mengw46.js} +348 -25
  24. package/dist/{cli-03gedhng.js → cli-5twdke5x.js} +6 -6
  25. package/dist/{cli-6razj6t4.js → cli-711vwbxn.js} +2 -2
  26. package/dist/{cli-s2sp957d.js → cli-78vv7446.js} +1 -1
  27. package/dist/{cli-3fbmmawm.js → cli-7n6s5man.js} +4 -4
  28. package/dist/{cli-hnhtk6cj.js → cli-8wwxw8a3.js} +1 -1
  29. package/dist/{cli-hnmhtzvh.js → cli-9jzdj90z.js} +2 -0
  30. package/dist/{cli-skxvvpap.js → cli-cvv9k0hh.js} +28 -25
  31. package/dist/{cli-74ahz61x.js → cli-d9286m9h.js} +2 -2
  32. package/dist/{cli-4jxm2qtz.js → cli-gxzkg1de.js} +28 -16
  33. package/dist/{cli-zkpks3r3.js → cli-h1arqn31.js} +1 -1
  34. package/dist/{cli-r62a35ea.js → cli-hqr88ew6.js} +1 -1
  35. package/dist/{cli-ra0xzggj.js → cli-j67mh2vg.js} +2 -2
  36. package/dist/{cli-29jc5q7w.js → cli-ksphgh48.js} +2 -2
  37. package/dist/{cli-eejsgvm4.js → cli-mgz4nbgg.js} +4 -4
  38. package/dist/{cli-vatyhshy.js → cli-mz5as5ha.js} +4 -4
  39. package/dist/{cli-f7kr7svz.js → cli-n8ndw8j7.js} +2 -2
  40. package/dist/{cli-1e4b6bzh.js → cli-naqzr17j.js} +7 -7
  41. package/dist/{cli-q0ke1yww.js → cli-nm1p8jfr.js} +6 -6
  42. package/dist/{cli-m50t4309.js → cli-p3zbc1rw.js} +5 -5
  43. package/dist/{cli-cgtksawx.js → cli-pe05f8yk.js} +23 -3
  44. package/dist/{cli-vdach5a9.js → cli-qmzsbkvy.js} +4 -4
  45. package/dist/{cli-n3hzg89b.js → cli-qnm4e9e4.js} +57 -579
  46. package/dist/{cli-s4setq91.js → cli-qqy4q48b.js} +3 -3
  47. package/dist/{cli-k382tshb.js → cli-r35rw5de.js} +8 -7
  48. package/dist/{cli-k3dp5tpf.js → cli-r610p9bs.js} +2 -2
  49. package/dist/{cli-e6mpjhed.js → cli-rc6pf5kv.js} +1 -1
  50. package/dist/{cli-3dj8k4vy.js → cli-rdxm77k3.js} +1 -1
  51. package/dist/{cli-tqaazemc.js → cli-s69nfyeh.js} +1 -1
  52. package/dist/{cli-6jyj59nb.js → cli-sawv63e8.js} +2 -2
  53. package/dist/{cli-q50942rs.js → cli-sg19gbf0.js} +1 -1
  54. package/dist/{cli-p1x85d2t.js → cli-shk7r5cs.js} +12 -9
  55. package/dist/{cli-przpmdgr.js → cli-sksetb2q.js} +11 -1
  56. package/dist/{cli-d2qr7aj4.js → cli-t02z59f2.js} +1 -1
  57. package/dist/{cli-j0pfptdp.js → cli-v7g551te.js} +39 -43
  58. package/dist/{cli-bzszjf2p.js → cli-vgqzp4em.js} +519 -126
  59. package/dist/cli-wfnemr1t.js +86 -0
  60. package/dist/{cli-ne236afy.js → cli-wms6x9db.js} +8 -8
  61. package/dist/{cli-5hkm7whv.js → cli-x1r545x8.js} +1 -1
  62. package/dist/{cli-kt6zqg0e.js → cli-xczsgshn.js} +2 -2
  63. package/dist/{cli-ef1hg1wv.js → cli-y3167wt1.js} +430 -238
  64. package/dist/{cli-tsde1hdr.js → cli-yjmqpxt3.js} +12 -11
  65. package/dist/{cli-9nfx44eh.js → cli-z780ec7c.js} +1 -1
  66. package/dist/{cli-r2pnvj8g.js → cli-zm9t2vde.js} +2 -2
  67. package/dist/cli.js +24 -23
  68. package/dist/{codex-adapter-y81sssk0.js → codex-adapter-4z16v9n0.js} +8 -8
  69. package/dist/{codex-login-f2rcfpp9.js → codex-login-pkepy2j1.js} +2 -2
  70. package/dist/{codex-session-runner-8vx4j6a9.js → codex-session-runner-rvms9y2x.js} +8 -8
  71. package/dist/{commands-kcpvpcrr.js → commands-2a9rtyat.js} +8 -8
  72. package/dist/{controller-ywrmt6yd.js → controller-3k3madbn.js} +4 -4
  73. package/dist/{controllers-connect-kwpvj3rf.js → controllers-connect-v9yn8zb9.js} +30 -30
  74. package/dist/{cursor-adapter-cehv1qns.js → cursor-adapter-98gbhyzn.js} +4 -4
  75. package/dist/{db-ww5d7jq3.js → db-eywwsnjw.js} +9 -7
  76. package/dist/{db-k8ankj20.js → db-ncypk9np.js} +9 -9
  77. package/dist/{devin-adapter-x1022qb1.js → devin-adapter-zeq8b9gz.js} +2 -2
  78. package/dist/{dispatcher-e8gq5m68.js → dispatcher-w94kc575.js} +26 -26
  79. package/dist/{docker-ff6y1190.js → docker-b1e4cqab.js} +2 -2
  80. package/dist/{dsh-adapter-3epjm3af.js → dsh-adapter-47ztej36.js} +4 -4
  81. package/dist/{e2b-8ygwhg0f.js → e2b-bbt8ey45.js} +3 -3
  82. package/dist/{e2b-swhnpw3n.js → e2b-dp804sen.js} +3 -3
  83. package/dist/{event-bus-6axnngdj.js → event-bus-j2jdg3qb.js} +2 -2
  84. package/dist/{external-channel-h3359cwn.js → external-channel-zrkg8ar9.js} +8 -8
  85. package/dist/{grok-adapter-0d4x7gkx.js → grok-adapter-s9ncp9vy.js} +11 -11
  86. package/dist/{handlers-an1gzv21.js → handlers-kqftvjgw.js} +29 -29
  87. package/dist/{heartbeat-6dakeb61.js → heartbeat-9q96p8v9.js} +31 -31
  88. package/dist/{hook-6v6cv6d5.js → hook-56qe10dp.js} +7 -7
  89. package/dist/{hook-sa5xye4y.js → hook-yy6rw42k.js} +8 -8
  90. package/dist/{http-r7f3eky0.js → http-r2dnb8f8.js} +963 -75
  91. package/dist/{index-7ep4gzdk.js → index-3gtmkhc6.js} +26 -26
  92. package/dist/{index-v1v6n7hg.js → index-3k5hkz7r.js} +31 -31
  93. package/dist/{index-rmpw9a8q.js → index-5e101240.js} +26 -26
  94. package/dist/{index-s2wxgzr0.js → index-5wyfjc2c.js} +392 -15
  95. package/dist/{index-zk96aq1k.js → index-vxvhxxbn.js} +7 -7
  96. package/dist/{keepalive-be9vwy5h.js → keepalive-psj6j54c.js} +9 -9
  97. package/dist/{lead-0861cwrf.js → lead-hajz4688.js} +43 -43
  98. package/dist/{maintenance-mzzgc8bn.js → maintenance-ps02sb5m.js} +7 -7
  99. package/dist/{node-mn1zs7an.js → node-mwb5r697.js} +1 -1
  100. package/dist/{oauth-refresh-sweep-cpdnfwdj.js → oauth-refresh-sweep-0284h93b.js} +8 -8
  101. package/dist/{onboard-fh8qs4r3.js → onboard-rbf6bg01.js} +2 -2
  102. package/dist/{opencode-adapter-nm42wwj8.js → opencode-adapter-13dcatgq.js} +8 -8
  103. package/dist/{otel-impl-x6c1c9g4.js → otel-impl-zvqcbhp7.js} +2 -2
  104. package/dist/{pi-mono-adapter-9n2ya240.js → pi-mono-adapter-xxy47q4v.js} +7 -7
  105. package/dist/{pricing-refresh-qb0rv0ap.js → pricing-refresh-kefx9v3t.js} +9 -9
  106. package/dist/{rbac-roles-1gzdwbh1.js → rbac-roles-3m36rxm2.js} +8 -8
  107. package/dist/{rbac-roles-3mraey52.js → rbac-roles-z76xv0hq.js} +9 -9
  108. package/dist/{render-v2-agpa45zz.js → render-v2-wz73bs25.js} +9 -9
  109. package/dist/{resume-x0ydy9s9.js → resume-k4wzge8w.js} +14 -14
  110. package/dist/{setup-as9ga0d6.js → setup-2fp2pypp.js} +2 -2
  111. package/dist/{work-objects-y5g1y7ev.js → work-objects-6k0ywz3m.js} +29 -29
  112. package/dist/{worker-860c3e38.js → worker-5v3rxsdh.js} +43 -43
  113. package/dist/{x-m477fr0h.js → x-4mtdeym1.js} +2 -2
  114. package/openapi.json +777 -6
  115. package/package.json +8 -7
  116. package/src/agentmail/handlers.ts +21 -11
  117. package/src/apps/definition.ts +16 -0
  118. package/src/azure-devops/api.ts +52 -2
  119. package/src/azure-devops/boards-sync.ts +50 -20
  120. package/src/azure-devops/index.ts +1 -0
  121. package/src/azure-devops/outbound.ts +95 -0
  122. package/src/be/boot-seeds.ts +11 -0
  123. package/src/be/db/model-profiles.ts +315 -0
  124. package/src/be/db/tasks/read.ts +7 -0
  125. package/src/be/db.ts +53 -47
  126. package/src/be/migrations/219_relabel_pi_context_compactions.sql +22 -0
  127. package/src/be/migrations/220_model_profiles.sql +137 -0
  128. package/src/be/model-tier-resolution.ts +145 -11
  129. package/src/be/model-validation.ts +8 -1
  130. package/src/be/scripts/typecheck.ts +12 -0
  131. package/src/be/secret-registry.ts +6 -2
  132. package/src/be/seed-model-profiles.ts +41 -0
  133. package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +368 -0
  134. package/src/be/seed-scripts/index.ts +9 -0
  135. package/src/be/swarm-config-guard.ts +1 -0
  136. package/src/commands/runner.ts +3 -17
  137. package/src/extensions/catalog.generated.json +17 -0
  138. package/src/extensions/contract-types.generated.ts +1 -1
  139. package/src/extensions/loader.ts +10 -5
  140. package/src/heartbeat/heartbeat.ts +2 -1
  141. package/src/http/agents.ts +49 -2
  142. package/src/http/all-routes.ts +1 -0
  143. package/src/http/apps.ts +30 -3
  144. package/src/http/config.ts +26 -2
  145. package/src/http/core.ts +4 -0
  146. package/src/http/index.ts +11 -1
  147. package/src/http/model-profiles.ts +303 -0
  148. package/src/http/pages-public.ts +11 -5
  149. package/src/http/poll.ts +16 -5
  150. package/src/http/script-runs.ts +1 -1
  151. package/src/http/scripts.ts +25 -2
  152. package/src/http/tasks.ts +18 -0
  153. package/src/http/webhooks.ts +1 -2
  154. package/src/pages/feedback-overlay.ts +65 -6
  155. package/src/providers/acp-targets.ts +1 -1
  156. package/src/providers/amp-adapter.ts +1 -1
  157. package/src/providers/dsh-adapter.ts +1 -1
  158. package/src/providers/pi-mono-extension.ts +17 -3
  159. package/src/providers/reasoning-effort.ts +10 -0
  160. package/src/rbac/legacy-policy.ts +8 -0
  161. package/src/rbac/permissions.ts +5 -0
  162. package/src/scheduler/deferred-task-waits.ts +18 -5
  163. package/src/scheduler/scheduler.ts +7 -1
  164. package/src/scripts-runtime/executors/quickjs-prelude.ts +75 -6
  165. package/src/scripts-runtime/executors/quickjs-runner.ts +85 -8
  166. package/src/scripts-runtime/executors/quickjs-web-shims.ts +1041 -0
  167. package/src/scripts-runtime/extract-args-schema.ts +9 -8
  168. package/src/scripts-runtime/quickjs-compat.ts +464 -0
  169. package/src/scripts-runtime/sdk-allowlist.ts +4 -0
  170. package/src/scripts-runtime/types/stdlib.d.ts +12 -0
  171. package/src/scripts-runtime/types/swarm-extension.d.ts +3 -0
  172. package/src/scripts-runtime/types/swarm-sdk.d.ts +12 -0
  173. package/src/server.ts +10 -1
  174. package/src/slack/blocks.ts +4 -2
  175. package/src/slack/reaction-shortcode.ts +14 -0
  176. package/src/slack/render-v2.ts +12 -7
  177. package/src/slack/responses.ts +5 -4
  178. package/src/slack/thread-buffer.ts +41 -22
  179. package/src/tasks/worker-follow-up.ts +11 -2
  180. package/src/tests/agentmail-filters.test.ts +47 -0
  181. package/src/tests/apps-spike2.test.ts +35 -0
  182. package/src/tests/azure-devops-outbound.test.ts +420 -0
  183. package/src/tests/db-task-read-contract.test.ts +2 -0
  184. package/src/tests/db-task-write-contract.test.ts +1 -0
  185. package/src/tests/deferred-task-wake.test.ts +78 -1
  186. package/src/tests/docs-index-footprint.test.js +2 -2
  187. package/src/tests/dsh-adapter.test.ts +1 -1
  188. package/src/tests/env-flag.test.ts +1 -0
  189. package/src/tests/extensions-catalog.test.ts +1 -0
  190. package/src/tests/extensions-example-comms-adherence-gate.test.ts +413 -0
  191. package/src/tests/extensions-loader.test.ts +15 -0
  192. package/src/tests/heartbeat-jev-gate-credentials.test.ts +168 -0
  193. package/src/tests/heartbeat-jev-gate.test.ts +385 -0
  194. package/src/tests/model-profile-backcompat.test.ts +327 -0
  195. package/src/tests/model-profile-providers.test.ts +302 -0
  196. package/src/tests/model-profile-runtime-secrets.test.ts +223 -0
  197. package/src/tests/model-profiles-api.test.ts +614 -0
  198. package/src/tests/pages-feedback-keys.test.ts +121 -0
  199. package/src/tests/pages-feedback.test.ts +3 -0
  200. package/src/tests/pi-context-compaction.test.ts +193 -0
  201. package/src/tests/profile-claim.test.ts +269 -0
  202. package/src/tests/quickjs-compat.test.ts +213 -0
  203. package/src/tests/rbac-engine.test.ts +21 -0
  204. package/src/tests/script-connections.test.ts +4 -0
  205. package/src/tests/script-executor-conformance.test.ts +252 -1
  206. package/src/tests/script-executor-quickjs.test.ts +96 -0
  207. package/src/tests/script-runs-http.test.ts +89 -0
  208. package/src/tests/scripts-http.test.ts +16 -0
  209. package/src/tests/secret-scrubber.test.ts +10 -0
  210. package/src/tests/seed-model-profiles.test.ts +174 -0
  211. package/src/tests/seed-scripts.test.ts +2 -2
  212. package/src/tests/skill-invocations.test.ts +4 -2
  213. package/src/tests/slack-blocks.test.ts +36 -0
  214. package/src/tests/slack-inline-output.test.ts +14 -0
  215. package/src/tests/slack-reaction-config.test.ts +20 -0
  216. package/src/tests/slack-render-v2.test.ts +37 -0
  217. package/src/tests/slack-thread-buffer.test.ts +98 -1
  218. package/src/tests/workflow-llm.test.ts +61 -0
  219. package/src/tools/model-profiles/list-model-profiles.ts +54 -0
  220. package/src/tools/request-human-input.ts +11 -3
  221. package/src/tools/script-apis.ts +5 -1
  222. package/src/tools/script-upsert.ts +4 -1
  223. package/src/tools/send-task.ts +24 -1
  224. package/src/tools/system-one-decide.ts +104 -0
  225. package/src/tools/task-action.ts +22 -1
  226. package/src/tools/tool-config.ts +6 -0
  227. package/src/types.ts +36 -1
  228. package/src/utils/secret-scrubber.ts +19 -12
  229. package/src/utils/vcs-repo-match.ts +29 -0
  230. package/src/workflows/executors/raw-llm.ts +8 -3
  231. package/src/workflows/executors/system-one-decision.ts +21 -11
  232. package/src/workflows/executors/workflow-llm.ts +22 -0
  233. package/src/workflows/index.ts +21 -12
  234. package/templates/extensions/README.md +1 -0
  235. package/templates/extensions/comms-adherence-gate/README.md +88 -0
  236. package/templates/extensions/comms-adherence-gate/hooks.ts +489 -0
  237. package/templates/extensions/comms-adherence-gate/manifest.json +8 -0
  238. package/dist/cli-5t4f29ez.js +0 -33
@@ -0,0 +1,413 @@
1
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, mock, test } from "bun:test";
2
+ import * as contractRuntime from "../extensions/contract-runtime";
3
+
4
+ // Unit tests for templates/extensions/comms-adherence-gate/hooks.ts. Every Jev call goes
5
+ // to a mocked fetch and the DB read to a mocked ctx.swarm.db_query; nothing reaches the
6
+ // network.
7
+
8
+ // `swarm-extension` is a bare import the extension loader shims to contract-runtime.
9
+ mock.module("swarm-extension", () => contractRuntime);
10
+
11
+ type Handler = (event: any, ctx: any) => Promise<any>;
12
+ let handler: Handler;
13
+ let mod: any;
14
+
15
+ const ENV_KEYS = ["TYPESAFE_API_KEY", "OPENROUTER_API_KEY"];
16
+ const savedEnv: Record<string, string | undefined> = {};
17
+
18
+ beforeAll(async () => {
19
+ for (const k of ENV_KEYS) {
20
+ savedEnv[k] = process.env[k];
21
+ delete process.env[k];
22
+ }
23
+ mod = await import("../../templates/extensions/comms-adherence-gate/hooks.ts");
24
+ mod.default({
25
+ on: (name: string, h: Handler) => {
26
+ if (name === "pre.tool.call") handler = h;
27
+ },
28
+ });
29
+ });
30
+
31
+ afterAll(() => {
32
+ for (const k of ENV_KEYS) {
33
+ if (savedEnv[k] === undefined) delete process.env[k];
34
+ else process.env[k] = savedEnv[k];
35
+ }
36
+ });
37
+
38
+ const TASK_ID = "11111111-2222-3333-4444-555555555555";
39
+ const PREFS = { verbosity: "very terse", tone: "direct, plain", language: "English" };
40
+ const TASK_TEXT =
41
+ "<thread_context>\n<@U1|Alice>: earlier chatter about deploys\n</thread_context>\n\nwhich option did you pick?";
42
+
43
+ const realFetch = globalThis.fetch;
44
+ let jevCalls: Array<{ url: string; body: any; auth: string }> = [];
45
+
46
+ const mockJev = (reply: (body: any) => Response | Promise<Response>) => {
47
+ globalThis.fetch = (async (url: any, init: any) => {
48
+ const body = JSON.parse(String(init?.body ?? "{}"));
49
+ jevCalls.push({ url: String(url), body, auth: String(init?.headers?.Authorization ?? "") });
50
+ return reply(body);
51
+ }) as typeof fetch;
52
+ };
53
+ /** Answer every question with the same score, or per-question scores. */
54
+ const jevScores = (scores: number | Record<string, number>) =>
55
+ mockJev((body) => {
56
+ const answers: Record<string, unknown> = {};
57
+ for (const id of Object.keys(body.questions ?? {})) {
58
+ const s = typeof scores === "number" ? scores : (scores[id] ?? 1);
59
+ answers[id] = { type: "noul", noul: s };
60
+ }
61
+ return Response.json({ model: "jev-1.13.0", answers });
62
+ });
63
+
64
+ type Row = Record<string, unknown> | null;
65
+ const taskRow = (over: Record<string, unknown> = {}): Row => ({
66
+ source: "slack",
67
+ slackThreadTs: "1700000000.000100",
68
+ agentId: "agent-1",
69
+ requestedByUserId: "user-1",
70
+ comms: JSON.stringify(PREFS),
71
+ name: "Alice",
72
+ askHead: TASK_TEXT.slice(0, 400),
73
+ askTail: TASK_TEXT.slice(-1400),
74
+ ...over,
75
+ });
76
+
77
+ const makeCtx = (
78
+ config: Record<string, unknown> = {},
79
+ opts: {
80
+ row?: Row;
81
+ key?: string;
82
+ openrouterKey?: string;
83
+ dbThrows?: boolean;
84
+ /** The caller's agents.isLead; null means no agents row. */
85
+ callerIsLead?: boolean | null;
86
+ } = {},
87
+ ) => {
88
+ const kv: Record<string, unknown> = {};
89
+ const counters: Record<string, number> = {};
90
+ const queries: any[] = [];
91
+ const setEnv = (name: string, value: string) => {
92
+ if (value) process.env[name] = value;
93
+ else delete process.env[name];
94
+ };
95
+ setEnv("TYPESAFE_API_KEY", opts.key ?? "tsk-test");
96
+ setEnv("OPENROUTER_API_KEY", opts.openrouterKey ?? "");
97
+ const row = opts.row === undefined ? taskRow() : opts.row;
98
+ const swarm = {
99
+ db_query: async (args: any) => {
100
+ queries.push(args);
101
+ if (opts.dbThrows) throw new Error("db down");
102
+ if (String(args.sql).includes("FROM agents")) {
103
+ const lead = opts.callerIsLead ?? false;
104
+ return {
105
+ success: true,
106
+ data: { rows: opts.callerIsLead === null ? [] : [{ isLead: lead ? 1 : 0 }] },
107
+ };
108
+ }
109
+ return { success: true, data: { rows: row ? [row] : [] } };
110
+ },
111
+ kv_set: async ({ key, value }: any) => {
112
+ kv[key] = value;
113
+ return { success: true };
114
+ },
115
+ kv_get: async ({ key }: any) =>
116
+ key in counters ? { success: true, data: { value: counters[key] } } : { success: false },
117
+ kv_incr: async ({ key }: any) => {
118
+ counters[key] = (counters[key] ?? 0) + 1;
119
+ return { success: true };
120
+ },
121
+ };
122
+ const warns: unknown[] = [];
123
+ return {
124
+ ctx: {
125
+ swarm,
126
+ config,
127
+ state: {},
128
+ log: { debug() {}, info() {}, warn: (...a: unknown[]) => warns.push(a), error() {} },
129
+ signal: new AbortController().signal,
130
+ },
131
+ kv,
132
+ counters,
133
+ queries,
134
+ warns,
135
+ };
136
+ };
137
+
138
+ const completion = (output: string, extra: Record<string, unknown> = {}) => ({
139
+ tool: "store-progress",
140
+ args: { taskId: TASK_ID, status: "completed", output, ...extra },
141
+ requestInfo: { callOrigin: "mcp", agentId: "agent-1" },
142
+ });
143
+
144
+ beforeEach(() => {
145
+ jevCalls = [];
146
+ mod.resetKeyCache();
147
+ });
148
+ afterEach(() => {
149
+ globalThis.fetch = realFetch;
150
+ });
151
+
152
+ describe("defaults", () => {
153
+ test("a new install defaults to shadow", () => {
154
+ expect(mod.cfgOf(undefined)).toMatchObject({
155
+ mode: "shadow",
156
+ threshold: 0.5,
157
+ timeoutMs: 4000,
158
+ maxBlocksPerTask: 1,
159
+ provider: "auto",
160
+ });
161
+ });
162
+
163
+ test("an invalid config falls back to defaults", () => {
164
+ expect(mod.cfgOf({ mode: "loud" }).mode).toBe("shadow");
165
+ });
166
+ });
167
+
168
+ describe("latestAsk", () => {
169
+ test("takes the text after the last thread context", () => {
170
+ expect(mod.latestAsk(TASK_TEXT)).toBe("which option did you pick?");
171
+ });
172
+
173
+ test("strips the thread follow-up header", () => {
174
+ const text =
175
+ "<thread_context>\nx\n</thread_context>\n\n[Thread follow-up — 1 message(s) buffered]\n\ntell me a long joke";
176
+ expect(mod.latestAsk(text)).toBe("tell me a long joke");
177
+ });
178
+
179
+ test("falls back to the end of the context when nothing follows it", () => {
180
+ expect(mod.latestAsk("<thread_context>\nplease explain in detail\n</thread_context>")).toBe(
181
+ "<thread_context>\nplease explain in detail",
182
+ );
183
+ });
184
+ });
185
+
186
+ describe("pass", () => {
187
+ test("scores at or above threshold pass and are recorded", async () => {
188
+ jevScores(0.9);
189
+ const { ctx, kv, counters, queries } = makeCtx({ mode: "enforce" });
190
+ const result = await handler(completion("Picked B: it keeps the migration reversible."), ctx);
191
+ expect(result).toBeUndefined();
192
+ expect(counters).toMatchObject({ total: 1, pass: 1 });
193
+ expect(counters.fail).toBeUndefined();
194
+ expect(kv.last).toMatchObject({ taskId: TASK_ID, failed: [], provider: "typesafe" });
195
+ // The task id is bound, never interpolated into the SQL.
196
+ expect(queries[0].params).toEqual([TASK_ID]);
197
+ expect(queries[0].sql).not.toContain(TASK_ID);
198
+ const call = jevCalls[0]!;
199
+ expect(call.url).toBe("https://api.typesafe.ai/v1/systemone");
200
+ expect(call.auth).toBe("Bearer tsk-test");
201
+ expect(Object.keys(call.body.questions).sort()).toEqual(["language", "tone", "verbosity"]);
202
+ expect(call.body.state.latest_request).toBe("which option did you pick?");
203
+ });
204
+
205
+ test("non-Slack tasks, requesters without prefs, non-completions and other agents' tasks skip Jev", async () => {
206
+ jevScores(0.1);
207
+ const cases: Array<[any, Row]> = [
208
+ [completion("x"), taskRow({ source: "mcp", slackThreadTs: null })],
209
+ [completion("x"), taskRow({ comms: null })],
210
+ [completion("x"), taskRow({ comms: JSON.stringify({ timezone: "Europe/Madrid" }) })],
211
+ [
212
+ { ...completion("x"), args: { taskId: TASK_ID, status: "in_progress", progress: "hi" } },
213
+ taskRow(),
214
+ ],
215
+ [{ ...completion("x"), tool: "send-task" }, taskRow()],
216
+ [{ ...completion("x"), requestInfo: { callOrigin: "extension" } }, taskRow()],
217
+ // Another agent's task, or a caller with no agent id.
218
+ [completion("x"), taskRow({ agentId: "agent-2" })],
219
+ [{ ...completion("x"), requestInfo: { callOrigin: "mcp" } }, taskRow()],
220
+ [completion("x", { taskId: "not-a-uuid" }), taskRow()],
221
+ ];
222
+ for (const [event, row] of cases) {
223
+ const { ctx } = makeCtx({ mode: "enforce" }, { row });
224
+ expect(await handler(event, ctx)).toBeUndefined();
225
+ }
226
+ expect(jevCalls).toHaveLength(0);
227
+ });
228
+
229
+ test("a non-lead worker completing another agent's task causes zero judge egress", async () => {
230
+ jevScores(0.1);
231
+ for (const callerIsLead of [false, null]) {
232
+ const { ctx, queries } = makeCtx(
233
+ { mode: "enforce" },
234
+ { row: taskRow({ agentId: "agent-2" }), callerIsLead },
235
+ );
236
+ expect(await handler(completion("x"), ctx)).toBeUndefined();
237
+ // Lead status is read for the caller from the agents row.
238
+ expect(queries[1]).toMatchObject({ params: ["agent-1"] });
239
+ }
240
+ expect(jevCalls).toHaveLength(0);
241
+ });
242
+
243
+ test("a lead completing a worker-owned task reaches the judge", async () => {
244
+ jevScores(0.1);
245
+ const { ctx, counters } = makeCtx(
246
+ { mode: "enforce" },
247
+ { row: taskRow({ agentId: "agent-2" }), callerIsLead: true },
248
+ );
249
+ expect(await handler(completion("x"), ctx)).toMatchObject({ action: "block" });
250
+ expect(jevCalls).toHaveLength(1);
251
+ expect(counters).toMatchObject({ total: 1, fail: 1, blocked: 1 });
252
+ });
253
+
254
+ test("a completion on an unassigned task reaches the judge", async () => {
255
+ jevScores(0.1);
256
+ const { ctx, counters, queries } = makeCtx(
257
+ { mode: "enforce" },
258
+ { row: taskRow({ agentId: null }) },
259
+ );
260
+ expect(await handler(completion("x"), ctx)).toMatchObject({ action: "block" });
261
+ expect(jevCalls).toHaveLength(1);
262
+ expect(counters).toMatchObject({ total: 1, fail: 1, blocked: 1 });
263
+ // Owner and unassigned cases never need the caller's agents row.
264
+ expect(queries).toHaveLength(1);
265
+ });
266
+
267
+ test("mode off never reads the task", async () => {
268
+ jevScores(0.1);
269
+ const { ctx, queries } = makeCtx({ mode: "off" });
270
+ expect(await handler(completion("x"), ctx)).toBeUndefined();
271
+ expect(queries).toHaveLength(0);
272
+ });
273
+ });
274
+
275
+ describe("block", () => {
276
+ test("enforce blocks a failing output once per task, then lets it through", async () => {
277
+ jevScores({ verbosity: 0.2, tone: 0.9, language: 0.95 });
278
+ const { ctx, kv, counters } = makeCtx({ mode: "enforce" });
279
+ const first = await handler(completion("Great question! Let me walk you through..."), ctx);
280
+ expect(first).toMatchObject({ action: "block" });
281
+ expect(first.reason).toContain("Alice's stated preferences");
282
+ expect(first.reason).toContain("verbosity (0.20 vs 0.5)");
283
+ expect(first.reason).toContain("very terse");
284
+ expect(counters).toMatchObject({ total: 1, fail: 1, blocked: 1, "dim:verbosity": 1 });
285
+ expect(counters[`blocks:${TASK_ID}`]).toBe(1);
286
+ expect(Object.values(kv).some((v: any) => v?.action === "blocked")).toBe(true);
287
+
288
+ const second = await handler(completion("Still long."), ctx);
289
+ expect(second).toBeUndefined();
290
+ expect(counters).toMatchObject({ total: 2, fail: 2, blocked: 1, escaped: 1 });
291
+ });
292
+
293
+ test("enforce with the default threshold passes a 0.6 score", async () => {
294
+ jevScores({ verbosity: 0.6, tone: 0.9, language: 0.95 });
295
+ const { ctx, counters } = makeCtx({ mode: "enforce" });
296
+ expect(await handler(completion("Picked B."), ctx)).toBeUndefined();
297
+ expect(counters).toMatchObject({ total: 1, pass: 1 });
298
+ expect(counters.blocked).toBeUndefined();
299
+ });
300
+
301
+ test("enforce with threshold 0.75 blocks a 0.6 score", async () => {
302
+ jevScores({ verbosity: 0.6, tone: 0.9, language: 0.95 });
303
+ const { ctx, counters } = makeCtx({ mode: "enforce", threshold: 0.75 });
304
+ const result = await handler(completion("Picked B."), ctx);
305
+ expect(result).toMatchObject({ action: "block" });
306
+ expect(result.reason).toContain("verbosity (0.60 vs 0.75)");
307
+ expect(counters).toMatchObject({ total: 1, fail: 1, blocked: 1 });
308
+ });
309
+
310
+ test("shadow records the failure and never blocks", async () => {
311
+ jevScores(0.1);
312
+ const { ctx, counters } = makeCtx();
313
+ expect(await handler(completion("x"), ctx)).toBeUndefined();
314
+ expect(counters).toMatchObject({ fail: 1, shadowed: 1 });
315
+ expect(counters.blocked).toBeUndefined();
316
+ });
317
+
318
+ test("openrouter carries the same request with its own model", async () => {
319
+ jevScores(0.1);
320
+ const { ctx } = makeCtx({ mode: "enforce" }, { key: "", openrouterKey: "or-test" });
321
+ const result = await handler(completion("x"), ctx);
322
+ expect(result).toMatchObject({ action: "block" });
323
+ expect(jevCalls[0]!.url).toBe("https://openrouter.ai/api/alpha/decisions");
324
+ expect(jevCalls[0]!.auth).toBe("Bearer or-test");
325
+ expect(jevCalls[0]!.body.model).toBe("typesafe/jev-1.13");
326
+ });
327
+ });
328
+
329
+ describe("fail open", () => {
330
+ const expectFailOpen = async (ctxBundle: ReturnType<typeof makeCtx>, pattern: RegExp) => {
331
+ const result = await handler(completion("x"), ctxBundle.ctx);
332
+ expect(result).toBeUndefined();
333
+ expect(ctxBundle.counters.error).toBe(1);
334
+ expect(String((ctxBundle.kv["error:last"] as any)?.error)).toMatch(pattern);
335
+ expect(ctxBundle.counters.blocked).toBeUndefined();
336
+ };
337
+
338
+ test("no key", async () => {
339
+ jevScores(0.1);
340
+ await expectFailOpen(
341
+ makeCtx({ mode: "enforce" }, { key: "" }),
342
+ /no TYPESAFE_API_KEY or OPENROUTER_API_KEY/,
343
+ );
344
+ expect(jevCalls).toHaveLength(0);
345
+ });
346
+
347
+ test("a redacted key counts as missing", async () => {
348
+ jevScores(0.1);
349
+ await expectFailOpen(
350
+ makeCtx({ mode: "enforce", provider: "typesafe" }, { key: "[REDACTED:TYPESAFE_API_KEY]" }),
351
+ /no TYPESAFE_API_KEY/,
352
+ );
353
+ });
354
+
355
+ test("HTTP error", async () => {
356
+ mockJev(() => new Response("nope", { status: 503 }));
357
+ await expectFailOpen(makeCtx({ mode: "enforce" }), /typesafe 503/);
358
+ });
359
+
360
+ test("network error", async () => {
361
+ mockJev(() => {
362
+ throw new Error("ECONNRESET");
363
+ });
364
+ await expectFailOpen(makeCtx({ mode: "enforce" }), /ECONNRESET/);
365
+ });
366
+
367
+ test("malformed answer", async () => {
368
+ mockJev(() => Response.json({ answers: { verbosity: { noul: "high" } } }));
369
+ await expectFailOpen(makeCtx({ mode: "enforce" }), /no scores in response/);
370
+ });
371
+
372
+ test("timeout", async () => {
373
+ // Honors the abort signal like real fetch; never answers on its own.
374
+ globalThis.fetch = ((_url: any, init: any) =>
375
+ new Promise<Response>((_, reject) => {
376
+ init?.signal?.addEventListener("abort", () => reject(new Error("aborted by signal")));
377
+ })) as typeof fetch;
378
+ await expectFailOpen(makeCtx({ mode: "enforce", timeoutMs: 10 }), /aborted by signal/);
379
+ });
380
+
381
+ test("DB read failure", async () => {
382
+ jevScores(0.1);
383
+ await expectFailOpen(makeCtx({ mode: "enforce" }, { dbThrows: true }), /db down/);
384
+ expect(jevCalls).toHaveLength(0);
385
+ });
386
+ });
387
+
388
+ describe("self-test", () => {
389
+ test("records both controls without judging real traffic", async () => {
390
+ mockJev((body) =>
391
+ Response.json({
392
+ answers: {
393
+ verbosity: { noul: String(body.state.agent_message).startsWith("Great") ? 0.1 : 0.9 },
394
+ },
395
+ }),
396
+ );
397
+ const { ctx, kv, queries, counters } = makeCtx();
398
+ const event = {
399
+ tool: "store-progress",
400
+ args: { taskId: TASK_ID, status: "in_progress", progress: "comms-gate-selftest now" },
401
+ requestInfo: { callOrigin: "mcp" },
402
+ };
403
+ expect(await handler(event, ctx)).toBeUndefined();
404
+ expect(kv["selftest:last"]).toMatchObject({
405
+ positiveControlBadScore: 0.1,
406
+ negativeControlGoodScore: 0.9,
407
+ controlsOk: true,
408
+ provider: "typesafe",
409
+ });
410
+ expect(queries).toHaveLength(0);
411
+ expect(counters.total).toBeUndefined();
412
+ });
413
+ });
@@ -52,6 +52,21 @@ describe("extension loader", () => {
52
52
  expect(await Bun.file(loaded.sourcePath).exists()).toBe(false);
53
53
  });
54
54
 
55
+ test("loads hooks from a safe nested bundle path", async () => {
56
+ const bundle = await loadBundleFixture("minimal");
57
+ const hooks = bundle.files[bundle.manifest.assets.hooks]!;
58
+ bundle.manifest = {
59
+ ...bundle.manifest,
60
+ assets: { ...bundle.manifest.assets, hooks: "nested/hooks.ts" },
61
+ };
62
+ bundle.files = { "nested/hooks.ts": hooks };
63
+ const installed = await installExtension(bundle);
64
+ const loaded = await loadExtension({ record: installed.extension, ...bundle });
65
+
66
+ expect(await Bun.file(loaded.sourcePath).exists()).toBe(true);
67
+ await loaded.dispose();
68
+ });
69
+
55
70
  test("defensively rejects file traversal before writing bundle files", async () => {
56
71
  const input = await installLoadable("minimal");
57
72
  input.files["../escape.ts"] = "export default () => {};";
@@ -0,0 +1,168 @@
1
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { closeDb, createAgent, initDb, upsertSwarmConfig } from "../be/db";
4
+ import { buildScriptCredentialBindingsWithFailures } from "../be/script-credential-broker";
5
+ import { patchFetchWithEgressSubstitution } from "../scripts-runtime/egress-secrets";
6
+ import { runScript } from "../scripts-runtime/loader";
7
+ import { registerSystemOneDecideTool } from "../tools/system-one-decide";
8
+ import { refreshSecretScrubberCache } from "../utils/secret-scrubber";
9
+ import { SKIP_SANDBOX_SPAWN_TESTS } from "./sandbox-spawn-test-helpers";
10
+
11
+ // The Jev gate's two provider keys must never reach a script's JavaScript realm,
12
+ // and an OpenRouter key must never go to openrouter.ai while the deployment
13
+ // routes OpenRouter through a gateway. These tests drive the real credential
14
+ // resolver, the real fetch patch and the real script runtime.
15
+
16
+ const TYPESAFE_KEY = "typesafe-jev-regression-key";
17
+ const OPENROUTER_KEY = "openrouter-jev-regression-key";
18
+ const GATEWAY = "https://gateway.example/v1";
19
+
20
+ const savedEnv = { ...process.env };
21
+ const realFetch = globalThis.fetch;
22
+ const spawnTest = test.skipIf(SKIP_SANDBOX_SPAWN_TESTS);
23
+
24
+ let agentId: string;
25
+
26
+ beforeAll(async () => {
27
+ initDb(":memory:");
28
+ agentId = (await createAgent({ name: "jev-cred-lead", isLead: true, status: "idle" })).id;
29
+ for (const [key, value] of [
30
+ ["TYPESAFE_API_KEY", TYPESAFE_KEY],
31
+ ["OPENROUTER_API_KEY", OPENROUTER_KEY],
32
+ ]) {
33
+ await upsertSwarmConfig({ scope: "global", key, value, isSecret: true });
34
+ }
35
+ });
36
+
37
+ afterAll(() => closeDb());
38
+
39
+ beforeEach(() => {
40
+ process.env.AGENT_SWARM_API_KEY = "example-jev-gate-credentials-api-key-123";
41
+ // The gateway is set only in the environment, never as a stored config row.
42
+ process.env.OPENROUTER_BASE_URL = GATEWAY;
43
+ refreshSecretScrubberCache();
44
+ });
45
+
46
+ afterEach(() => {
47
+ globalThis.fetch = realFetch;
48
+ for (const key of Object.keys(process.env)) {
49
+ if (!(key in savedEnv)) delete process.env[key];
50
+ }
51
+ for (const [key, value] of Object.entries(savedEnv)) {
52
+ if (value === undefined) delete process.env[key];
53
+ else process.env[key] = value;
54
+ }
55
+ refreshSecretScrubberCache();
56
+ });
57
+
58
+ /** Records the Authorization header of every request that reaches the network layer. */
59
+ function recordAuthorization(): Array<{ url: string; authorization: string | null }> {
60
+ const sent: Array<{ url: string; authorization: string | null }> = [];
61
+ globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
62
+ const url = input instanceof Request ? input.url : String(input);
63
+ sent.push({ url, authorization: new Headers(init?.headers).get("authorization") });
64
+ return Response.json({
65
+ model: "jev-latest",
66
+ answers: { act: { type: "noul", noul: 0.2 } },
67
+ usage: { input_tokens: 10, output_tokens: 0 },
68
+ });
69
+ }) as typeof fetch;
70
+ return sent;
71
+ }
72
+
73
+ describe("script egress never carries the Jev keys", () => {
74
+ test("a script fetch to openrouter.ai or api.typesafe.ai gets no stored key", async () => {
75
+ const { egressSecrets } = await buildScriptCredentialBindingsWithFailures({ agentId });
76
+ const sent = recordAuthorization();
77
+ patchFetchWithEgressSubstitution(egressSecrets);
78
+ await fetch("https://openrouter.ai/api/alpha/decisions", {
79
+ method: "POST",
80
+ headers: { Authorization: "Bearer [REDACTED:OPENROUTER_API_KEY]" },
81
+ body: "{}",
82
+ });
83
+ await fetch("https://api.typesafe.ai/v1/systemone", {
84
+ method: "POST",
85
+ headers: { Authorization: "Bearer [REDACTED:TYPESAFE_API_KEY]" },
86
+ body: "{}",
87
+ });
88
+
89
+ expect(sent).toHaveLength(2);
90
+ for (const { authorization } of sent) {
91
+ expect(authorization ?? "").not.toContain(OPENROUTER_KEY);
92
+ expect(authorization ?? "").not.toContain(TYPESAFE_KEY);
93
+ }
94
+ expect(egressSecrets.map((s) => s.configKey)).not.toContain("OPENROUTER_API_KEY");
95
+ expect(egressSecrets.map((s) => s.configKey)).not.toContain("TYPESAFE_API_KEY");
96
+ });
97
+
98
+ spawnTest("a script that hooks Headers.prototype.set captures neither key", async () => {
99
+ // Same env-sourced resolution runScript() uses when the caller passes no bindings.
100
+ process.env.TYPESAFE_API_KEY = TYPESAFE_KEY;
101
+ process.env.OPENROUTER_API_KEY = OPENROUTER_KEY;
102
+ const output = await runScript({
103
+ agentId,
104
+ resources: { memoryMb: 2048 },
105
+ source: `
106
+ async function capture(url, placeholder) {
107
+ let captured = "";
108
+ const savedSet = Headers.prototype.set;
109
+ Headers.prototype.set = function (key, value) {
110
+ if (String(key).toLowerCase() === "authorization") captured = String(value).slice(7);
111
+ return savedSet.call(this, key, value);
112
+ };
113
+ const abort = new AbortController();
114
+ abort.abort();
115
+ await fetch(url, {
116
+ signal: abort.signal,
117
+ headers: { Authorization: "Bearer " + placeholder },
118
+ }).catch(() => {});
119
+ Headers.prototype.set = savedSet;
120
+ return btoa(captured);
121
+ }
122
+ export default async () => ({
123
+ typesafe: await capture("https://api.typesafe.ai/", "[REDACTED:TYPESAFE_API_KEY]"),
124
+ openrouter: await capture("https://openrouter.ai/", "[REDACTED:OPENROUTER_API_KEY]"),
125
+ });
126
+ `,
127
+ });
128
+
129
+ expect(output.error).toBeUndefined();
130
+ const result = output.result as { typesafe: string; openrouter: string };
131
+ expect(atob(result.typesafe)).not.toContain(TYPESAFE_KEY);
132
+ expect(atob(result.openrouter)).not.toContain(OPENROUTER_KEY);
133
+ });
134
+ });
135
+
136
+ describe("system-one-decide keeps the key on the server", () => {
137
+ const extra = () => ({ sessionId: "s", requestInfo: { headers: { "x-agent-id": agentId } } });
138
+ const tool = () => {
139
+ const server = new McpServer({ name: "jev-cred-decide", version: "1" });
140
+ registerSystemOneDecideTool(server);
141
+ return (server as any)._registeredTools["system-one-decide"];
142
+ };
143
+ const request = (provider: string) => ({
144
+ provider,
145
+ state: { now: "2026-10-16 10:00 UTC" },
146
+ questions: { act: { type: "noul", instructions: "Act now?" } },
147
+ maxRetries: 0,
148
+ });
149
+
150
+ test("openrouter is refused while OPENROUTER_BASE_URL is a gateway, with no request sent", async () => {
151
+ const sent = recordAuthorization();
152
+ const result = await tool().handler(request("openrouter"), extra());
153
+ expect(result.isError).toBe(true);
154
+ expect(result.structuredContent.message).toContain("OPENROUTER_BASE_URL points at a gateway");
155
+ expect(sent).toHaveLength(0);
156
+ });
157
+
158
+ test("typesafe answers; the key goes only to api.typesafe.ai and never into the result", async () => {
159
+ const sent = recordAuthorization();
160
+ const result = await tool().handler(request("typesafe"), extra());
161
+ expect(result.isError).toBeFalsy();
162
+ expect(result.structuredContent.answers.act.noul).toBe(0.2);
163
+ expect(sent).toEqual([
164
+ { url: "https://api.typesafe.ai/v1/systemone", authorization: `Bearer ${TYPESAFE_KEY}` },
165
+ ]);
166
+ expect(JSON.stringify(result)).not.toContain(TYPESAFE_KEY);
167
+ });
168
+ });