@desplega.ai/agent-swarm 1.166.0 → 1.167.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 (194) hide show
  1. package/dist/acp-adapter-zpvy6321.js +34 -0
  2. package/dist/{actions-qz3m692w.js → actions-fq2zrwtf.js} +25 -25
  3. package/dist/{amp-adapter-j1xbmsqz.js → amp-adapter-5gbk7501.js} +4 -4
  4. package/dist/{anthropic-messages-hst62ws9.js → anthropic-messages-13yn502s.js} +4 -4
  5. package/dist/{app-43vrdmv8.js → app-nctetn1e.js} +10 -9
  6. package/dist/{assistant-ertffmka.js → assistant-0aq8gjvj.js} +26 -26
  7. package/dist/{azure-openai-responses-mh8tvf78.js → azure-openai-responses-9st34pfz.js} +5 -5
  8. package/dist/{boot-reembed-998jtpdg.js → boot-reembed-1ze5853n.js} +9 -8
  9. package/dist/{boot-reembed-s7k7c6v2.js → boot-reembed-fmp5ww69.js} +9 -8
  10. package/dist/{boot-scrub-sweep-nepsk4t1.js → boot-scrub-sweep-cbp8xd4e.js} +10 -9
  11. package/dist/{boot-seeds-vvzs1zhk.js → boot-seeds-22tx8a53.js} +12 -11
  12. package/dist/{claude-adapter-2zvmekrp.js → claude-adapter-0jhgeh1d.js} +24 -22
  13. package/dist/{claude-managed-adapter-s49wbk8t.js → claude-managed-adapter-tzwxy803.js} +4 -4
  14. package/dist/{claude-managed-setup-cvhqq8qn.js → claude-managed-setup-nrhfx5b0.js} +3 -3
  15. package/dist/{claude-sdk-session-kkbxtw5q.js → claude-sdk-session-wh77rh83.js} +22 -22
  16. package/dist/{cli-zsh093hk.js → cli-014vns2y.js} +63 -1
  17. package/dist/{cli-qvfk83fy.js → cli-13xwcqvj.js} +2 -2
  18. package/dist/{cli-dw2q9f7g.js → cli-1yrcfa5s.js} +4 -4
  19. package/dist/{cli-jvj6a0cv.js → cli-577bxkf6.js} +27 -17
  20. package/dist/{cli-zy7vwsne.js → cli-5qkcdv8c.js} +16 -9
  21. package/dist/{cli-cwxhbf43.js → cli-5t5my0h8.js} +85 -2
  22. package/dist/{cli-y59h2pk9.js → cli-69egj654.js} +1 -1
  23. package/dist/{cli-68r1j4sg.js → cli-6bpvv8h4.js} +3 -3
  24. package/dist/{cli-cpfmqz3n.js → cli-6qxmjpem.js} +5 -5
  25. package/dist/{cli-aqcey741.js → cli-6vfz44sr.js} +1 -1
  26. package/dist/{cli-32bca7nr.js → cli-7avy4v8w.js} +1 -1
  27. package/dist/{cli-f2n17zhp.js → cli-7mhy3ths.js} +21 -18
  28. package/dist/{cli-q38n4bq0.js → cli-95zs84qw.js} +3 -3
  29. package/dist/cli-a35hzdve.js +33 -0
  30. package/dist/{cli-x3x0qwky.js → cli-bxqez1p0.js} +51 -23
  31. package/dist/{cli-gt8azyet.js → cli-c2tpw9ck.js} +1 -1
  32. package/dist/{cli-ecknp7p1.js → cli-c8r99akr.js} +10 -10
  33. package/dist/{cli-3qn4r334.js → cli-cexctgn5.js} +20 -14
  34. package/dist/{cli-vzc73wet.js → cli-cf2vgn9d.js} +87 -69
  35. package/dist/{cli-ng97fjq7.js → cli-cmxkbzzs.js} +33 -7
  36. package/dist/{cli-nexwh8zg.js → cli-g7k7zbcz.js} +12 -5
  37. package/dist/{cli-nvvfd8bq.js → cli-jbzbr36n.js} +4091 -3793
  38. package/dist/{cli-3ddybbbx.js → cli-jvmtxqtc.js} +26 -14
  39. package/dist/{cli-pkke6geq.js → cli-km4x66j0.js} +83 -65
  40. package/dist/{cli-j2jbr8af.js → cli-kw0jcfje.js} +1 -1
  41. package/dist/{cli-8t229cmr.js → cli-m7vmeepg.js} +30 -17
  42. package/dist/{cli-yshaazs1.js → cli-mkdsy1nq.js} +62 -20
  43. package/dist/{cli-b0f70z24.js → cli-mn8qkd8v.js} +1 -1
  44. package/dist/{cli-0qjcsxgz.js → cli-mxry91sa.js} +30 -9
  45. package/dist/cli-nkqjj42q.js +104 -0
  46. package/dist/{cli-rggdq8wq.js → cli-p8rt8spp.js} +3 -3
  47. package/dist/{cli-v2a8x45d.js → cli-pkj1w46r.js} +10 -5
  48. package/dist/cli-q1vnz8wv.js +96 -0
  49. package/dist/{cli-fj86xr3c.js → cli-qd97v6z2.js} +2 -2
  50. package/dist/{cli-66zh26sp.js → cli-qzmzwegq.js} +57 -17
  51. package/dist/{cli-p4ztb80h.js → cli-r7eeddkq.js} +13 -8
  52. package/dist/{cli-8ssjez7e.js → cli-rhy7z1rp.js} +22 -11
  53. package/dist/{cli-4mevd1hf.js → cli-s9y81s7j.js} +5 -5
  54. package/dist/{cli-h3x613ze.js → cli-tc5zec7v.js} +6 -6
  55. package/dist/{cli-ttbsse3f.js → cli-tmhsxhb8.js} +1 -1
  56. package/dist/{cli-0nhdwxd5.js → cli-tx2zvn23.js} +3 -3
  57. package/dist/{cli-mwegs4af.js → cli-ty4bzkqr.js} +3 -3
  58. package/dist/{cli-ysnyhws9.js → cli-v1pwxreq.js} +54 -51
  59. package/dist/{cli-feh3dkah.js → cli-v1w7pbd8.js} +2 -1
  60. package/dist/{cli-10zmp1p7.js → cli-vr5avem2.js} +27 -32
  61. package/dist/{cli-bzr0ftht.js → cli-wnb9b2g3.js} +19 -0
  62. package/dist/{cli-df56tqdw.js → cli-wydz3feg.js} +108 -57
  63. package/dist/{cli-krqm5f11.js → cli-xezn9nkh.js} +16 -13
  64. package/dist/{cli-8kt0bmgx.js → cli-xf713zz0.js} +2 -2
  65. package/dist/{cli-x7kpr72a.js → cli-y0kjpfm9.js} +2 -2
  66. package/dist/{cli-6zcfw0x7.js → cli-y195hv1a.js} +1 -1
  67. package/dist/{cli-fn697bnt.js → cli-ykt8f7va.js} +21 -8
  68. package/dist/{cli-4r1sjfc4.js → cli-ymmc1zxg.js} +8 -8
  69. package/dist/{cli-8kgvmkak.js → cli-ypgba1a8.js} +1 -1
  70. package/dist/{cli-rsy933yz.js → cli-z1dqhc9y.js} +30 -19
  71. package/dist/{cli-qbksc292.js → cli-zy39d433.js} +2 -0
  72. package/dist/cli.js +26 -26
  73. package/dist/{cloudflare-workers-ai-system-one-tt484t61.js → cloudflare-workers-ai-system-one-sta45jf9.js} +7 -5
  74. package/dist/{codex-adapter-dw1wxqt8.js → codex-adapter-cgesnjkr.js} +18 -18
  75. package/dist/{codex-login-terbdsqq.js → codex-login-g1cyt5zm.js} +1 -1
  76. package/dist/{codex-session-runner-3cgb8y1g.js → codex-session-runner-8kd7ddhr.js} +18 -18
  77. package/dist/{commands-mhxam6z2.js → commands-h6zaaxym.js} +9 -8
  78. package/dist/{cursor-adapter-2qda3st7.js → cursor-adapter-v8mm6bp5.js} +5 -5
  79. package/dist/{db-sjks418k.js → db-qg3myn53.js} +9 -8
  80. package/dist/{dispatcher-1es759rn.js → dispatcher-7vnwj9qw.js} +24 -24
  81. package/dist/{dsh-adapter-bek4cyw9.js → dsh-adapter-tz6hfp5r.js} +5 -5
  82. package/dist/{easter-egg-3d-mtpnw06s.js → easter-egg-3d-66zxervv.js} +2 -2
  83. package/dist/{execute-s4ccjq3n.js → execute-h4jgevyw.js} +61 -11
  84. package/dist/{google-generative-ai-d6nphg3q.js → google-generative-ai-b8zxrpax.js} +5 -5
  85. package/dist/{google-vertex-cpshkref.js → google-vertex-nrdd697t.js} +5 -5
  86. package/dist/{grok-adapter-ar9cw9jg.js → grok-adapter-jdt2dz2s.js} +18 -8
  87. package/dist/{handlers-1gkbhktr.js → handlers-2qtkpncr.js} +27 -27
  88. package/dist/{hook-ds2zxq9v.js → hook-51yfcgsv.js} +42 -31
  89. package/dist/{hook-vxa3tcsy.js → hook-qnbdgwvd.js} +14 -14
  90. package/dist/{http-3h21z9md.js → http-jsmtag08.js} +320 -98
  91. package/dist/{index-9b65j1m5.js → index-2t9kesn1.js} +9 -8
  92. package/dist/{index-dkfxpkmm.js → index-bt8wkv1w.js} +25 -25
  93. package/dist/{index-f8xnqrye.js → index-cx1sb9bq.js} +33 -24
  94. package/dist/{index-bn6cy8ps.js → index-ekywyv2v.js} +24 -24
  95. package/dist/{index-n99fycq7.js → index-t4g06w93.js} +24 -24
  96. package/dist/{keepalive-3xspa9jw.js → keepalive-4aezx4bj.js} +11 -10
  97. package/dist/{lead-6qphc124.js → lead-r44bkpm5.js} +47 -47
  98. package/dist/{llama-cpp-classify-tgj35k8d.js → llama-cpp-classify-6erb7wwv.js} +3 -1
  99. package/dist/{maintenance-0teem1bb.js → maintenance-grz5jrzh.js} +9 -8
  100. package/dist/{mistral-conversations-8445ddjn.js → mistral-conversations-059y51zt.js} +4 -4
  101. package/dist/{node-9sg7ajby.js → node-qm9eb6z0.js} +1 -1
  102. package/dist/{oauth-refresh-sweep-sh9q25xp.js → oauth-refresh-sweep-wm4ctjfj.js} +10 -9
  103. package/dist/{onboard-54qmnqxe.js → onboard-v60zgqf0.js} +2 -2
  104. package/dist/{openai-codex-responses-9ynm88zy.js → openai-codex-responses-jzxkta9b.js} +8 -7
  105. package/dist/{openai-completions-nhqwjmaw.js → openai-completions-663yncfq.js} +4 -4
  106. package/dist/openai-decisions-vj9nkpf8.js +162 -0
  107. package/dist/{openai-responses-xx1c4kgm.js → openai-responses-m3g43rrt.js} +5 -5
  108. package/dist/{opencode-adapter-wjfa2wvd.js → opencode-adapter-yjyqynzf.js} +7 -7
  109. package/dist/{openrouter-images-8yx3487g.js → openrouter-images-kr60n6b9.js} +1 -1
  110. package/dist/{otel-impl-ycqzp2a1.js → otel-impl-ghh4dyf8.js} +1 -1
  111. package/dist/{pi-messages-vxdt8v7j.js → pi-messages-xszk613n.js} +1 -1
  112. package/dist/{pi-mono-adapter-e77e9zst.js → pi-mono-adapter-0eg8kw2c.js} +68 -51
  113. package/dist/{pricing-refresh-atnkngqs.js → pricing-refresh-mjrqvnvw.js} +11 -10
  114. package/dist/{rbac-roles-j78wdksm.js → rbac-roles-axv4d74x.js} +10 -9
  115. package/dist/{rbac-roles-9pjphh9q.js → rbac-roles-fd5tskj9.js} +9 -8
  116. package/dist/{render-v2-pxjs49fx.js → render-v2-k50j33qd.js} +10 -9
  117. package/dist/{runtime-v2t1r79e.js → runtime-h9ha1mq6.js} +4 -4
  118. package/dist/{setup-ymej0xtx.js → setup-4syh0pd2.js} +2 -2
  119. package/dist/{typesafe-system-one-datfr82p.js → typesafe-system-one-e988vty7.js} +7 -5
  120. package/dist/{virtual-modules-wm8mxdxx.js → virtual-modules-ftr9kwe8.js} +11 -11
  121. package/dist/{work-objects-3nh4ymzb.js → work-objects-jcdbg8rr.js} +27 -27
  122. package/dist/{worker-qa9fkz5s.js → worker-x8j8c9sx.js} +47 -47
  123. package/openapi.json +1 -1
  124. package/package.json +8 -8
  125. package/src/be/memory/providers/openai-embedding.ts +7 -2
  126. package/src/be/memory/raters/llm-summarizer.ts +7 -2
  127. package/src/be/modelsdev-cache.json +61 -0
  128. package/src/be/seed-pricing.ts +84 -53
  129. package/src/be/swarm-config-guard.ts +13 -0
  130. package/src/commands/provider-credentials.ts +28 -5
  131. package/src/commands/runner.ts +7 -12
  132. package/src/heartbeat/heartbeat.ts +91 -19
  133. package/src/hooks/hook.ts +72 -27
  134. package/src/http/mcp.ts +5 -0
  135. package/src/http/onboarding.ts +2 -0
  136. package/src/providers/acp-targets.ts +9 -4
  137. package/src/providers/amp-adapter.ts +1 -1
  138. package/src/providers/claude-adapter.ts +26 -0
  139. package/src/providers/codex-adapter.ts +30 -3
  140. package/src/providers/codex-oauth/auth-json.ts +21 -4
  141. package/src/providers/codex-oauth/types.ts +2 -0
  142. package/src/providers/dsh-adapter.ts +10 -1
  143. package/src/providers/grok-adapter.ts +18 -2
  144. package/src/providers/opencode-adapter.ts +21 -5
  145. package/src/providers/pi-mono-extension.ts +56 -25
  146. package/src/providers/pricing-sources.md +2 -0
  147. package/src/server.ts +63 -3
  148. package/src/tests/acp-adapter.test.ts +152 -12
  149. package/src/tests/ci-unit-test-report.test.ts +73 -0
  150. package/src/tests/claude-default-route.test.ts +75 -0
  151. package/src/tests/claude-hook-output.test.ts +233 -0
  152. package/src/tests/codex-adapter-pool-auth-revalidate.test.ts +4 -2
  153. package/src/tests/codex-adapter.test.ts +166 -26
  154. package/src/tests/codex-oauth-auth-timestamps.test.ts +76 -0
  155. package/src/tests/codex-oauth.test.ts +4 -1
  156. package/src/tests/context-window.test.ts +2 -2
  157. package/src/tests/credential-check.test.ts +51 -1
  158. package/src/tests/dsh-adapter.test.ts +23 -1
  159. package/src/tests/enabled-tools.test.ts +269 -0
  160. package/src/tests/entrypoint-codex-oauth-seed.test.ts +12 -0
  161. package/src/tests/grok-adapter.test.ts +13 -0
  162. package/src/tests/haiku-5-5-pricing.test.ts +38 -0
  163. package/src/tests/heartbeat-checklist.test.ts +158 -1
  164. package/src/tests/onboarding.test.ts +44 -0
  165. package/src/tests/opencode-adapter.test.ts +66 -3
  166. package/src/tests/opencode-plugin.test.ts +37 -0
  167. package/src/tests/openrouter-attribution-call-sites.test.ts +204 -0
  168. package/src/tests/openrouter-base-url.test.ts +30 -39
  169. package/src/tests/pi-mono-extension.test.ts +161 -2
  170. package/src/tests/pricing-refresh.test.ts +70 -1
  171. package/src/tests/reasoning-effort.test.ts +3 -1
  172. package/src/tests/telemetry-trigger.test.ts +12 -1
  173. package/src/tests/workflow-llm.test.ts +14 -0
  174. package/src/tests/workflow-system-one-decision-providers.test.ts +33 -0
  175. package/src/tools/request-human-input.ts +7 -2
  176. package/src/utils/enabled-tools.ts +27 -0
  177. package/src/utils/internal-ai/complete-structured.ts +11 -3
  178. package/src/utils/openrouter-base-url.ts +23 -0
  179. package/src/workflows/executors/raw-llm.ts +1 -0
  180. package/src/workflows/executors/system-one-decision.ts +2 -0
  181. package/src/workflows/executors/validate.ts +1 -0
  182. package/src/workflows/executors/workflow-llm.ts +9 -2
  183. package/templates/skills/agent-browser/SKILL.md +2 -0
  184. package/templates/skills/agent-browser/content.md +2 -0
  185. package/templates/skills/code-quality/content.md +1 -0
  186. package/templates/skills/heartbeat-runbook/content.md +1 -1
  187. package/templates/skills/work-on-task/SKILL.md +4 -0
  188. package/templates/skills/work-on-task/content.md +4 -0
  189. package/templates/skills/workflow-iterate/content.md +9 -0
  190. package/dist/acp-adapter-yzfxn8vj.js +0 -23
  191. package/dist/cli-3dpj7atn.js +0 -11
  192. package/dist/cli-gvcgmx69.js +0 -183
  193. package/dist/{cli-jhm6hzjn.js → cli-mxvnnv5q.js} +7 -7
  194. package/dist/{codex-hook-d2rkvf39.js → codex-hook-15dgc4cb.js} +1 -1
