@desplega.ai/agent-swarm 1.166.0 → 1.168.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 (221) hide show
  1. package/dist/acp-adapter-zhejfecx.js +34 -0
  2. package/dist/{actions-qz3m692w.js → actions-7jg840nr.js} +25 -25
  3. package/dist/{amp-adapter-j1xbmsqz.js → amp-adapter-ye6rnetm.js} +4 -4
  4. package/dist/{anthropic-messages-hst62ws9.js → anthropic-messages-13yn502s.js} +4 -4
  5. package/dist/{app-43vrdmv8.js → app-ffes2kyf.js} +10 -9
  6. package/dist/{assistant-ertffmka.js → assistant-fhm5smyz.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-20q3nzvg.js} +9 -8
  9. package/dist/{boot-reembed-s7k7c6v2.js → boot-reembed-jvfadhsk.js} +9 -8
  10. package/dist/{boot-scrub-sweep-nepsk4t1.js → boot-scrub-sweep-jtfajzve.js} +10 -9
  11. package/dist/{boot-seeds-vvzs1zhk.js → boot-seeds-9qt0j8j2.js} +12 -11
  12. package/dist/{claude-adapter-2zvmekrp.js → claude-adapter-a8znhthz.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-7x7chdrs.js} +7608 -7596
  16. package/dist/{cli-zsh093hk.js → cli-014vns2y.js} +63 -1
  17. package/dist/{cli-8kgvmkak.js → cli-0jngabtb.js} +1 -1
  18. package/dist/{cli-qvfk83fy.js → cli-13xwcqvj.js} +2 -2
  19. package/dist/{cli-0nhdwxd5.js → cli-43jg9xdw.js} +3 -3
  20. package/dist/{cli-cpfmqz3n.js → cli-47zfzshm.js} +5 -5
  21. package/dist/{cli-zy7vwsne.js → cli-5qkcdv8c.js} +16 -9
  22. package/dist/{cli-cwxhbf43.js → cli-5t5my0h8.js} +85 -2
  23. package/dist/{cli-y59h2pk9.js → cli-69egj654.js} +1 -1
  24. package/dist/{cli-68r1j4sg.js → cli-6bpvv8h4.js} +3 -3
  25. package/dist/{cli-yshaazs1.js → cli-74ez9b66.js} +63 -20
  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-fn697bnt.js → cli-9q0bh411.js} +21 -8
  30. package/dist/{cli-rggdq8wq.js → cli-9q24yyvs.js} +3 -3
  31. package/dist/cli-a35hzdve.js +33 -0
  32. package/dist/{cli-3qn4r334.js → cli-aj1e0h0d.js} +20 -14
  33. package/dist/{cli-rsy933yz.js → cli-bcspch6h.js} +30 -19
  34. package/dist/{cli-x3x0qwky.js → cli-bxqez1p0.js} +51 -23
  35. package/dist/{cli-ng97fjq7.js → cli-cmxkbzzs.js} +33 -7
  36. package/dist/{cli-df56tqdw.js → cli-ee0402n1.js} +198 -70
  37. package/dist/{cli-mwegs4af.js → cli-ew0c3a1j.js} +3 -3
  38. package/dist/{cli-ysnyhws9.js → cli-ewhwpyrb.js} +54 -51
  39. package/dist/{cli-j2jbr8af.js → cli-fhj4y16f.js} +1 -1
  40. package/dist/{cli-8t229cmr.js → cli-fqq06kkb.js} +30 -17
  41. package/dist/{cli-jvj6a0cv.js → cli-g6gx2dn1.js} +54 -19
  42. package/dist/{cli-nexwh8zg.js → cli-g7k7zbcz.js} +12 -5
  43. package/dist/{cli-ecknp7p1.js → cli-j05x7pnb.js} +10 -10
  44. package/dist/{cli-gt8azyet.js → cli-jb931ba2.js} +1 -1
  45. package/dist/{cli-nvvfd8bq.js → cli-jbzbr36n.js} +4091 -3793
  46. package/dist/{cli-3ddybbbx.js → cli-jvmtxqtc.js} +26 -14
  47. package/dist/{cli-pkke6geq.js → cli-km4x66j0.js} +83 -65
  48. package/dist/{cli-b0f70z24.js → cli-mn8qkd8v.js} +1 -1
  49. package/dist/{cli-0qjcsxgz.js → cli-nctfv2yy.js} +99 -9
  50. package/dist/{cli-4r1sjfc4.js → cli-ng1pmdx6.js} +10 -10
  51. package/dist/cli-nkqjj42q.js +104 -0
  52. package/dist/{cli-aqcey741.js → cli-p5jqk8fr.js} +1 -1
  53. package/dist/{cli-66zh26sp.js → cli-pgehz8e1.js} +57 -17
  54. package/dist/cli-q1vnz8wv.js +96 -0
  55. package/dist/{cli-fj86xr3c.js → cli-qd97v6z2.js} +2 -2
  56. package/dist/{cli-p4ztb80h.js → cli-r7eeddkq.js} +13 -8
  57. package/dist/{cli-8ssjez7e.js → cli-rhy7z1rp.js} +22 -11
  58. package/dist/{cli-vzc73wet.js → cli-rr5z1j4p.js} +87 -69
  59. package/dist/{cli-4mevd1hf.js → cli-s9y81s7j.js} +5 -5
  60. package/dist/{cli-dw2q9f7g.js → cli-svah6b6w.js} +4 -4
  61. package/dist/{cli-h3x613ze.js → cli-tc5zec7v.js} +6 -6
  62. package/dist/{cli-ttbsse3f.js → cli-tmhsxhb8.js} +1 -1
  63. package/dist/{cli-feh3dkah.js → cli-v1w7pbd8.js} +2 -1
  64. package/dist/{cli-10zmp1p7.js → cli-vr5avem2.js} +27 -32
  65. package/dist/{cli-bzr0ftht.js → cli-wnb9b2g3.js} +19 -0
  66. package/dist/{cli-krqm5f11.js → cli-xezn9nkh.js} +16 -13
  67. package/dist/{cli-8kt0bmgx.js → cli-xf713zz0.js} +2 -2
  68. package/dist/{cli-6zcfw0x7.js → cli-y195hv1a.js} +1 -1
  69. package/dist/{cli-x7kpr72a.js → cli-ytmmyj7f.js} +2 -2
  70. package/dist/{cli-v2a8x45d.js → cli-z48ggfyq.js} +11 -6
  71. package/dist/{cli-qbksc292.js → cli-zy39d433.js} +2 -0
  72. package/dist/cli.js +28 -28
  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-n63k33d3.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-xkxfd81a.js} +18 -18
  77. package/dist/{commands-mhxam6z2.js → commands-q4mfhn51.js} +9 -8
  78. package/dist/{cursor-adapter-2qda3st7.js → cursor-adapter-v8mm6bp5.js} +5 -5
  79. package/dist/{db-sjks418k.js → db-1t0bj3pc.js} +9 -8
  80. package/dist/{dispatcher-1es759rn.js → dispatcher-k1fhavcf.js} +24 -24
  81. package/dist/{dsh-adapter-bek4cyw9.js → dsh-adapter-5423p70m.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-phrndeb8.js} +18 -8
  87. package/dist/{handlers-1gkbhktr.js → handlers-59ywqwzh.js} +27 -27
  88. package/dist/{hook-vxa3tcsy.js → hook-atj0swc2.js} +14 -14
  89. package/dist/{hook-ds2zxq9v.js → hook-mew1chq9.js} +42 -31
  90. package/dist/{http-3h21z9md.js → http-ef2fnn9v.js} +953 -98
  91. package/dist/{index-n99fycq7.js → index-48pb9erw.js} +24 -24
  92. package/dist/{index-bn6cy8ps.js → index-9fc2h7ww.js} +24 -24
  93. package/dist/{index-9b65j1m5.js → index-gr69c43q.js} +9 -8
  94. package/dist/{index-f8xnqrye.js → index-h26wjcfy.js} +43 -24
  95. package/dist/{index-dkfxpkmm.js → index-w2yhye23.js} +25 -25
  96. package/dist/{keepalive-3xspa9jw.js → keepalive-qhcqz56v.js} +11 -10
  97. package/dist/{lead-6qphc124.js → lead-hpk5r3b1.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-nvkyp2xe.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-ngskthwh.js} +10 -9
  103. package/dist/{onboard-54qmnqxe.js → onboard-yfnkqhxc.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-hhm64r6m.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-35f91nx2.js} +11 -10
  114. package/dist/{rbac-roles-j78wdksm.js → rbac-roles-97w370qj.js} +10 -9
  115. package/dist/{rbac-roles-9pjphh9q.js → rbac-roles-z6sje3gy.js} +9 -8
  116. package/dist/{render-v2-pxjs49fx.js → render-v2-n4v66bj3.js} +10 -9
  117. package/dist/{runtime-v2t1r79e.js → runtime-h9ha1mq6.js} +4 -4
  118. package/dist/{setup-ymej0xtx.js → setup-m3hhyv5f.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-mxf2c842.js} +27 -27
  122. package/dist/{worker-qa9fkz5s.js → worker-9t07z1gq.js} +47 -47
  123. package/openapi.json +72 -1
  124. package/package.json +10 -10
  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/script-connections.ts +40 -6
  129. package/src/be/seed-pricing.ts +84 -53
  130. package/src/be/swarm-config-guard.ts +14 -0
  131. package/src/commands/provider-credentials.ts +28 -5
  132. package/src/commands/runner.ts +7 -12
  133. package/src/extensions/catalog.generated.json +17 -0
  134. package/src/heartbeat/heartbeat.ts +91 -19
  135. package/src/hooks/hook.ts +72 -27
  136. package/src/http/mcp.ts +5 -0
  137. package/src/http/onboarding.ts +2 -0
  138. package/src/http/script-connections.ts +35 -0
  139. package/src/providers/acp-targets.ts +10 -5
  140. package/src/providers/amp-adapter.ts +1 -1
  141. package/src/providers/claude-adapter.ts +32 -0
  142. package/src/providers/claude-background-keepalive.ts +100 -0
  143. package/src/providers/claude-sdk-session.ts +6 -0
  144. package/src/providers/codex-adapter.ts +30 -3
  145. package/src/providers/codex-oauth/auth-json.ts +21 -4
  146. package/src/providers/codex-oauth/types.ts +2 -0
  147. package/src/providers/dsh-adapter.ts +11 -2
  148. package/src/providers/grok-adapter.ts +18 -2
  149. package/src/providers/opencode-adapter.ts +21 -5
  150. package/src/providers/pi-mono-extension.ts +56 -25
  151. package/src/providers/pricing-sources.md +2 -0
  152. package/src/scheduler/scheduler.ts +82 -13
  153. package/src/scripts-runtime/executors/registry.ts +1 -1
  154. package/src/server.ts +63 -3
  155. package/src/tests/acp-adapter.test.ts +152 -12
  156. package/src/tests/ci-unit-test-report.test.ts +73 -0
  157. package/src/tests/claude-background-keepalive-wiring.test.ts +220 -0
  158. package/src/tests/claude-background-keepalive.fixture.ts +162 -0
  159. package/src/tests/claude-background-keepalive.test.ts +139 -0
  160. package/src/tests/claude-default-route.test.ts +75 -0
  161. package/src/tests/claude-hook-output.test.ts +233 -0
  162. package/src/tests/codex-adapter-pool-auth-revalidate.test.ts +4 -2
  163. package/src/tests/codex-adapter.test.ts +166 -26
  164. package/src/tests/codex-oauth-auth-timestamps.test.ts +76 -0
  165. package/src/tests/codex-oauth.test.ts +4 -1
  166. package/src/tests/connection-embedded-auth.test.ts +51 -0
  167. package/src/tests/context-window.test.ts +2 -2
  168. package/src/tests/credential-check.test.ts +51 -1
  169. package/src/tests/docs-index-footprint.test.js +3 -3
  170. package/src/tests/dsh-adapter.test.ts +24 -2
  171. package/src/tests/enabled-tools.test.ts +269 -0
  172. package/src/tests/entrypoint-codex-oauth-seed.test.ts +12 -0
  173. package/src/tests/extensions-catalog.test.ts +1 -0
  174. package/src/tests/extensions-example-slack-reply-gate.test.ts +756 -0
  175. package/src/tests/grok-adapter.test.ts +13 -0
  176. package/src/tests/haiku-5-5-pricing.test.ts +38 -0
  177. package/src/tests/heartbeat-checklist.test.ts +158 -1
  178. package/src/tests/onboarding.test.ts +44 -0
  179. package/src/tests/opencode-adapter.test.ts +66 -3
  180. package/src/tests/opencode-plugin.test.ts +37 -0
  181. package/src/tests/openrouter-attribution-call-sites.test.ts +204 -0
  182. package/src/tests/openrouter-base-url.test.ts +30 -39
  183. package/src/tests/pi-mono-extension.test.ts +161 -2
  184. package/src/tests/pricing-refresh.test.ts +70 -1
  185. package/src/tests/reasoning-effort.test.ts +3 -1
  186. package/src/tests/schedule-target-type.test.ts +136 -0
  187. package/src/tests/script-connections-http.test.ts +45 -0
  188. package/src/tests/script-connections-mcp.test.ts +41 -0
  189. package/src/tests/script-executor-registry.test.ts +18 -0
  190. package/src/tests/send-task-self-send.test.ts +107 -0
  191. package/src/tests/telemetry-trigger.test.ts +12 -1
  192. package/src/tests/workflow-llm.test.ts +14 -0
  193. package/src/tests/workflow-system-one-decision-providers.test.ts +33 -0
  194. package/src/tools/request-human-input.ts +7 -2
  195. package/src/tools/script-connections/tool.ts +35 -3
  196. package/src/tools/send-task.ts +9 -1
  197. package/src/utils/enabled-tools.ts +27 -0
  198. package/src/utils/internal-ai/complete-structured.ts +11 -3
  199. package/src/utils/openrouter-base-url.ts +23 -0
  200. package/src/workflows/executors/raw-llm.ts +1 -0
  201. package/src/workflows/executors/system-one-decision.ts +2 -0
  202. package/src/workflows/executors/validate.ts +1 -0
  203. package/src/workflows/executors/workflow-llm.ts +9 -2
  204. package/templates/extensions/README.md +1 -0
  205. package/templates/extensions/slack-reply-gate/README.md +98 -0
  206. package/templates/extensions/slack-reply-gate/hooks.ts +582 -0
  207. package/templates/extensions/slack-reply-gate/manifest.json +8 -0
  208. package/templates/skills/agent-browser/SKILL.md +2 -0
  209. package/templates/skills/agent-browser/content.md +2 -0
  210. package/templates/skills/code-quality/content.md +1 -0
  211. package/templates/skills/heartbeat-runbook/content.md +1 -1
  212. package/templates/skills/swarm-scripts/SKILL.md +10 -0
  213. package/templates/skills/swarm-scripts/content.md +10 -0
  214. package/templates/skills/work-on-task/SKILL.md +4 -0
  215. package/templates/skills/work-on-task/content.md +4 -0
  216. package/templates/skills/workflow-iterate/content.md +9 -0
  217. package/dist/acp-adapter-yzfxn8vj.js +0 -23
  218. package/dist/cli-3dpj7atn.js +0 -11
  219. package/dist/cli-gvcgmx69.js +0 -183
  220. package/dist/{cli-jhm6hzjn.js → cli-mxvnnv5q.js} +7 -7
  221. package/dist/{codex-hook-d2rkvf39.js → codex-hook-15dgc4cb.js} +1 -1
