@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.
- package/dist/acp-adapter-zhejfecx.js +34 -0
- package/dist/{actions-qz3m692w.js → actions-7jg840nr.js} +25 -25
- package/dist/{amp-adapter-j1xbmsqz.js → amp-adapter-ye6rnetm.js} +4 -4
- package/dist/{anthropic-messages-hst62ws9.js → anthropic-messages-13yn502s.js} +4 -4
- package/dist/{app-43vrdmv8.js → app-ffes2kyf.js} +10 -9
- package/dist/{assistant-ertffmka.js → assistant-fhm5smyz.js} +26 -26
- package/dist/{azure-openai-responses-mh8tvf78.js → azure-openai-responses-9st34pfz.js} +5 -5
- package/dist/{boot-reembed-998jtpdg.js → boot-reembed-20q3nzvg.js} +9 -8
- package/dist/{boot-reembed-s7k7c6v2.js → boot-reembed-jvfadhsk.js} +9 -8
- package/dist/{boot-scrub-sweep-nepsk4t1.js → boot-scrub-sweep-jtfajzve.js} +10 -9
- package/dist/{boot-seeds-vvzs1zhk.js → boot-seeds-9qt0j8j2.js} +12 -11
- package/dist/{claude-adapter-2zvmekrp.js → claude-adapter-a8znhthz.js} +24 -22
- package/dist/{claude-managed-adapter-s49wbk8t.js → claude-managed-adapter-tzwxy803.js} +4 -4
- package/dist/{claude-managed-setup-cvhqq8qn.js → claude-managed-setup-nrhfx5b0.js} +3 -3
- package/dist/{claude-sdk-session-kkbxtw5q.js → claude-sdk-session-7x7chdrs.js} +7608 -7596
- package/dist/{cli-zsh093hk.js → cli-014vns2y.js} +63 -1
- package/dist/{cli-8kgvmkak.js → cli-0jngabtb.js} +1 -1
- package/dist/{cli-qvfk83fy.js → cli-13xwcqvj.js} +2 -2
- package/dist/{cli-0nhdwxd5.js → cli-43jg9xdw.js} +3 -3
- package/dist/{cli-cpfmqz3n.js → cli-47zfzshm.js} +5 -5
- package/dist/{cli-zy7vwsne.js → cli-5qkcdv8c.js} +16 -9
- package/dist/{cli-cwxhbf43.js → cli-5t5my0h8.js} +85 -2
- package/dist/{cli-y59h2pk9.js → cli-69egj654.js} +1 -1
- package/dist/{cli-68r1j4sg.js → cli-6bpvv8h4.js} +3 -3
- package/dist/{cli-yshaazs1.js → cli-74ez9b66.js} +63 -20
- package/dist/{cli-32bca7nr.js → cli-7avy4v8w.js} +1 -1
- package/dist/{cli-f2n17zhp.js → cli-7mhy3ths.js} +21 -18
- package/dist/{cli-q38n4bq0.js → cli-95zs84qw.js} +3 -3
- package/dist/{cli-fn697bnt.js → cli-9q0bh411.js} +21 -8
- package/dist/{cli-rggdq8wq.js → cli-9q24yyvs.js} +3 -3
- package/dist/cli-a35hzdve.js +33 -0
- package/dist/{cli-3qn4r334.js → cli-aj1e0h0d.js} +20 -14
- package/dist/{cli-rsy933yz.js → cli-bcspch6h.js} +30 -19
- package/dist/{cli-x3x0qwky.js → cli-bxqez1p0.js} +51 -23
- package/dist/{cli-ng97fjq7.js → cli-cmxkbzzs.js} +33 -7
- package/dist/{cli-df56tqdw.js → cli-ee0402n1.js} +198 -70
- package/dist/{cli-mwegs4af.js → cli-ew0c3a1j.js} +3 -3
- package/dist/{cli-ysnyhws9.js → cli-ewhwpyrb.js} +54 -51
- package/dist/{cli-j2jbr8af.js → cli-fhj4y16f.js} +1 -1
- package/dist/{cli-8t229cmr.js → cli-fqq06kkb.js} +30 -17
- package/dist/{cli-jvj6a0cv.js → cli-g6gx2dn1.js} +54 -19
- package/dist/{cli-nexwh8zg.js → cli-g7k7zbcz.js} +12 -5
- package/dist/{cli-ecknp7p1.js → cli-j05x7pnb.js} +10 -10
- package/dist/{cli-gt8azyet.js → cli-jb931ba2.js} +1 -1
- package/dist/{cli-nvvfd8bq.js → cli-jbzbr36n.js} +4091 -3793
- package/dist/{cli-3ddybbbx.js → cli-jvmtxqtc.js} +26 -14
- package/dist/{cli-pkke6geq.js → cli-km4x66j0.js} +83 -65
- package/dist/{cli-b0f70z24.js → cli-mn8qkd8v.js} +1 -1
- package/dist/{cli-0qjcsxgz.js → cli-nctfv2yy.js} +99 -9
- package/dist/{cli-4r1sjfc4.js → cli-ng1pmdx6.js} +10 -10
- package/dist/cli-nkqjj42q.js +104 -0
- package/dist/{cli-aqcey741.js → cli-p5jqk8fr.js} +1 -1
- package/dist/{cli-66zh26sp.js → cli-pgehz8e1.js} +57 -17
- package/dist/cli-q1vnz8wv.js +96 -0
- package/dist/{cli-fj86xr3c.js → cli-qd97v6z2.js} +2 -2
- package/dist/{cli-p4ztb80h.js → cli-r7eeddkq.js} +13 -8
- package/dist/{cli-8ssjez7e.js → cli-rhy7z1rp.js} +22 -11
- package/dist/{cli-vzc73wet.js → cli-rr5z1j4p.js} +87 -69
- package/dist/{cli-4mevd1hf.js → cli-s9y81s7j.js} +5 -5
- package/dist/{cli-dw2q9f7g.js → cli-svah6b6w.js} +4 -4
- package/dist/{cli-h3x613ze.js → cli-tc5zec7v.js} +6 -6
- package/dist/{cli-ttbsse3f.js → cli-tmhsxhb8.js} +1 -1
- package/dist/{cli-feh3dkah.js → cli-v1w7pbd8.js} +2 -1
- package/dist/{cli-10zmp1p7.js → cli-vr5avem2.js} +27 -32
- package/dist/{cli-bzr0ftht.js → cli-wnb9b2g3.js} +19 -0
- package/dist/{cli-krqm5f11.js → cli-xezn9nkh.js} +16 -13
- package/dist/{cli-8kt0bmgx.js → cli-xf713zz0.js} +2 -2
- package/dist/{cli-6zcfw0x7.js → cli-y195hv1a.js} +1 -1
- package/dist/{cli-x7kpr72a.js → cli-ytmmyj7f.js} +2 -2
- package/dist/{cli-v2a8x45d.js → cli-z48ggfyq.js} +11 -6
- package/dist/{cli-qbksc292.js → cli-zy39d433.js} +2 -0
- package/dist/cli.js +28 -28
- package/dist/{cloudflare-workers-ai-system-one-tt484t61.js → cloudflare-workers-ai-system-one-sta45jf9.js} +7 -5
- package/dist/{codex-adapter-dw1wxqt8.js → codex-adapter-n63k33d3.js} +18 -18
- package/dist/{codex-login-terbdsqq.js → codex-login-g1cyt5zm.js} +1 -1
- package/dist/{codex-session-runner-3cgb8y1g.js → codex-session-runner-xkxfd81a.js} +18 -18
- package/dist/{commands-mhxam6z2.js → commands-q4mfhn51.js} +9 -8
- package/dist/{cursor-adapter-2qda3st7.js → cursor-adapter-v8mm6bp5.js} +5 -5
- package/dist/{db-sjks418k.js → db-1t0bj3pc.js} +9 -8
- package/dist/{dispatcher-1es759rn.js → dispatcher-k1fhavcf.js} +24 -24
- package/dist/{dsh-adapter-bek4cyw9.js → dsh-adapter-5423p70m.js} +5 -5
- package/dist/{easter-egg-3d-mtpnw06s.js → easter-egg-3d-66zxervv.js} +2 -2
- package/dist/{execute-s4ccjq3n.js → execute-h4jgevyw.js} +61 -11
- package/dist/{google-generative-ai-d6nphg3q.js → google-generative-ai-b8zxrpax.js} +5 -5
- package/dist/{google-vertex-cpshkref.js → google-vertex-nrdd697t.js} +5 -5
- package/dist/{grok-adapter-ar9cw9jg.js → grok-adapter-phrndeb8.js} +18 -8
- package/dist/{handlers-1gkbhktr.js → handlers-59ywqwzh.js} +27 -27
- package/dist/{hook-vxa3tcsy.js → hook-atj0swc2.js} +14 -14
- package/dist/{hook-ds2zxq9v.js → hook-mew1chq9.js} +42 -31
- package/dist/{http-3h21z9md.js → http-ef2fnn9v.js} +953 -98
- package/dist/{index-n99fycq7.js → index-48pb9erw.js} +24 -24
- package/dist/{index-bn6cy8ps.js → index-9fc2h7ww.js} +24 -24
- package/dist/{index-9b65j1m5.js → index-gr69c43q.js} +9 -8
- package/dist/{index-f8xnqrye.js → index-h26wjcfy.js} +43 -24
- package/dist/{index-dkfxpkmm.js → index-w2yhye23.js} +25 -25
- package/dist/{keepalive-3xspa9jw.js → keepalive-qhcqz56v.js} +11 -10
- package/dist/{lead-6qphc124.js → lead-hpk5r3b1.js} +47 -47
- package/dist/{llama-cpp-classify-tgj35k8d.js → llama-cpp-classify-6erb7wwv.js} +3 -1
- package/dist/{maintenance-0teem1bb.js → maintenance-nvkyp2xe.js} +9 -8
- package/dist/{mistral-conversations-8445ddjn.js → mistral-conversations-059y51zt.js} +4 -4
- package/dist/{node-9sg7ajby.js → node-qm9eb6z0.js} +1 -1
- package/dist/{oauth-refresh-sweep-sh9q25xp.js → oauth-refresh-sweep-ngskthwh.js} +10 -9
- package/dist/{onboard-54qmnqxe.js → onboard-yfnkqhxc.js} +2 -2
- package/dist/{openai-codex-responses-9ynm88zy.js → openai-codex-responses-jzxkta9b.js} +8 -7
- package/dist/{openai-completions-nhqwjmaw.js → openai-completions-663yncfq.js} +4 -4
- package/dist/openai-decisions-vj9nkpf8.js +162 -0
- package/dist/{openai-responses-xx1c4kgm.js → openai-responses-m3g43rrt.js} +5 -5
- package/dist/{opencode-adapter-wjfa2wvd.js → opencode-adapter-yjyqynzf.js} +7 -7
- package/dist/{openrouter-images-8yx3487g.js → openrouter-images-kr60n6b9.js} +1 -1
- package/dist/{otel-impl-ycqzp2a1.js → otel-impl-hhm64r6m.js} +1 -1
- package/dist/{pi-messages-vxdt8v7j.js → pi-messages-xszk613n.js} +1 -1
- package/dist/{pi-mono-adapter-e77e9zst.js → pi-mono-adapter-0eg8kw2c.js} +68 -51
- package/dist/{pricing-refresh-atnkngqs.js → pricing-refresh-35f91nx2.js} +11 -10
- package/dist/{rbac-roles-j78wdksm.js → rbac-roles-97w370qj.js} +10 -9
- package/dist/{rbac-roles-9pjphh9q.js → rbac-roles-z6sje3gy.js} +9 -8
- package/dist/{render-v2-pxjs49fx.js → render-v2-n4v66bj3.js} +10 -9
- package/dist/{runtime-v2t1r79e.js → runtime-h9ha1mq6.js} +4 -4
- package/dist/{setup-ymej0xtx.js → setup-m3hhyv5f.js} +2 -2
- package/dist/{typesafe-system-one-datfr82p.js → typesafe-system-one-e988vty7.js} +7 -5
- package/dist/{virtual-modules-wm8mxdxx.js → virtual-modules-ftr9kwe8.js} +11 -11
- package/dist/{work-objects-3nh4ymzb.js → work-objects-mxf2c842.js} +27 -27
- package/dist/{worker-qa9fkz5s.js → worker-9t07z1gq.js} +47 -47
- package/openapi.json +72 -1
- package/package.json +10 -10
- package/src/be/memory/providers/openai-embedding.ts +7 -2
- package/src/be/memory/raters/llm-summarizer.ts +7 -2
- package/src/be/modelsdev-cache.json +61 -0
- package/src/be/script-connections.ts +40 -6
- package/src/be/seed-pricing.ts +84 -53
- package/src/be/swarm-config-guard.ts +14 -0
- package/src/commands/provider-credentials.ts +28 -5
- package/src/commands/runner.ts +7 -12
- package/src/extensions/catalog.generated.json +17 -0
- package/src/heartbeat/heartbeat.ts +91 -19
- package/src/hooks/hook.ts +72 -27
- package/src/http/mcp.ts +5 -0
- package/src/http/onboarding.ts +2 -0
- package/src/http/script-connections.ts +35 -0
- package/src/providers/acp-targets.ts +10 -5
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/claude-adapter.ts +32 -0
- package/src/providers/claude-background-keepalive.ts +100 -0
- package/src/providers/claude-sdk-session.ts +6 -0
- package/src/providers/codex-adapter.ts +30 -3
- package/src/providers/codex-oauth/auth-json.ts +21 -4
- package/src/providers/codex-oauth/types.ts +2 -0
- package/src/providers/dsh-adapter.ts +11 -2
- package/src/providers/grok-adapter.ts +18 -2
- package/src/providers/opencode-adapter.ts +21 -5
- package/src/providers/pi-mono-extension.ts +56 -25
- package/src/providers/pricing-sources.md +2 -0
- package/src/scheduler/scheduler.ts +82 -13
- package/src/scripts-runtime/executors/registry.ts +1 -1
- package/src/server.ts +63 -3
- package/src/tests/acp-adapter.test.ts +152 -12
- package/src/tests/ci-unit-test-report.test.ts +73 -0
- package/src/tests/claude-background-keepalive-wiring.test.ts +220 -0
- package/src/tests/claude-background-keepalive.fixture.ts +162 -0
- package/src/tests/claude-background-keepalive.test.ts +139 -0
- package/src/tests/claude-default-route.test.ts +75 -0
- package/src/tests/claude-hook-output.test.ts +233 -0
- package/src/tests/codex-adapter-pool-auth-revalidate.test.ts +4 -2
- package/src/tests/codex-adapter.test.ts +166 -26
- package/src/tests/codex-oauth-auth-timestamps.test.ts +76 -0
- package/src/tests/codex-oauth.test.ts +4 -1
- package/src/tests/connection-embedded-auth.test.ts +51 -0
- package/src/tests/context-window.test.ts +2 -2
- package/src/tests/credential-check.test.ts +51 -1
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/dsh-adapter.test.ts +24 -2
- package/src/tests/enabled-tools.test.ts +269 -0
- package/src/tests/entrypoint-codex-oauth-seed.test.ts +12 -0
- package/src/tests/extensions-catalog.test.ts +1 -0
- package/src/tests/extensions-example-slack-reply-gate.test.ts +756 -0
- package/src/tests/grok-adapter.test.ts +13 -0
- package/src/tests/haiku-5-5-pricing.test.ts +38 -0
- package/src/tests/heartbeat-checklist.test.ts +158 -1
- package/src/tests/onboarding.test.ts +44 -0
- package/src/tests/opencode-adapter.test.ts +66 -3
- package/src/tests/opencode-plugin.test.ts +37 -0
- package/src/tests/openrouter-attribution-call-sites.test.ts +204 -0
- package/src/tests/openrouter-base-url.test.ts +30 -39
- package/src/tests/pi-mono-extension.test.ts +161 -2
- package/src/tests/pricing-refresh.test.ts +70 -1
- package/src/tests/reasoning-effort.test.ts +3 -1
- package/src/tests/schedule-target-type.test.ts +136 -0
- package/src/tests/script-connections-http.test.ts +45 -0
- package/src/tests/script-connections-mcp.test.ts +41 -0
- package/src/tests/script-executor-registry.test.ts +18 -0
- package/src/tests/send-task-self-send.test.ts +107 -0
- package/src/tests/telemetry-trigger.test.ts +12 -1
- package/src/tests/workflow-llm.test.ts +14 -0
- package/src/tests/workflow-system-one-decision-providers.test.ts +33 -0
- package/src/tools/request-human-input.ts +7 -2
- package/src/tools/script-connections/tool.ts +35 -3
- package/src/tools/send-task.ts +9 -1
- package/src/utils/enabled-tools.ts +27 -0
- package/src/utils/internal-ai/complete-structured.ts +11 -3
- package/src/utils/openrouter-base-url.ts +23 -0
- package/src/workflows/executors/raw-llm.ts +1 -0
- package/src/workflows/executors/system-one-decision.ts +2 -0
- package/src/workflows/executors/validate.ts +1 -0
- package/src/workflows/executors/workflow-llm.ts +9 -2
- package/templates/extensions/README.md +1 -0
- package/templates/extensions/slack-reply-gate/README.md +98 -0
- package/templates/extensions/slack-reply-gate/hooks.ts +582 -0
- package/templates/extensions/slack-reply-gate/manifest.json +8 -0
- package/templates/skills/agent-browser/SKILL.md +2 -0
- package/templates/skills/agent-browser/content.md +2 -0
- package/templates/skills/code-quality/content.md +1 -0
- package/templates/skills/heartbeat-runbook/content.md +1 -1
- package/templates/skills/swarm-scripts/SKILL.md +10 -0
- package/templates/skills/swarm-scripts/content.md +10 -0
- package/templates/skills/work-on-task/SKILL.md +4 -0
- package/templates/skills/work-on-task/content.md +4 -0
- package/templates/skills/workflow-iterate/content.md +9 -0
- package/dist/acp-adapter-yzfxn8vj.js +0 -23
- package/dist/cli-3dpj7atn.js +0 -11
- package/dist/cli-gvcgmx69.js +0 -183
- package/dist/{cli-jhm6hzjn.js → cli-mxvnnv5q.js} +7 -7
- 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
|
|
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(
|
|
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
|
-
|
|
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
|
}
|
|
@@ -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 {
|
|
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.
|
|
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 {
|
|
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
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
// ===
|
|
598
|
-
//
|
|
599
|
-
//
|
|
600
|
-
pi.on("
|
|
601
|
-
if (!config.taskId) return
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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
|
-
|
|
618
|
-
|
|
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
|
-
|
|
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
|
|
87
|
-
|
|
88
|
-
args: schedule.scriptArgs ?? {},
|
|
89
|
-
fsMode: "none",
|
|
88
|
+
const args = schedule.scriptArgs ?? {};
|
|
89
|
+
const record = {
|
|
90
90
|
agentId,
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
122
|
+
const message =
|
|
99
123
|
output.stderr ||
|
|
100
|
-
|
|
101
|
-
|
|
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: {
|
|
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
|
}
|