@@ -355,6 +355,7 @@ describe("credentialsToAuthJson", () => {
355
355
  accountId: "c724a178-abc",
356
356
  };
357
357
 
358
+ const before = Date.now();
358
359
  const authJson = credentialsToAuthJson(creds);
359
360
  expect(authJson.auth_mode).toBe("chatgpt");
360
361
  expect(authJson.OPENAI_API_KEY).toBeNull();
@@ -362,7 +363,9 @@ describe("credentialsToAuthJson", () => {
362
363
  expect(authJson.tokens.refresh_token).toBe(creds.refresh);
363
364
  expect(authJson.tokens.account_id).toBe(creds.accountId);
364
365
  expect(authJson.tokens.id_token).toBe(creds.access);
365
- expect(authJson.last_refresh).toBe(new Date(creds.expires).toISOString());
366
+ expect(Date.parse(authJson.last_refresh)).toBeGreaterThanOrEqual(before);
367
+ expect(Date.parse(authJson.last_refresh)).toBeLessThanOrEqual(Date.now());
368
+ expect(authJson.expires).toBe(creds.expires);
366
369
  });
367
370
  });
368
371
 
@@ -32,9 +32,9 @@ describe("getContextWindowSize", () => {
32
32
  expect(getContextWindowSize("sonnet")).toBe(1_000_000);
33
33
  });