@@ -2,7 +2,7 @@
2
2
  * Conversion utilities between CodexOAuthCredentials (our internal type)
3
3
  * and ~/.codex/auth.json (the format the Codex CLI reads natively).
4
4
  *
5
- * The Codex CLI expects auth.json in this exact format:
5
+ * The Codex CLI expects these native auth.json fields:
6
6
  * {
7
7
  * "auth_mode": "chatgpt",
8
8
  * "OPENAI_API_KEY": null,
@@ -14,6 +14,7 @@
14
14
  * },
15
15
  * "last_refresh": "<ISO 8601>"
16
16
  * }
17
+ * Swarm also writes optional `expires` metadata for lossless conversion.
17
18
  *
18
19
  * Note: `id_token` is set to the `access_token` value because the token
19
20
  * exchange endpoint doesn't return a separate `id_token`. This matches
@@ -23,7 +24,7 @@
23
24
  */
24
25
 
25
26
  import { codexPlanFromClaim } from "../../utils/subscription-plans.js";
26
- import { extractChatgptPlanType, extractChatgptUserId } from "./flow.js";
27
+ import { decodeJwt, extractChatgptPlanType, extractChatgptUserId } from "./flow.js";
27
28
  import type { CodexAuthJson, CodexOAuthCredentials } from "./types.js";
28
29
 
29
30
  /**
@@ -96,15 +97,31 @@ export function credentialsToAuthJson(
96
97
  refresh_token: includeRefreshToken ? creds.refresh : "",
97
98
  account_id: creds.accountId,
98
99
  },
99
- last_refresh: new Date(creds.expires).toISOString(),
100
+ last_refresh: new Date().toISOString(),
101
+ // Swarm-only metadata keeps opaque-token round trips lossless. Codex
102
+ // ignores this field and may drop it when it rewrites auth.json.
103
+ expires: creds.expires,
100
104
  };
101
105
  }
102
106
 
103
107
  export function authJsonToCredentials(auth: CodexAuthJson): CodexOAuthCredentials {
108
+ // A CLI refresh may replace the access token and discard our metadata.
109
+ // Prefer its JWT expiry, which describes the token actually in the file.
110
+ const exp = decodeJwt(auth.tokens.access_token)?.exp;
111
+ const jwtExpires = typeof exp === "number" ? exp * 1000 : NaN;
112
+ const expires =
113
+ Number.isFinite(jwtExpires) && jwtExpires >= 0
114
+ ? jwtExpires
115
+ : typeof auth.expires === "number" && Number.isFinite(auth.expires)
116
+ ? auth.expires
117
+ : new Date(auth.last_refresh).getTime();
118
+
104
119
  return {
105
120
  access: auth.tokens.access_token,
106
121
  refresh: auth.tokens.refresh_token,
107
- expires: new Date(auth.last_refresh).getTime(),
122
+ // Legacy swarm files put expiry in last_refresh; keep that fallback
123
+ // only when neither the token nor the explicit metadata carries it.
124
+ expires,
108
125
  accountId: auth.tokens.account_id,
109
126
  };
110
127
  }
@@ -28,6 +28,8 @@ export type CodexAuthJson = {
28
28
  account_id: string;
29
29
  };
30
30
  last_refresh: string;
31
+ /** Swarm-only access-token expiry (milliseconds); native Codex files omit it. */
32
+ expires?: number;
31
33
  };