34
34
 
35
- test("returns 200K for haiku models", () => {
35
+ test("returns 200K for Haiku 4.5 and 1M for the haiku shortname (Haiku 5.5)", () => {
36
36
  expect(getContextWindowSize("claude-haiku-4-5")).toBe(200_000);
37
- expect(getContextWindowSize("haiku")).toBe(200_000);
37
+ expect(getContextWindowSize("haiku")).toBe(1_000_000);
38
38
  });
39
39
 
40
40
  test("reads OpenAI windows from the catalog", () => {
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, mock, test } from "bun:test";
2
2
  import {
3
3
  buildCredStatusReport,
4
+ CREDENTIAL_PROVIDER_CHECKERS,
4
5
  checkProviderCredentials,
5
6
  isBedrockMode,
6
7
  isCredCheckDisabled,
@@ -8,6 +9,7 @@ import {
8
9
  shouldRefreshBedrockStatus,
9
10
  validateProviderCredentials,
10
11
  } from "../commands/provider-credentials";
12
+ import { ACP_TARGET_IDS } from "../providers/acp-target-catalog";
11
13
  import { checkClaudeCredentials } from "../providers/claude-adapter";
12
14
  import { checkClaudeManagedCredentials } from "../providers/claude-managed-adapter";
13
15
  import { checkCodexCredentials } from "../providers/codex-adapter";
@@ -660,6 +662,54 @@ describe("checkOpencodeCredentials", () => {
660
662
  });
661
663
  });
662
664
 
665
+ describe("ACP readiness", () => {
666
+ test.each(["typo", "", " \t "])("rejects unsupported target %j", async (target) => {
667
+ const status = await checkProviderCredentials("acp", {
668
+ ACP_TARGET: target,
669
+ ACP_TARGET_COMMAND: "custom-acp --stdio",
670
+ });
671
+ expect(status.ready).toBe(false);
672
+ expect(status.missing).toEqual(["ACP_TARGET"]);
673
+ expect(status.hint).toContain("ACP_TARGET");
674
+ for (const supported of ACP_TARGET_IDS) expect(status.hint).toContain(supported);
675
+ });
676
+
677
+ test.each([undefined, "custom"])("requires a command for target %s", async (target) => {
678
+ const status = await CREDENTIAL_PROVIDER_CHECKERS.acp({ ACP_TARGET: target });
679
+ expect(status.ready).toBe(false);
680
+ expect(status.missing).toEqual(["ACP_TARGET_COMMAND"]);
681
+ expect(status.hint).toContain("ACP_TARGET_COMMAND");
682
+ expect(status.hint).toContain("ACP_COMMAND");
683
+ });
684
+
685
+ test.each(["", " \t "])("rejects a blank custom command %j", async (command) => {
686
+ const status = await checkProviderCredentials("acp", {
687
+ ACP_TARGET: "custom",
688
+ ACP_TARGET_COMMAND: command,
689
+ ACP_COMMAND: "legacy-acp",
690
+ });
691
+ expect(status.ready).toBe(false);
692
+ expect(status.missing).toEqual(["ACP_TARGET_COMMAND"]);
693
+ });
694
+
695
+ test.each(["ACP_TARGET_COMMAND", "ACP_COMMAND"])("accepts a command from %s", async (key) => {
696
+ for (const target of [undefined, "custom"]) {
697
+ const status = await checkProviderCredentials("acp", {
698
+ ACP_TARGET: target,
699
+ [key]: "custom-acp --stdio",
700
+ });
701
+ expect(status).toEqual({ ready: true, missing: [], satisfiedBy: "sdk-delegated" });
702
+ }
703
+ });
704
+
705
+ test.each(
706
+ ACP_TARGET_IDS.filter((target) => target !== "custom"),
707
+ )("keeps preset %s ready without a custom command", async (target) => {
708
+ const status = await checkProviderCredentials("acp", { ACP_TARGET: target });
709
+ expect(status).toEqual({ ready: true, missing: [], satisfiedBy: "sdk-delegated" });
710
+ });
711
+ });
712
+
663
713
  // ─── dispatcher ──────────────────────────────────────────────────────────────
664
714
 
665
715
  describe("checkProviderCredentials dispatcher", () => {
@@ -720,7 +770,7 @@ describe("checkProviderCredentials dispatcher", () => {
720
770
  ).ready,
721
771
  ).toBe(true);
722
772
 
723
- const acpStatus = await checkProviderCredentials("acp", {});
773
+ const acpStatus = await checkProviderCredentials("acp", { ACP_TARGET: "opencode" });
724
774
  expect(acpStatus.ready).toBe(true);
725
775
  expect(acpStatus.satisfiedBy).toBe("sdk-delegated");
726
776
  });
@@ -9,7 +9,10 @@ import type { ProviderEvent, ProviderSessionConfig } from "../providers/types";
9
9
  import { DEFAULT_MODEL_TIER_MAP } from "../types";
10
10
  import { getModelAwareCredentialVars } from "../utils/credentials";
11
11
  import { resolveHarnessProvider } from "../utils/harness-provider";
12
- import { DEFAULT_OPENROUTER_BASE_URL } from "../utils/openrouter-base-url";
12
+ import {
13
+ DEFAULT_OPENROUTER_BASE_URL,
14
+ OPENROUTER_APP_ATTRIBUTION_HEADERS,
15
+ } from "../utils/openrouter-base-url";
13
16
 
14
17
  const directories: string[] = [];
15
18
  afterEach(async () => {
@@ -166,6 +169,7 @@ describe("dsh harness", () => {
166
169
  openrouter: {
167
170
  apiKeyEnv: "OPENROUTER_API_KEY",
168
171
  baseURL: DEFAULT_OPENROUTER_BASE_URL,
172
+ headers: OPENROUTER_APP_ATTRIBUTION_HEADERS,
169
173
  api: "openai-completions",
170
174
  models: [{ id: modelId }],
171
175
  },
@@ -316,6 +320,24 @@ describe("dsh harness", () => {
316
320
  model: "deepseek/deepseek-v4.1-flash",
317
321
  });
318
322
  expect(patch[2].config.providers.openrouter.baseURL).toBe("https://gateway.example/proxy/v1");
323
+ // A gateway gets no OpenRouter app attribution.
324
+ expect(patch[2].config.providers.openrouter).not.toHaveProperty("headers");
325
+ });
326
+
327
+ test("OPENROUTER_APP_ATTRIBUTION=false drops the attribution headers", async () => {
328
+ const config = await fixture();
329
+ config.model = "openrouter/vendor/new-model";
330
+ config.env = {
331
+ ...config.env,
332
+ DEEPSEEK_API_KEY: "",
333
+ OPENROUTER_API_KEY: "openrouter-test-key",
334
+ OPENROUTER_APP_ATTRIBUTION: "false",
335
+ };
336
+ const session = await new DshAdapter().createSession(config);
337
+ expect((await session.waitForCompletion()).isError).toBe(false);
338
+ const { patch } = await Bun.file(join(config.cwd, "invocation.json")).json();
339
+ expect(patch[2].config.providers.openrouter.baseURL).toBe(DEFAULT_OPENROUTER_BASE_URL);
340
+ expect(patch[2].config.providers.openrouter).not.toHaveProperty("headers");
319
341
  });
320
342
 
321
343
  test("bare models keep direct DeepSeek when both keys exist", async () => {
@@ -0,0 +1,269 @@
1
+ import {
2
+ afterAll,
3
+ afterEach,
4
+ beforeAll,
5
+ beforeEach,
6
+ describe,
7
+ expect,
8
+ spyOn,
9
+ test,
10
+ } from "bun:test";
11
+ import { unlink } from "node:fs/promises";
12
+ import {
13
+ createServer as createHttpServer,
14
+ type IncomingMessage,
15
+ type Server,
16
+ type ServerResponse,
17
+ } from "node:http";
18
+ import type { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
19
+ import { closeDb, createAgent, getDbClient, initDb, upsertSwarmConfig } from "../be/db";
20
+ import { validateConfigValue } from "../be/swarm-config-guard";
21
+ import {
22
+ _resetAutoReloadForTests,
23
+ flushPendingIntegrationsReload,
24
+ loadGlobalConfigsIntoEnv,
25
+ } from "../http/core";
26
+ import { handleMcp } from "../http/mcp";
27
+ import { createServer } from "../server";
28
+ import { parseEnabledTools } from "../utils/enabled-tools";
29
+ import { listenOnFreePort } from "./test-net";
30
+
31
+ const TEST_DB_PATH = "./test-enabled-tools.sqlite";
32
+
33
+ async function removeDbFiles(path: string): Promise<void> {
34
+ for (const suffix of ["", "-wal", "-shm"]) {
35
+ try {
36
+ await unlink(path + suffix);
37
+ } catch (error) {
38
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
39
+ }
40
+ }
41
+ }
42
+
43
+ async function toolNames(opts: Parameters<typeof createServer>[0]): Promise<string[]> {
44
+ const server = (await createServer(opts)) as unknown as {
45
+ _registeredTools: Record<string, unknown>;
46
+ };
47
+ return Object.keys(server._registeredTools).sort();
48
+ }
49
+
50
+ function parseMcpPayload(text: string): unknown {
51
+ const trimmed = text.trim();
52
+ if (trimmed.startsWith("event:") || trimmed.startsWith("data:")) {
53
+ return JSON.parse(
54
+ trimmed
55
+ .split("\n")
56
+ .filter((line) => line.startsWith("data:"))
57
+ .map((line) => line.slice("data:".length).trim())
58
+ .join("\n"),
59
+ );
60
+ }
61
+ return JSON.parse(trimmed);
62
+ }
63
+
64
+ let httpServer: Server;
65
+ let baseUrl: string;
66
+ let savedEnv: string | undefined;
67
+ let savedCapabilities: string | undefined;
68
+
69
+ async function mcpPost(agentId: string, body: Record<string, unknown>, sessionId?: string) {
70
+ const headers: Record<string, string> = {
71
+ Accept: "application/json, text/event-stream",
72
+ "Content-Type": "application/json",
73
+ "X-Agent-ID": agentId,
74
+ };
75
+ if (sessionId) headers["mcp-session-id"] = sessionId;
76
+ const response = await fetch(`${baseUrl}/mcp`, {
77
+ method: "POST",
78
+ headers,
79
+ body: JSON.stringify(body),
80
+ });
81
+ const text = await response.text();
82
+ return { response, payload: text ? parseMcpPayload(text) : null };
83
+ }
84
+
85
+ async function listSessionTools(agentId: string): Promise<string[]> {
86
+ const init = await mcpPost(agentId, {
87
+ jsonrpc: "2.0",
88
+ id: 1,
89
+ method: "initialize",
90
+ params: {
91
+ protocolVersion: "2024-11-05",
92
+ clientInfo: { name: "enabled-tools", version: "1" },
93
+ capabilities: {},
94
+ },
95
+ });
96
+ const sessionId = init.response.headers.get("mcp-session-id");
97
+ if (!sessionId) throw new Error("missing MCP session ID");
98
+ await mcpPost(agentId, { jsonrpc: "2.0", method: "notifications/initialized" }, sessionId);
99
+ const listed = await mcpPost(
100
+ agentId,
101
+ { jsonrpc: "2.0", id: 2, method: "tools/list", params: {} },
102
+ sessionId,
103
+ );
104
+ return (listed.payload as { result: { tools: Array<{ name: string }> } }).result.tools
105
+ .map((tool) => tool.name)
106
+ .sort();
107
+ }
108
+
109
+ beforeAll(async () => {
110
+ // Drain any config reload a previous suite left queued (see
111
+ // scripts-only-gating.test.ts) before touching the global config rows.
112
+ const originalSlackDisable = process.env.SLACK_DISABLE;
113
+ process.env.SLACK_DISABLE = "true";
114
+ await flushPendingIntegrationsReload();
115
+ _resetAutoReloadForTests();
116
+ if (originalSlackDisable === undefined) delete process.env.SLACK_DISABLE;
117
+ else process.env.SLACK_DISABLE = originalSlackDisable;
118
+
119
+ await removeDbFiles(TEST_DB_PATH);
120
+ closeDb();
121
+ initDb(TEST_DB_PATH);
122
+
123
+ const transports: Record<string, StreamableHTTPServerTransport> = {};
124
+ const sessionAgents: Record<string, string> = {};
125
+ httpServer = createHttpServer(async (req: IncomingMessage, res: ServerResponse) => {
126
+ if (await handleMcp(req, res, transports, {}, sessionAgents)) return;
127
+ res.writeHead(404);
128
+ res.end("Not Found");
129
+ });
130
+ baseUrl = `http://127.0.0.1:${await listenOnFreePort(httpServer, "127.0.0.1")}`;
131
+ });
132
+
133
+ afterAll(async () => {
134
+ await new Promise<void>((resolve) => httpServer.close(() => resolve()));
135
+ closeDb();
136
+ await removeDbFiles(TEST_DB_PATH);
137
+ });
138
+
139
+ beforeEach(() => {
140
+ savedEnv = process.env.SWARM_ENABLED_TOOLS;
141
+ delete process.env.SWARM_ENABLED_TOOLS;
142
+ // Pin the default capability set; worker containers export their own.
143
+ savedCapabilities = process.env.CAPABILITIES;
144
+ delete process.env.CAPABILITIES;
145
+ });
146
+
147
+ afterEach(async () => {
148
+ await getDbClient().run("DELETE FROM swarm_config WHERE key = 'SWARM_ENABLED_TOOLS'");
149
+ // Un-inject the global row so it cannot leak into later suites.
150
+ await loadGlobalConfigsIntoEnv(true);
151
+ if (savedEnv === undefined) delete process.env.SWARM_ENABLED_TOOLS;
152
+ else process.env.SWARM_ENABLED_TOOLS = savedEnv;
153
+ if (savedCapabilities === undefined) delete process.env.CAPABILITIES;
154
+ else process.env.CAPABILITIES = savedCapabilities;
155
+ });
156
+
157
+ describe("parseEnabledTools", () => {
158
+ test.each([
159
+ [undefined, undefined],
160
+ ["", undefined],
161
+ [" ", undefined],
162
+ [" , ,", undefined],
163
+ ["[]", undefined],
164
+ ["get-tasks, store-progress", ["get-tasks", "store-progress"]],
165
+ ["get-tasks,get-tasks,", ["get-tasks"]],
166
+ ['["get-tasks", " store-progress "]', ["get-tasks", "store-progress"]],
167
+ ] as Array<[string | undefined, string[] | undefined]>)("parses %#", (value, expected) => {
168
+ expect(parseEnabledTools(value)).toEqual(expected);
169
+ });
170
+
171
+ test("rejects a JSON value that is not an array of strings", () => {
172
+ expect(() => parseEnabledTools("[1, 2]")).toThrow();
173
+ expect(() => parseEnabledTools("[not json")).toThrow();
174
+ expect(validateConfigValue("SWARM_ENABLED_TOOLS", "[1]")).toContain("SWARM_ENABLED_TOOLS");
175
+ expect(validateConfigValue("SWARM_ENABLED_TOOLS", "get-tasks,nope")).toBeNull();
176
+ });
177
+ });
178
+
179
+ describe("SWARM_ENABLED_TOOLS on the MCP server", () => {
180
+ test("unset keeps the capability-driven surface", async () => {
181
+ const unset = await toolNames({});
182
+ expect(unset).toContain("send-task");
183
+ // messaging is a default-disabled capability
184
+ expect(unset).not.toContain("post-message");
185
+ expect(await toolNames({ enabledTools: undefined })).toEqual(unset);
186
+ });
187
+
188
+ test("set registers exactly the listed tools, ignoring capabilities", async () => {
189
+ expect(await toolNames({ enabledTools: "get-tasks, post-message" })).toEqual([
190
+ "get-tasks",
191
+ "post-message",
192
+ ]);
193
+ });
194
+
195
+ test("reads the deployment env when no session value is passed", async () => {
196
+ process.env.SWARM_ENABLED_TOOLS = "store-progress";
197
+ expect(await toolNames({})).toEqual(["store-progress"]);
198
+ });
199
+
200
+ test("overrides scripts-only mode", async () => {
201
+ expect(await toolNames({ scriptsOnly: true, enabledTools: "get-tasks" })).toEqual([
202
+ "get-tasks",
203
+ ]);
204
+ });
205
+
206
+ test("does not affect the full-surface bridge server", async () => {
207
+ const tools = await toolNames({ fullSurface: true, enabledTools: "get-tasks" });
208
+ expect(tools).toContain("post-message");
209
+ expect(tools.length).toBeGreaterThan(1);
210
+ });
211
+
212
+ test("warns once on unknown names and keeps the known ones", async () => {
213
+ const warn = spyOn(console, "warn").mockImplementation(() => {});
214
+ try {
215
+ const value = "get-tasks, no-such-tool-a1, no-such-tool-b2";
216
+ expect(await toolNames({ enabledTools: value })).toEqual(["get-tasks"]);
217
+ await toolNames({ enabledTools: value });
218
+ const messages = warn.mock.calls.map((call) => String(call[0]));
219
+ const unknown = messages.filter((m) => m.includes("unknown tools"));
220
+ expect(unknown).toEqual([
221
+ "[MCP] SWARM_ENABLED_TOOLS names unknown tools, ignoring them: no-such-tool-a1, no-such-tool-b2",
222
+ ]);
223
+ } finally {
224
+ warn.mockRestore();
225
+ }
226
+ });
227
+
228
+ test("an empty value logs and behaves as unset", async () => {
229
+ const unset = await toolNames({});
230
+ const warn = spyOn(console, "warn").mockImplementation(() => {});
231
+ try {
232
+ expect(await toolNames({ enabledTools: " , " })).toEqual(unset);
233
+ expect(await toolNames({ enabledTools: "" })).toEqual(unset);
234
+ expect(
235
+ warn.mock.calls.some((call) => String(call[0]).includes("lists no tools; ignoring it")),
236
+ ).toBe(true);
237
+ } finally {
238
+ warn.mockRestore();
239
+ }
240
+ });
241
+
242
+ test("an agent-scoped value beats the swarm-wide one, per session", async () => {
243
+ const scoped = await createAgent({ name: "allowlist-agent", isLead: false, status: "idle" });
244
+ const neighbor = await createAgent({
245
+ name: "allowlist-neighbor",
246
+ isLead: false,
247
+ status: "idle",
248
+ });
249
+ await upsertSwarmConfig({ scope: "global", key: "SWARM_ENABLED_TOOLS", value: "get-tasks" });
250
+ await upsertSwarmConfig({
251
+ scope: "agent",
252
+ scopeId: scoped.id,
253
+ key: "SWARM_ENABLED_TOOLS",
254
+ value: "store-progress,post-message",
255
+ });
256
+
257
+ expect(await listSessionTools(scoped.id)).toEqual(["post-message", "store-progress"]);
258
+ expect(await listSessionTools(neighbor.id)).toEqual(["get-tasks"]);
259
+
260
+ // Reloadable: a config change applies on the next session, no restart.
261
+ await upsertSwarmConfig({
262
+ scope: "agent",
263
+ scopeId: scoped.id,
264
+ key: "SWARM_ENABLED_TOOLS",
265
+ value: "store-progress",
266
+ });
267
+ expect(await listSessionTools(scoped.id)).toEqual(["store-progress"]);
268
+ });
269
+ });
@@ -119,6 +119,7 @@ describe("docker-entrypoint.sh: codex_oauth boot-seed jq transform", () => {
119
119
  test(
120
120
  "blanks refresh_token for a flat {access,refresh,accountId,expires} input",
121
121
  async () => {
122
+ const before = Math.floor(Date.now() / 1000) * 1000;
122
123
  const result = (await runJqFilter(filter, {
123
124
  access: "example-access-tok",
124
125
  refresh: "example-live-refresh-tok",
@@ -126,6 +127,8 @@ describe("docker-entrypoint.sh: codex_oauth boot-seed jq transform", () => {
126
127
  expires: 1_800_000_000_000,
127
128
  })) as {
128
129
  auth_mode: string;
130
+ last_refresh: string;
131
+ expires: number;
129
132
  tokens: {
130
133
  id_token: string;
131
134
  access_token: string;
@@ -139,6 +142,9 @@ describe("docker-entrypoint.sh: codex_oauth boot-seed jq transform", () => {
139
142
  expect(result.tokens.id_token).toBe("example-access-tok");
140
143
  expect(result.tokens.access_token).toBe("example-access-tok");
141
144
  expect(result.tokens.account_id).toBe("acct-456");
145
+ expect(Date.parse(result.last_refresh)).toBeGreaterThanOrEqual(before);
146
+ expect(Date.parse(result.last_refresh)).toBeLessThanOrEqual(Date.now());
147
+ expect(result.expires).toBe(1_800_000_000_000);
142
148
  },
143
149
  CHILD_PROCESS_TEST_BUDGET_MS,
144
150
  );
@@ -182,6 +188,7 @@ describe("docker-entrypoint.sh: codex_oauth boot-seed jq transform", () => {
182
188
  test(
183
189
  "preserves refresh_token (from the flat `refresh` field) for a flat input on the standalone path",
184
190
  async () => {
191
+ const before = Math.floor(Date.now() / 1000) * 1000;
185
192
  const result = (await runJqFilter(
186
193
  filter,
187
194
  {
@@ -193,9 +200,14 @@ describe("docker-entrypoint.sh: codex_oauth boot-seed jq transform", () => {
193
200
  true,
194
201
  )) as {
195
202
  tokens: { refresh_token: string };
203
+ last_refresh: string;
204
+ expires: number;
196
205
  };
197
206
 
198
207
  expect(result.tokens.refresh_token).toBe("example-live-refresh-tok");
208
+ expect(Date.parse(result.last_refresh)).toBeGreaterThanOrEqual(before);
209
+ expect(Date.parse(result.last_refresh)).toBeLessThanOrEqual(Date.now());
210
+ expect(result.expires).toBe(1_800_000_000_000);
199
211
  },
200
212
  CHILD_PROCESS_TEST_BUDGET_MS,
201
213
  );
@@ -25,6 +25,7 @@ import {
25
25
  buildGrokConfigToml,
26
26
  checkGrokCredentials,
27
27
  GrokAdapter,
28
+ grokOpenRouterModel,
28
29
  installedClaudePluginNames,
29
30
  } from "../providers/grok-adapter";
30
31
  import type { ProviderEvent, ProviderSessionConfig } from "../providers/types";
@@ -487,6 +488,7 @@ describe("GrokAdapter", () => {
487
488
  'base_url = "https://openrouter.ai/api/v1"',
488
489
  'env_key = "OPENROUTER_API_KEY"',
489
490
  'api_backend = "chat_completions"',
491
+ 'extra_headers = { "HTTP-Referer" = "https://agent-swarm.dev", "X-OpenRouter-Title" = "Agent Swarm", "X-OpenRouter-Categories" = "personal-agent,cloud-agent" }',
490
492
  ].join("\n"),
491
493
  );
492
494
  } finally {
@@ -495,6 +497,17 @@ describe("GrokAdapter", () => {
495
497
  }
496
498
  });
497
499
 
500
+ test("OpenRouter app attribution rides only on direct openrouter.ai models", () => {
501
+ const toml = (env: Record<string, string>) =>
502
+ buildGrokConfigToml([], grokOpenRouterModel("openrouter/acme/thing", env));
503
+ expect(toml({})).toContain('extra_headers = { "HTTP-Referer" = "https://agent-swarm.dev"');
504
+ expect(toml({ OPENROUTER_BASE_URL: "https://gateway.example/v1" })).not.toContain(
505
+ "extra_headers",
506
+ );
507
+ expect(toml({ OPENROUTER_APP_ATTRIBUTION: "false" })).not.toContain("extra_headers");
508
+ expect(buildGrokConfigToml([], null)).not.toContain("extra_headers");
509
+ });
510
+
498
511
  test("an xai/-qualified model runs as the bare xAI id", async () => {
499
512
  const dir = makeTempDir();
500
513
  const grok = writeFakeGrok(dir, "ok");
@@ -0,0 +1,38 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { loadModelsDevCache } from "../be/modelsdev-cache";
3
+ import { buildModelsDevSeedRows } from "../be/seed-pricing";
4
+
5
+ function ratesFor(provider: string, model: string) {
6
+ const cache = loadModelsDevCache();
7
+ expect(cache).not.toBeNull();
8
+ return Object.fromEntries(
9
+ buildModelsDevSeedRows(cache!)
10
+ .filter((row) => row.provider === provider && row.model === model)
11
+ .map((row) => [row.tokenClass, row.pricePerMillionUsd]),
12
+ );
13
+ }
14
+
15
+ describe("Haiku 5.5 pricing seed", () => {
16
+ test("seeds Anthropic rates for the claude harnesses, including the 1h cache write", () => {
17
+ for (const provider of ["claude", "claude-managed"]) {
18
+ expect(ratesFor(provider, "claude-haiku-5-5")).toEqual({
19
+ input: 0.1,
20
+ output: 0.5,
21
+ cached_input: 0.01,
22
+ cache_write: 0.125,
23
+ cache_write_1h: 0.2,
24
+ });
25
+ }
26
+ });
27
+
28
+ test("seeds the same rates without a 1h class for amp and cursor", () => {
29
+ for (const provider of ["amp", "cursor"]) {
30
+ expect(ratesFor(provider, "claude-haiku-5-5")).toEqual({
31
+ input: 0.1,
32
+ output: 0.5,
33
+ cached_input: 0.01,
34
+ cache_write: 0.125,
35
+ });
36
+ }
37
+ });
38
+ });