32
34
 
33
35
  type TokenResult =
@@ -9,7 +9,10 @@ import {
9
9
  } from "../utils/context-window";
10
10
  import { addDshStepUsage, type DshStepUsage, normalizeDshStepUsage } from "../utils/dsh-usage";
11
11
  import { swarmRuntimeInstanceId } from "../utils/multi-runtime";
12
- import { getOpenRouterBaseUrl } from "../utils/openrouter-base-url";
12
+ import {
13
+ getOpenRouterAttributionHeaders,
14
+ getOpenRouterBaseUrl,
15
+ } from "../utils/openrouter-base-url";
13
16
  import {
14
17
  detachedProcessGroup,
15
18
  registerProcessGroup,
@@ -34,7 +37,7 @@ import type {
34
37
  } from "./types";
35
38
 
36
39
  // npm's `latest` is older and lacks the JSON/stdin contract used here.
37
- export const DSH_PACKAGE = "@deepseek-ai/dsh@0.2.1-alpha.1";
40
+ export const DSH_PACKAGE = "@deepseek-ai/dsh@0.2.1-alpha.2";
38
41
 
39
42
  export function checkDshCredentials(env: Record<string, string | undefined>): CredStatus {
40
43
  return env.DEEPSEEK_API_KEY?.trim() || env.OPENROUTER_API_KEY?.trim()
@@ -342,6 +345,9 @@ export class DshAdapter implements ProviderAdapter {
342
345
  const directory = await mkdtemp(join(tmpdir(), "swarm-dsh-"));
343
346
  try {
344
347
  const patchPath = join(directory, "patch.json");
348
+ const openRouterHeaders = openrouter
349
+ ? getOpenRouterAttributionHeaders(getOpenRouterBaseUrl(env), env)
350
+ : {};
345
351
  const runtimeInstanceId = swarmRuntimeInstanceId();
346
352
  // JSON is valid YAML. Values stay data, including arbitrary system prompts.
347
353
  await writeFile(
@@ -364,6 +370,9 @@ export class DshAdapter implements ProviderAdapter {
364
370
  openrouter: {
365
371
  apiKeyEnv: keyEnv,
366
372
  baseURL: getOpenRouterBaseUrl(env),
373
+ ...(Object.keys(openRouterHeaders).length > 0
374
+ ? { headers: openRouterHeaders }
375
+ : {}),
367
376
  api: "openai-completions",
368
377
  // Declare the selected ID even if the bundled catalog predates it.
369
378
  models: [
@@ -3,7 +3,10 @@ import { mkdtemp, rm } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { normalizeGrokModel } from "@desplega/model-catalog";
6
- import { getOpenRouterBaseUrl } from "../utils/openrouter-base-url";
6
+ import {
7
+ getOpenRouterAttributionHeaders,
8
+ getOpenRouterBaseUrl,
9
+ } from "../utils/openrouter-base-url";
7
10
  import { registerVolatileSecret } from "../utils/secret-scrubber";
8
11
  import { ACPAdapter } from "./acp-adapter";
9
12
  import { grokTargetProfile, isGrokOpenRouterModel } from "./acp-targets";
@@ -64,6 +67,8 @@ export interface GrokCustomModel {
64
67
  baseUrl: string;
65
68
  /** Env var holding the endpoint's API key. */
66
69
  envKey: string;
70
+ /** Sent verbatim on every request to the endpoint (`extra_headers`). */
71
+ headers?: Record<string, string>;
67
72
  }
68
73
 
69
74
  /** `openrouter/<vendor>/<id>` -> a `[model."openrouter/<vendor>/<id>"]` block on OpenRouter. */
@@ -73,14 +78,24 @@ export function grokOpenRouterModel(
73
78
  ): GrokCustomModel | null {
74
79
  const id = model.trim();
75
80
  if (!isGrokOpenRouterModel(id)) return null;
81
+ const baseUrl = getOpenRouterBaseUrl(env);
76
82
  return {
77
83
  id,
78
84
  model: id.slice("openrouter/".length),
79
- baseUrl: getOpenRouterBaseUrl(env),
85
+ baseUrl,
80
86
  envKey: "OPENROUTER_API_KEY",
87
+ headers: getOpenRouterAttributionHeaders(baseUrl, env),
81
88
  };
82
89
  }
83
90
 
91
+ /** `key = { "k" = "v", ... }`, or nothing for an empty map. JSON strings are valid TOML basic strings. */
92
+ function tomlInlineTable(key: string, entries: Record<string, string> | undefined): string[] {
93
+ const pairs = Object.entries(entries ?? {});
94
+ if (pairs.length === 0) return [];
95
+ const body = pairs.map(([k, v]) => `${JSON.stringify(k)} = ${JSON.stringify(v)}`).join(", ");
96
+ return [`${key} = { ${body} }`];
97
+ }
98
+
84
99
  /** `$GROK_HOME/config.toml` for one swarm session. */
85
100
  export function buildGrokConfigToml(
86
101
  disabledPlugins: readonly string[],
@@ -95,6 +110,7 @@ export function buildGrokConfigToml(
95
110
  `base_url = ${JSON.stringify(customModel.baseUrl)}`,
96
111
  `env_key = ${JSON.stringify(customModel.envKey)}`,
97
112
  `api_backend = "chat_completions"`,
113
+ ...tomlInlineTable("extra_headers", customModel.headers),
98
114
  ]
99
115
  : [];
100
116
  return [
@@ -20,7 +20,11 @@ import {
20
20
  import { validateOpencodeCredentials } from "../utils/credentials";
21
21
  import { fetchInstalledMcpServers } from "../utils/mcp-server-fetcher";
22
22
  import { swarmRuntimeInstanceId } from "../utils/multi-runtime";
23
- import { DEFAULT_OPENROUTER_BASE_URL, getOpenRouterBaseUrl } from "../utils/openrouter-base-url";
23
+ import {
24
+ DEFAULT_OPENROUTER_BASE_URL,
25
+ getOpenRouterAttributionHeaders,
26
+ getOpenRouterBaseUrl,
27
+ } from "../utils/openrouter-base-url";
24
28
  import { scrubSecrets } from "../utils/secret-scrubber";
25
29
  import { resolveSlashSkillPrompt } from "./codex-skill-resolver";
26
30
  import { CTX_MODE_NUDGE_EVERY } from "./ctx-mode-env";
@@ -177,22 +181,34 @@ function formatUnknownError(err: unknown): string {
177
181
  * Route OpenRouter traffic through the configured gateway (see
178
182
  * src/utils/openrouter-base-url.ts). opencode's bundled openrouter provider
179
183
  * honors `provider.openrouter.options.baseURL`. Mutates `opencodeConfig` in
180
- * place; no-op when `OPENROUTER_BASE_URL` is unset/blank/default, so default
181
- * openrouter.ai behavior is preserved. Exported for tests.
184
+ * place, and attribute direct OpenRouter requests to Agent Swarm.
182
185
  */
183
186
  export function applyOpenRouterBaseUrlOverride(
184
187
  opencodeConfig: Config,
185
188
  env: Record<string, string | undefined> = process.env,
186
189
  ): void {
187
190
  const openRouterBaseUrl = getOpenRouterBaseUrl(env as NodeJS.ProcessEnv);
188
- if (openRouterBaseUrl === DEFAULT_OPENROUTER_BASE_URL) return;
191
+ const headers = getOpenRouterAttributionHeaders(openRouterBaseUrl, env);
192
+ const overrideBaseUrl = openRouterBaseUrl !== DEFAULT_OPENROUTER_BASE_URL;
193
+ if (!overrideBaseUrl && Object.keys(headers).length === 0) return;
189
194
  opencodeConfig.provider = {
190
195
  ...opencodeConfig.provider,
191
196
  openrouter: {
192
197
  ...opencodeConfig.provider?.openrouter,
193
198
  options: {
194
199
  ...opencodeConfig.provider?.openrouter?.options,
195
- baseURL: openRouterBaseUrl,
200
+ ...(overrideBaseUrl ? { baseURL: openRouterBaseUrl } : {}),
201
+ ...(Object.keys(headers).length > 0
202
+ ? {
203
+ headers: {
204
+ ...(opencodeConfig.provider?.openrouter?.options?.headers as
205
+ | Record<string, string>
206
+ | undefined),
207
+ ...headers,
208
+ "X-Title": "Agent Swarm",
209
+ },
210
+ }
211
+ : {}),
196
212
  },
197
213
  },
198
214
  };
@@ -11,6 +11,7 @@ import { buildRatingsFromLlm, fetchRetrievalsForTask, postRatings } from "../be/
11
11
  import { checkToolLoop, clearToolHistory } from "../hooks/tool-loop-detection";
12
12
  import { summarizeSession as runSummarize } from "../utils/internal-ai";
13
13
  import { getMemoryRaterNames } from "../utils/memory-raters";
14
+ import { getOpenRouterAttributionHeaders } from "../utils/openrouter-base-url";
14
15
  import { scrubSecrets } from "../utils/secret-scrubber";
15
16
 
16
17
  export interface SwarmHooksConfig {
@@ -421,6 +422,24 @@ export async function summarizeSessionForPi(
421
422
  }
422
423
  }
423
424
 
425
+ /** `customType` of the advisory messages this extension sends to the model. */
426
+ export const SWARM_NUDGE_CUSTOM_TYPE = "swarm-nudge";
427
+
428
+ /**
429
+ * Deliver an advisory nudge to the model. pi runs extensions in-process and
430
+ * never feeds their stdout to the model, so a console.log here only reaches
431
+ * the worker log. `sendMessage` steers the text in after the current tool
432
+ * batch while the agent is streaming, and otherwise appends it to the session
433
+ * so the next prompt carries it.
434
+ */
435
+ function sendNudge(pi: Parameters<ExtensionFactory>[0], text: string): void {
436
+ pi.sendMessage({
437
+ customType: SWARM_NUDGE_CUSTOM_TYPE,
438
+ content: [{ type: "text", text }],
439
+ display: false,
440
+ });
441
+ }
442
+
424
443
  /**
425
444
  * Create the swarm hooks extension factory for pi-mono.
426
445
  *
@@ -431,6 +450,13 @@ export function createSwarmHooksExtension(config: SwarmHooksConfig): ExtensionFa
431
450
  return (pi) => {
432
451
  let lastContextPostTime = 0;
433
452
 
453
+ pi.on("before_provider_headers", (event, ctx) => {
454
+ Object.assign(
455
+ event.headers,
456
+ getOpenRouterAttributionHeaders(ctx.model?.baseUrl ?? "", config.env),
457
+ );
458
+ });
459
+
434
460
  // === session_start → SessionStart ===
435
461
  pi.on("session_start", async (_event, _ctx) => {
436
462
  // Installed MCP servers: pi's MCP extension connects them in the
@@ -458,7 +484,7 @@ export function createSwarmHooksExtension(config: SwarmHooksConfig): ExtensionFa
458
484
  if (config.isLead) {
459
485
  const ctx = await fetchConcurrentContext(config);
460
486
  if (ctx) {
461
- console.log(ctx);
487
+ sendNudge(pi, ctx);
462
488
  }
463
489
  }
464
490
  });
@@ -498,7 +524,7 @@ export function createSwarmHooksExtension(config: SwarmHooksConfig): ExtensionFa
498
524
  }
499
525
 
500
526
  if (loopResult.severity === "warning" && loopResult.reason) {
501
- console.log(`Warning: ${loopResult.reason}`);
527
+ sendNudge(pi, `Warning: ${loopResult.reason}`);
502
528
  }
503
529
  }
504
530
 
@@ -586,7 +612,8 @@ export function createSwarmHooksExtension(config: SwarmHooksConfig): ExtensionFa
586
612
 
587
613
  // Reminders
588
614
  if (config.isLead && event.toolName?.endsWith("send-task")) {
589
- console.log(
615
+ sendNudge(
616
+ pi,
590
617
  "Task sent successfully. Monitor progress using the get-task-details tool periodically.",
591
618
  );
592
619
  }
@@ -594,29 +621,30 @@ export function createSwarmHooksExtension(config: SwarmHooksConfig): ExtensionFa
594
621
  return undefined;
595
622
  });
596
623
 
597
- // === context → PreCompact ===
598
- // The context event allows injecting messages before compaction.
599
- // We log the goal reminder to console (it gets captured in context).
600
- pi.on("context", async (_event, ctx) => {
601
- if (!config.taskId) return undefined;
602
-
603
- try {
604
- const taskDetails = await fetchTaskDetails(config);
605
- if (taskDetails) {
606
- const reminder = [
607
- "=== GOAL REMINDER (injected before context compaction) ===",
608
- `Task ID: ${taskDetails.id}`,
609
- `Task: ${taskDetails.task}`,
610
- ];
611
- if (taskDetails.progress) {
612
- reminder.push(`Current Progress: ${taskDetails.progress}`);
613
- }
614
- reminder.push("=== Continue working on this task after compaction ===");
615
- console.log(reminder.join("\n"));
624
+ // === session_compact → PreCompact ===
625
+ // Re-anchor the model on its task once compaction has summarized the
626
+ // history. `context` fires before every LLM call, so it cannot carry this.
627
+ pi.on("session_compact", async () => {
628
+ if (!config.taskId) return;
629
+
630
+ const taskDetails = await fetchTaskDetails(config);
631
+ if (taskDetails) {
632
+ const reminder = [
633
+ "=== GOAL REMINDER (injected before context compaction) ===",
634
+ `Task ID: ${taskDetails.id}`,
635
+ `Task: ${taskDetails.task}`,
636
+ ];
637
+ if (taskDetails.progress) {
638
+ reminder.push(`Current Progress: ${taskDetails.progress}`);
616
639
  }
617
- } catch {
618
- /* don't block compaction */
640
+ reminder.push("=== Continue working on this task after compaction ===");
641
+ sendNudge(pi, reminder.join("\n"));
619
642
  }
643
+ });
644
+
645
+ // === context (before every LLM call) ===
646
+ pi.on("context", async (_event, ctx) => {
647
+ if (!config.taskId) return undefined;
620
648
 
621
649
  // Report context usage as a compaction event
622
650
  const usage = ctx.getContextUsage?.();
@@ -644,7 +672,10 @@ export function createSwarmHooksExtension(config: SwarmHooksConfig): ExtensionFa
644
672
  const { cancelled, reason } = await isTaskCancelled(config);
645
673
  if (cancelled) {
646
674
  const cancelReason = reason || "Task cancelled by lead or creator";
647
- console.log(
675
+ // "handled" skips the turn; the nudge lands in the session for the
676
+ // next one instead of paying for a turn on a cancelled task.
677
+ sendNudge(
678
+ pi,
648
679
  `🛑 TASK CANCELLED: ${cancelReason}. Stop working and use store-progress to acknowledge.`,
649
680
  );
650
681
  return { action: "handled" as const };
@@ -178,6 +178,8 @@ and heartbeat-classified roots are omitted, and the metrics remain separate.
178
178
  - **Codex usage:** the app-server reports cumulative token counters, including cache writes.
179
179
  The adapter calculates each turn's delta before adding it to the session total.
180
180
  This prevents repeated billing of earlier turns when the adapter processes queued input.
181
+ Context occupancy is separate: it comes from the per-request `tokenUsage.last`
182
+ snapshot (and `modelContextWindow` when present), never from the turn delta.
181
183
  - **Claude breakdown validity is all-or-nothing:** the claude adapter drops the
182
184
  entire `modelUsage` breakdown when any entry carries a missing, non-finite,
183
185
  or negative token counter — zero-filling would let the server price a
@@ -15,6 +15,7 @@ import {
15
15
  getUserById,
16
16
  getWorkflow,
17
17
  getWorkflowRun,
18
+ recordInlineScriptRun,
18
19
  type UpdateScheduledTaskData,
19
20
  updateScheduledTask,
20
21
  } from "@/be/db";
@@ -32,6 +33,7 @@ import { startWorkflowExecution } from "@/workflows/engine";
32
33
  import type { ExecutorRegistry } from "@/workflows/executors/registry";
33
34
  import { handleScheduleTrigger } from "@/workflows/triggers";
34
35
  import { ensure } from "../utils/business-use";
36
+ import { scrubObject, scrubSecrets } from "../utils/secret-scrubber";
35
37
 
36
38
  import {
37
39
  dispatchDeferredTaskWait,
@@ -83,23 +85,90 @@ async function executeScheduleScript(schedule: ScheduledTask): Promise<void> {
83
85
  }
84
86
 
85
87
  const agentId = schedule.createdByAgentId ?? "schedule";
86
- const output = await runScript({
87
- source: script.source,
88
- args: schedule.scriptArgs ?? {},
89
- fsMode: "none",
88
+ const args = schedule.scriptArgs ?? {};
89
+ const record = {
90
90
  agentId,
91
- egressSecrets: await buildScriptCredentialBindings({ agentId }),
92
- apiConnections: getScriptApiConnectionDescriptors({ agentId }),
93
- mcpConnections: getScriptMcpConnectionDescriptors({ agentId }),
94
- timeoutMs: 60_000,
95
- });
91
+ scriptName: schedule.scriptName,
92
+ // A pointer, not a copy: script schedules fire as often as every minute and
93
+ // script_runs has no retention sweep, so copying the source on every fire
94
+ // would grow the table by tens of MB a day. script_versions keeps the text.
95
+ source: `// Scheduled run of '${script.name}' v${script.version} (global, contentHash ${script.contentHash}) by schedule '${schedule.name}'.\n// Full source: script_versions row for this version.\n`,
96
+ args,
97
+ startedAt: new Date().toISOString(),
98
+ };
99
+
100
+ let output: Awaited<ReturnType<typeof runScript>>;
101
+ try {
102
+ output = await runScript({
103
+ source: script.source,
104
+ args,
105
+ fsMode: "none",
106
+ agentId,
107
+ egressSecrets: await buildScriptCredentialBindings({ agentId }),
108
+ apiConnections: getScriptApiConnectionDescriptors({ agentId }),
109
+ mcpConnections: getScriptMcpConnectionDescriptors({ agentId }),
110
+ timeoutMs: 60_000,
111
+ });
112
+ } catch (err) {
113
+ await recordScheduleScriptRun({
114
+ ...record,
115
+ status: "failed",
116
+ error: err instanceof Error ? err.message : String(err),
117
+ });
118
+ throw err;
119
+ }
96
120
 
97
121
  if (output.exitCode !== 0 || output.error) {
98
- throw new Error(
122
+ const message =
99
123
  output.stderr ||
100
- `Script '${schedule.scriptName}' exited with code ${output.exitCode}${
101
- output.error ? ` (${output.error})` : ""
102
- }`,
124
+ `Script '${schedule.scriptName}' exited with code ${output.exitCode}${
125
+ output.error ? ` (${output.error})` : ""
126
+ }`;
127
+ await recordScheduleScriptRun({
128
+ ...record,
129
+ status: "failed",
130
+ output: output.result,
131
+ error: message,
132
+ });
133
+ throw new Error(message);
134
+ }
135
+
136
+ await recordScheduleScriptRun({ ...record, status: "completed", output: output.result });
137
+ }
138
+
139
+ /**
140
+ * Persist a scheduled script fire as a terminal `script_runs` row, so script
141
+ * audits see schedule-driven runs the same way as POST /api/scripts/run.
142
+ * Best effort: a persistence error never changes the schedule's outcome.
143
+ */
144
+ async function recordScheduleScriptRun(run: {
145
+ agentId: string;
146
+ scriptName: string;
147
+ source: string;
148
+ args: unknown;
149
+ startedAt: string;
150
+ status: "completed" | "failed";
151
+ output?: unknown;
152
+ error?: string;
153
+ }): Promise<void> {
154
+ try {
155
+ await recordInlineScriptRun({
156
+ id: crypto.randomUUID(),
157
+ agentId: run.agentId,
158
+ scriptName: run.scriptName,
159
+ source: run.source,
160
+ args: scrubObject(run.args),
161
+ status: run.status,
162
+ output: scrubObject(run.output),
163
+ error: run.error === undefined ? undefined : scrubSecrets(run.error),
164
+ startedAt: run.startedAt,
165
+ finishedAt: new Date().toISOString(),
166
+ });
167
+ } catch (err) {
168
+ console.warn(
169
+ `[scheduler] could not record script run for '${run.scriptName}': ${scrubSecrets(
170
+ err instanceof Error ? err.message : String(err),
171
+ )}`,
103
172
  );
104
173
  }
105
174
  }
@@ -8,7 +8,7 @@ const EXECUTORS: Record<string, () => ScriptExecutor> = {
8
8
  };
9
9
 
10
10
  export function getScriptExecutor(name = process.env.SCRIPT_EXECUTOR ?? "native"): ScriptExecutor {
11
- const factory = EXECUTORS[name];
11
+ const factory = EXECUTORS[name.trim()];
12
12
  if (!factory) {
13
13
  throw new Error(
14
14
  `Unknown SCRIPT_EXECUTOR: ${name}. Available: ${Object.keys(EXECUTORS).join(", ")}`,
package/src/server.ts CHANGED
@@ -196,6 +196,7 @@ import {
196
196
  registerTriggerWorkflowTool,
197
197
  registerUpdateWorkflowTool,
198
198
  } from "./tools/workflows";
199
+ import { parseEnabledTools } from "./utils/enabled-tools";
199
200
  import { resolveScriptsOnlyMode } from "./utils/scripts-only-mode";
200
201
 
201
202
  // Every known capability, including the ones disabled by default. Exported for
@@ -293,18 +294,75 @@ export function isScriptsOnlyMcp(): boolean {
293
294
  return resolveScriptsOnlyMode({ env: process.env.SCRIPTS_ONLY_MCP });
294
295
  }
295
296
 
297
+ const loggedToolAllowlistWarnings = new Set<string>();
298
+
299
+ function warnToolAllowlistOnce(message: string): void {
300
+ if (loggedToolAllowlistWarnings.has(message)) return;
301
+ loggedToolAllowlistWarnings.add(message);
302
+ console.warn(message);
303
+ }
304
+
305
+ /**
306
+ * SWARM_ENABLED_TOOLS: an optional explicit allowlist for the externally
307
+ * exposed MCP surface. Returns undefined (capability-driven surface) when the
308
+ * value is unset, blank, or invalid. Never logs the raw value.
309
+ */
310
+ function resolveToolAllowlist(value: string | undefined): string[] | undefined {
311
+ if (value === undefined) return undefined;
312
+ try {
313
+ const tools = parseEnabledTools(value);
314
+ if (!tools) {
315
+ warnToolAllowlistOnce("[MCP] SWARM_ENABLED_TOOLS is set but lists no tools; ignoring it");
316
+ }
317
+ return tools;
318
+ } catch {
319
+ warnToolAllowlistOnce("[MCP] Invalid SWARM_ENABLED_TOOLS; ignoring it");
320
+ return undefined;
321
+ }
322
+ }
323
+
324
+ /** Drop every registered tool the allowlist does not name; warn on unknown names. */
325
+ function applyToolAllowlist(server: McpServer, allowlist: readonly string[]): void {
326
+ const tools = (server as unknown as { _registeredTools: Record<string, { remove(): void }> })
327
+ ._registeredTools;
328
+ const allowed = new Set(allowlist);
329
+ for (const [name, tool] of Object.entries(tools)) {
330
+ if (!allowed.has(name)) tool.remove();
331
+ }
332
+ const unknown = allowlist.filter((name) => !Object.hasOwn(tools, name));
333
+ if (unknown.length > 0) {
334
+ warnToolAllowlistOnce(
335
+ `[MCP] SWARM_ENABLED_TOOLS names unknown tools, ignoring them: ${unknown.join(", ")}`,
336
+ );
337
+ }
338
+ }
339
+
296
340
  export async function createServer(
297
- opts: { scriptsOnly?: boolean; fullSurface?: boolean; preloadedTools?: readonly string[] } = {},
341
+ opts: {
342
+ scriptsOnly?: boolean;
343
+ fullSurface?: boolean;
344
+ preloadedTools?: readonly string[];
345
+ /** Raw SWARM_ENABLED_TOOLS value for this session; defaults to the env. */
346
+ enabledTools?: string;
347
+ } = {},
298
348
  ) {
299
349
  // Reload env
300
350
  await loadGlobalConfigsIntoEnv(true);
301
351
 
352
+ // The tool allowlist replaces CAPABILITIES and SCRIPTS_ONLY_MCP when set.
353
+ // Full-surface consumers (the scripts SDK bridge) ignore it.
354
+ const toolAllowlist =
355
+ opts.fullSurface === true
356
+ ? undefined
357
+ : resolveToolAllowlist(opts.enabledTools ?? process.env.SWARM_ENABLED_TOOLS);
358
+
302
359
  // Capability flags shape the externally exposed MCP tool list only. Internal
303
360
  // full-surface consumers (the scripts SDK bridge, drift-check tests) pass
304
361
  // fullSurface to register every tool group regardless of CAPABILITIES.
362
+ // An allowlist also registers every group, then prunes to the listed names.
305
363
  // This shadows the module-level hasCapability for the registrations below.
306
364
  const hasCapability = (cap: CAPABILITIES_T): boolean =>
307
- opts.fullSurface === true || getCapabilities().has(cap);
365
+ opts.fullSurface === true || toolAllowlist !== undefined || getCapabilities().has(cap);
308
366
 
309
367
  // Initialize database with WAL mode
310
368
  // Uses DATABASE_PATH env var for Docker volume compatibility (WAL needs .sqlite, .sqlite-wal, .sqlite-shm on same filesystem)
@@ -355,7 +413,7 @@ export async function createServer(
355
413
  // Scripts-only surface (experimental code-mode): register just the script
356
414
  // catalog tools and stop. script-connections / script-apis stay out — they
357
415
  // are lead-only security admin and excluded from the scripts SDK too.
358
- if (opts.scriptsOnly ?? isScriptsOnlyMcp()) {
416
+ if (toolAllowlist === undefined && (opts.scriptsOnly ?? isScriptsOnlyMcp())) {
359
417
  registerScriptSearchTool(server);
360
418
  registerScriptRunTool(server);
361
419
  registerScriptUpsertTool(server);
@@ -630,5 +688,7 @@ export async function createServer(
630
688
  registerUpdateServiceStatusTool(server);
631
689
  }
632
690
 
691
+ if (toolAllowlist) applyToolAllowlist(server, toolAllowlist);
692
+
633
693
  return server;
634
694
  }