@desplega.ai/agent-swarm 1.150.0 → 1.152.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/README.md +11 -0
- package/dist/{acp-adapter-d5mzggyk.js → acp-adapter-d05sg0q4.js} +5 -5
- package/dist/{actions-wn6ew0p7.js → actions-4sc0v342.js} +38 -11
- package/dist/{anthropic-messages-ec35ktvx.js → anthropic-messages-2y8nktar.js} +16 -16
- package/dist/{app-dty7p9hg.js → app-2d0v6kt6.js} +7 -7
- package/dist/{assistant-q4vhgjtx.js → assistant-zs6wdnrf.js} +41 -16
- package/dist/{azure-openai-responses-g85jv7m8.js → azure-openai-responses-f38x2avq.js} +13 -13
- package/dist/{boot-reembed-n15q6s9x.js → boot-reembed-3dwdnnsr.js} +13 -15
- package/dist/{boot-reembed-92292pgf.js → boot-reembed-vhh33520.js} +8 -8
- package/dist/{boot-scrub-logs-44pc955m.js → boot-scrub-logs-e1fnabcg.js} +5 -5
- package/dist/{claude-adapter-cjegjbje.js → claude-adapter-h58xqzzd.js} +12 -13
- package/dist/{claude-managed-adapter-p3ts5m9m.js → claude-managed-adapter-dsmkcrjy.js} +3 -3
- package/dist/{claude-sdk-session-7ywbn12v.js → claude-sdk-session-pbaczbgt.js} +12 -13
- package/dist/{cli-9gqddmxs.js → cli-4fqn2vpg.js} +8 -8
- package/dist/{cli-mzb0n642.js → cli-4wppfkcm.js} +350 -20
- package/dist/{cli-h505yhex.js → cli-5bqt4gwa.js} +2 -2
- package/dist/{cli-hbagregr.js → cli-5p007m2q.js} +88 -13
- package/dist/{cli-3ckzxdt1.js → cli-6k2athpf.js} +94 -81
- package/dist/{cli-3mh119qv.js → cli-72vksk1n.js} +9855 -693
- package/dist/{cli-taym3dmg.js → cli-87hdfyde.js} +13 -13
- package/dist/{cli-zthatyrx.js → cli-8fy9dmpv.js} +5 -5
- package/dist/{cli-asvrh7kj.js → cli-9xvdm3bx.js} +1 -1
- package/dist/{cli-72xdszyx.js → cli-awb59f08.js} +2 -2
- package/dist/{cli-vr6qj104.js → cli-cs9z1hw3.js} +1 -1
- package/dist/{cli-kt7wevqh.js → cli-cwvey7n1.js} +164 -56
- package/dist/{cli-5my3bjsd.js → cli-d3p82erb.js} +7 -7
- package/dist/{cli-7kwmt3r5.js → cli-evgy5w3y.js} +9 -3
- package/dist/{cli-111ey3ra.js → cli-f1f9n77e.js} +5 -5
- package/dist/{cli-ttje5a4z.js → cli-fhdssm2v.js} +105 -56
- package/dist/{cli-a45d5v9b.js → cli-fm4ytsdk.js} +2 -2
- package/dist/{cli-qp69dd5d.js → cli-gacq5hg8.js} +116 -14
- package/dist/{cli-hmyynq9b.js → cli-ht5rs63n.js} +1 -1
- package/dist/{cli-dyx0237s.js → cli-j4nv2wqy.js} +2 -4
- package/dist/{cli-gm0fhz13.js → cli-jh9txj4n.js} +2 -1
- package/dist/{cli-8chp8xab.js → cli-nd5s3m4y.js} +2 -2
- package/dist/{cli-zd21jrrc.js → cli-p2hx1pxm.js} +9872 -424
- package/dist/cli-pf9q3696.js +172 -0
- package/dist/{cli-7fesa1ky.js → cli-pgj4a5fb.js} +1 -1
- package/dist/{cli-07jd68a1.js → cli-re8z5p8p.js} +1 -1
- package/dist/{cli-ryz8q8p1.js → cli-t1mykkng.js} +40 -40
- package/dist/{cli-ck1vqzdj.js → cli-t6axtdcg.js} +9 -4
- package/dist/{cli-1mx65de9.js → cli-wbyk465z.js} +1 -1
- package/dist/{cli-kyysm7m3.js → cli-wp7cejrg.js} +1 -1
- package/dist/{cli-n75jt793.js → cli-z61ykvam.js} +6 -5
- package/dist/{cli-ceb5y160.js → cli-zdcdh02m.js} +1 -1
- package/dist/{cli-cmp8g3sw.js → cli-zfzznn7c.js} +45 -9
- package/dist/cli.js +22 -21
- package/dist/{codex-adapter-rn3aq7jm.js → codex-adapter-v3cz462q.js} +8 -9
- package/dist/{codex-hook-gthcmm0g.js → codex-hook-8jexc34x.js} +2 -2
- package/dist/{codex-session-runner-bge0cbsw.js → codex-session-runner-de1a5g5z.js} +8 -9
- package/dist/{commands-26phw0pc.js → commands-wfqqpn95.js} +5 -5
- package/dist/{db-jxjx6s1b.js → db-9zzv5y05.js} +5 -5
- package/dist/dispatcher-hfm1jbgx.js +74 -0
- package/dist/{google-generative-ai-tsskx7te.js → google-generative-ai-ndjmj5fa.js} +7 -7
- package/dist/{google-vertex-pkhpkbq9.js → google-vertex-fr665tqs.js} +10 -10
- package/dist/handlers-wrrhsack.js +65 -0
- package/dist/{hook-jw0ztn96.js → hook-9skye8zd.js} +9 -10
- package/dist/{hook-t6fqsggw.js → hook-fgjv62k5.js} +9 -10
- package/dist/{http-hyc7s57s.js → http-7grttjfq.js} +1434 -579
- package/dist/{index-bcyx5djw.js → index-29gxx9ar.js} +25 -17
- package/dist/{index-wtwpthe1.js → index-30pz2h9w.js} +25 -19
- package/dist/{index-1d9r6a74.js → index-4ek1df87.js} +26 -20
- package/dist/{index-sfhaq59d.js → index-myfwqqyq.js} +120 -102
- package/dist/index-pfhzrh1d.js +25 -0
- package/dist/{keepalive-fbsak4ya.js → keepalive-j75m2m7k.js} +7 -7
- package/dist/{lead-rdzdm627.js → lead-a1hpjt0a.js} +41 -49
- package/dist/{maintenance-25z1c1vf.js → maintenance-tngjzqdq.js} +8 -8
- package/dist/{node-209sxmg3.js → node-x1g4v0pq.js} +9 -9
- package/dist/{oauth-refresh-sweep-j3d7gtse.js → oauth-refresh-sweep-em1fn044.js} +6 -6
- package/dist/{onboard-dkcwrc75.js → onboard-fv7ezvwx.js} +13 -4
- package/dist/{openai-codex-responses-pm5gnyfa.js → openai-codex-responses-wy6a43wd.js} +11 -11
- package/dist/{openai-responses-vwvk1taj.js → openai-responses-6xwf7bzg.js} +13 -13
- package/dist/{opencode-adapter-dce4wskd.js → opencode-adapter-28axqa09.js} +4 -4
- package/dist/{otel-impl-8rgyy3j8.js → otel-impl-6vbb568y.js} +1 -1
- package/dist/{pi-mono-adapter-4s2bbkgq.js → pi-mono-adapter-jcqt71cg.js} +14 -16
- package/dist/{pricing-refresh-9cek71nq.js → pricing-refresh-nmm949an.js} +7 -7
- package/dist/{rbac-roles-vk8mqp9v.js → rbac-roles-0fz5qryx.js} +7 -7
- package/dist/{rbac-roles-z98s2ypy.js → rbac-roles-xtp05syf.js} +13 -9
- package/dist/{render-v2-3j26g0qh.js → render-v2-fpb8whme.js} +7 -7
- package/dist/{seed-pricing-7rxhcnzm.js → seed-pricing-zwa8bs3t.js} +6 -6
- package/dist/{setup-e2t7vz0c.js → setup-3ybwbdpd.js} +2 -2
- package/dist/{work-objects-8d3nd3x2.js → work-objects-rmayxcx7.js} +41 -16
- package/dist/{worker-0x9fn7nq.js → worker-rng4jhnh.js} +41 -49
- package/openapi.json +1351 -265
- package/package.json +2 -1
- package/src/agentmail/handlers.ts +95 -71
- package/src/be/db-client.ts +5 -0
- package/src/be/db.ts +12 -4
- package/src/be/extensions/db.ts +461 -0
- package/src/be/extensions/validate.ts +107 -0
- package/src/be/memory/boot-reembed.ts +17 -12
- package/src/be/memory/index.ts +12 -0
- package/src/be/memory/providers/openai-embedding.ts +4 -0
- package/src/be/memory/types.ts +2 -0
- package/src/be/migrations/155_extensions.sql +66 -0
- package/src/be/migrations/156_inbox_item_state_read_at.sql +7 -0
- package/src/be/rbac-roles.ts +3 -2
- package/src/be/scripts/typecheck.ts +56 -38
- package/src/be/seed-skills/index.ts +7 -0
- package/src/be/swarm-config-guard.ts +10 -2
- package/src/commands/onboard/compose-generator.ts +14 -2
- package/src/commands/onboard/env-generator.ts +7 -0
- package/src/commands/runner.ts +1 -1
- package/src/extensions/apply-task-create.ts +135 -0
- package/src/extensions/bundle-path.ts +10 -0
- package/src/extensions/contract-runtime.ts +7 -0
- package/src/extensions/contract-types.generated.ts +4 -0
- package/src/extensions/contract.ts +270 -0
- package/src/extensions/ctx.ts +186 -0
- package/src/extensions/dispatcher.ts +479 -0
- package/src/extensions/identity.ts +44 -0
- package/src/extensions/lifecycle.ts +303 -0
- package/src/extensions/loader.ts +207 -0
- package/src/extensions/post-bridge.ts +161 -0
- package/src/github/handlers.ts +185 -149
- package/src/gitlab/handlers.ts +97 -81
- package/src/heartbeat/heartbeat.ts +152 -83
- package/src/http/agents.ts +1 -1
- package/src/http/all-routes.ts +1 -0
- package/src/http/apps.ts +14 -10
- package/src/http/core.ts +34 -0
- package/src/http/extensions.ts +552 -0
- package/src/http/inbox-state.ts +2 -0
- package/src/http/index.ts +25 -0
- package/src/http/mcp-bridge.ts +96 -62
- package/src/http/mcp.ts +1 -1
- package/src/http/status.ts +6 -4
- package/src/http/tasks.ts +51 -35
- package/src/integrations/kapso/inbound.ts +15 -11
- package/src/jira/sync.ts +26 -18
- package/src/linear/sync.ts +26 -18
- package/src/prompts/session-templates.ts +5 -4
- package/src/rbac/admission.ts +1 -1
- package/src/rbac/elevated-agents.ts +24 -0
- package/src/rbac/legacy-policy.ts +47 -6
- package/src/rbac/permissions.ts +8 -0
- package/src/scheduler/deferred-task-waits.ts +133 -51
- package/src/scheduler/schedule-task.ts +50 -24
- package/src/scheduler/scheduler.ts +4 -2
- package/src/scripts-runtime/executors/native.ts +5 -1
- package/src/scripts-runtime/executors/types.ts +5 -0
- package/src/scripts-runtime/import-allowlist.ts +79 -19
- package/src/scripts-runtime/sdk-allowlist.ts +8 -0
- package/src/scripts-runtime/swarm-config.ts +8 -0
- package/src/scripts-runtime/swarm-sdk.ts +3 -0
- package/src/scripts-runtime/types/stdlib.d.ts +13 -0
- package/src/scripts-runtime/types/swarm-extension.d.ts +562 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +13 -0
- package/src/server.ts +18 -0
- package/src/slack/HEURISTICS.md +1 -1
- package/src/slack/actions.ts +21 -17
- package/src/slack/blocks.ts +1 -1
- package/src/slack/handlers.ts +98 -5
- package/src/slack/inbound-files.ts +6 -2
- package/src/slack/render-v2.ts +181 -39
- package/src/slack/responses.ts +7 -4
- package/src/slack/router.ts +13 -3
- package/src/slack/steering.ts +3 -3
- package/src/slack/task-output.ts +21 -0
- package/src/slack/watcher.ts +3 -2
- package/src/slack/work-objects.ts +2 -1
- package/src/tasks/errors.ts +9 -0
- package/src/tasks/sibling-awareness.ts +35 -1
- package/src/tasks/worker-follow-up.ts +64 -12
- package/src/tests/base-prompt.test.ts +1 -0
- package/src/tests/deferred-task-wake.test.ts +161 -9
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/env-flag.test.ts +1 -0
- package/src/tests/extensions-db.test.ts +216 -0
- package/src/tests/extensions-dispatcher.test.ts +275 -0
- package/src/tests/extensions-example-heartbeat-remediate.test.ts +69 -0
- package/src/tests/extensions-example-mcp-install.test.ts +63 -0
- package/src/tests/extensions-example-no-exclamation-marks.test.ts +72 -0
- package/src/tests/extensions-example-rewrite-and-suppress.test.ts +70 -0
- package/src/tests/extensions-example-route-slack-channel.test.ts +217 -0
- package/src/tests/extensions-heartbeat.test.ts +291 -0
- package/src/tests/extensions-http.test.ts +512 -0
- package/src/tests/extensions-lead-equivalence.test.ts +70 -0
- package/src/tests/extensions-lifecycle.test.ts +209 -0
- package/src/tests/extensions-loader.test.ts +95 -0
- package/src/tests/extensions-mcp-tools.test.ts +353 -0
- package/src/tests/extensions-pre-task.test.ts +487 -0
- package/src/tests/extensions-run-context.test.ts +41 -0
- package/src/tests/extensions-tool-call.test.ts +288 -0
- package/src/tests/extensions-validate.test.ts +196 -0
- package/src/tests/fixtures/extensions/bad-import/hooks.ts +8 -0
- package/src/tests/fixtures/extensions/bad-import/manifest.json +9 -0
- package/src/tests/fixtures/extensions/bad-return-shape/hooks.ts +10 -0
- package/src/tests/fixtures/extensions/bad-return-shape/manifest.json +9 -0
- package/src/tests/fixtures/extensions/block-tasks-from-source/hooks.ts +22 -0
- package/src/tests/fixtures/extensions/block-tasks-from-source/manifest.json +9 -0
- package/src/tests/fixtures/extensions/ignore-channel/hooks.ts +22 -0
- package/src/tests/fixtures/extensions/ignore-channel/manifest.json +9 -0
- package/src/tests/fixtures/extensions/load.ts +10 -0
- package/src/tests/fixtures/extensions/minimal/hooks.ts +7 -0
- package/src/tests/fixtures/extensions/minimal/manifest.json +9 -0
- package/src/tests/fixtures/extensions/never-fail-long-tasks/hooks.ts +11 -0
- package/src/tests/fixtures/extensions/never-fail-long-tasks/manifest.json +9 -0
- package/src/tests/fixtures/extensions/no-exclamation-marks/hooks.ts +23 -0
- package/src/tests/fixtures/extensions/no-exclamation-marks/manifest.json +9 -0
- package/src/tests/fixtures/extensions/post-logger/hooks.ts +9 -0
- package/src/tests/fixtures/extensions/post-logger/manifest.json +9 -0
- package/src/tests/fixtures/extensions/priority-a/hooks.ts +7 -0
- package/src/tests/fixtures/extensions/priority-a/manifest.json +9 -0
- package/src/tests/fixtures/extensions/priority-b/hooks.ts +7 -0
- package/src/tests/fixtures/extensions/priority-b/manifest.json +9 -0
- package/src/tests/fixtures/extensions/record-only-on-tag/hooks.ts +16 -0
- package/src/tests/fixtures/extensions/record-only-on-tag/manifest.json +9 -0
- package/src/tests/fixtures/extensions/reserved-assets/hooks.ts +5 -0
- package/src/tests/fixtures/extensions/reserved-assets/manifest.json +10 -0
- package/src/tests/fixtures/extensions/rewrite-progress-prefix/hooks.ts +22 -0
- package/src/tests/fixtures/extensions/rewrite-progress-prefix/manifest.json +9 -0
- package/src/tests/fixtures/extensions/rewrite-task-priority/hooks.ts +7 -0
- package/src/tests/fixtures/extensions/rewrite-task-priority/manifest.json +9 -0
- package/src/tests/fixtures/extensions/route-channel-to-agent/hooks.ts +26 -0
- package/src/tests/fixtures/extensions/route-channel-to-agent/manifest.json +9 -0
- package/src/tests/fixtures/extensions/slow/hooks.ts +10 -0
- package/src/tests/fixtures/extensions/slow/manifest.json +9 -0
- package/src/tests/fixtures/extensions/suppress-lead-follow-up/hooks.ts +12 -0
- package/src/tests/fixtures/extensions/suppress-lead-follow-up/manifest.json +9 -0
- package/src/tests/fixtures/extensions/throws/hooks.ts +12 -0
- package/src/tests/fixtures/extensions/throws/manifest.json +9 -0
- package/src/tests/fixtures/extensions/worker-runtime/hooks.ts +5 -0
- package/src/tests/fixtures/extensions/worker-runtime/manifest.json +9 -0
- package/src/tests/fixtures/store-progress-malformed-output.json +4 -0
- package/src/tests/memory-edit-authorization.test.ts +32 -0
- package/src/tests/multi-runtime-enabled-stats.test.ts +2 -2
- package/src/tests/multi-runtime-registration.test.ts +14 -14
- package/src/tests/onboard-compose.test.ts +44 -0
- package/src/tests/preload.ts +5 -0
- package/src/tests/rbac-admission-e2e.test.ts +35 -4
- package/src/tests/rbac-admission.test.ts +18 -3
- package/src/tests/rbac-e2e-helpers.ts +18 -0
- package/src/tests/rbac-engine.test.ts +39 -0
- package/src/tests/rbac-mcp-admission-e2e.test.ts +22 -4
- package/src/tests/realtime-room-auth.test.ts +1 -1
- package/src/tests/reload-config.test.ts +61 -0
- package/src/tests/routing-affinity-inventory.test.ts +1 -1
- package/src/tests/script-connections.test.ts +2 -0
- package/src/tests/scripts-boot-reembed.test.ts +4 -0
- package/src/tests/scripts-embeddings.test.ts +4 -0
- package/src/tests/scripts-import-allowlist.test.ts +44 -1
- package/src/tests/scripts-mcp-e2e.test.ts +80 -0
- package/src/tests/scripts-only-gating.test.ts +6 -0
- package/src/tests/scripts-runtime-identity.test.ts +1 -1
- package/src/tests/slack-blocks.test.ts +12 -1
- package/src/tests/slack-render-v2.test.ts +100 -5
- package/src/tests/slack-router.test.ts +17 -2
- package/src/tests/slack-steering.test.ts +19 -0
- package/src/tests/slack-task-output.test.ts +36 -0
- package/src/tests/status.test.ts +12 -0
- package/src/tests/steering-enabled.test.ts +6 -8
- package/src/tests/store-progress-attachments-handler.test.ts +100 -1
- package/src/tests/swarm-config-reserved-keys.test.ts +6 -1
- package/src/tests/task-tool-preload-mcp.test.ts +4 -1
- package/src/tests/tool-annotations.test.ts +1 -1
- package/src/tests/tool-registrar-no-input.test.ts +42 -1
- package/src/tools/defer-task.ts +1 -1
- package/src/tools/extension-activate-version.ts +38 -0
- package/src/tools/extension-common.ts +85 -0
- package/src/tools/extension-delete.ts +29 -0
- package/src/tools/extension-disable.ts +36 -0
- package/src/tools/extension-enable.ts +36 -0
- package/src/tools/extension-install.ts +65 -0
- package/src/tools/extension-list.ts +66 -0
- package/src/tools/script-common.ts +43 -18
- package/src/tools/send-task.ts +69 -107
- package/src/tools/store-progress.ts +30 -0
- package/src/tools/task-action.ts +43 -22
- package/src/tools/task-tool-ctx.ts +2 -0
- package/src/tools/tool-config.ts +7 -1
- package/src/tools/utils.ts +140 -16
- package/src/types.ts +133 -0
- package/src/utils/multi-runtime.ts +2 -2
- package/src/utils/steering-enabled.ts +4 -4
- package/src/workflows/executors/agent-task.ts +13 -3
- package/templates/extensions/README.md +17 -0
- package/templates/extensions/notify-on-complete/README.md +11 -0
- package/templates/extensions/notify-on-complete/hooks.ts +61 -0
- package/templates/extensions/notify-on-complete/manifest.json +7 -0
- package/templates/extensions/require-ticket-ref/README.md +11 -0
- package/templates/extensions/require-ticket-ref/hooks.ts +30 -0
- package/templates/extensions/require-ticket-ref/manifest.json +7 -0
- package/templates/extensions/require-verification-note/README.md +11 -0
- package/templates/extensions/require-verification-note/hooks.ts +32 -0
- package/templates/extensions/require-verification-note/manifest.json +7 -0
- package/templates/skills/agent-browser/SKILL.md +84 -0
- package/templates/skills/agent-browser/content.md +21 -0
- package/templates/skills/scheduling/content.md +2 -0
- package/templates/skills/swarm-extensions/config.json +14 -0
- package/templates/skills/swarm-extensions/content.md +120 -0
- package/dist/cli-b8x19vbh.js +0 -823
- package/dist/cli-n1yj5fd5.js +0 -164
- package/dist/cli-q04q68be.js +0 -3066
- package/dist/cli-qgm2pfpq.js +0 -6377
- package/dist/cli-rnsr8asm.js +0 -4323
- package/dist/cli-rwp3ksm7.js +0 -545
- package/dist/cli-xjb9wghj.js +0 -668
- package/dist/cli-xvkmedgn.js +0 -332
- package/dist/cli-ydk0wjj8.js +0 -338
- package/dist/handlers-vr1djg4n.js +0 -41
- package/dist/{claude-managed-setup-f5pmsyrp.js → claude-managed-setup-48dwg9bv.js} +3 -3
- package/dist/{cli-81r683bv.js → cli-ch38q5z4.js} +0 -0
- package/dist/{cli-wk0gjrn3.js → cli-kj76p1ez.js} +6 -6
- package/dist/{cli-c71mg51n.js → cli-r2fpe7cx.js} +5 -5
- package/dist/{cli-8qx7jnrk.js → cli-y6x0gqtg.js} +16 -16
- package/dist/{index-aa3sa6s8.js → index-2wsdsnmm.js} +1 -1
- package/dist/{mistral-conversations-4ryk7h83.js → mistral-conversations-qhr84ph3.js} +9 -9
- package/dist/{openai-completions-qdcmkfnr.js → openai-completions-32rdwmze.js} +17 -17
- package/dist/{openrouter-images-qx18fyp8.js → openrouter-images-9f9ek4rw.js} +3 -3
- package/dist/{pi-messages-79jvzz1d.js → pi-messages-b1mbr6gp.js} +4 -4
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { slackTaskOutput } from "../slack/task-output";
|
|
3
|
+
|
|
4
|
+
const task = {
|
|
5
|
+
status: "completed" as const,
|
|
6
|
+
tags: ["deferred"],
|
|
7
|
+
output:
|
|
8
|
+
"Deferred until tomorrow 17:30 UTC ([12345678](https://example.com/schedules/12345678)) -> check the build",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
describe("Slack deferral output", () => {
|
|
12
|
+
test("keeps the pending work without the ETA or schedule link", () => {
|
|
13
|
+
expect(slackTaskOutput(task)).toBe("Pending: check the build");
|
|
14
|
+
expect(task.output).toContain("tomorrow 17:30 UTC");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("removes an ETA-only line and preserves punctuation in the pending description", () => {
|
|
18
|
+
expect(slackTaskOutput({ ...task, output: task.output.replace("check the build", "") })).toBe(
|
|
19
|
+
"",
|
|
20
|
+
);
|
|
21
|
+
expect(slackTaskOutput({ ...task, output: `${task.output} (CI) -> deploy` })).toBe(
|
|
22
|
+
"Pending: check the build (CI) -> deploy",
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("preserves final continuation results even with inherited deferred tags", () => {
|
|
27
|
+
expect(slackTaskOutput({ ...task, output: "Build passed." })).toBe("Build passed.");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("preserves schema output and non-deferral output verbatim", () => {
|
|
31
|
+
expect(slackTaskOutput({ ...task, outputSchema: { type: "string" } })).toBe(task.output);
|
|
32
|
+
expect(slackTaskOutput({ ...task, tags: [] })).toBe(task.output);
|
|
33
|
+
expect(slackTaskOutput({ ...task, status: "in_progress" })).toBe(task.output);
|
|
34
|
+
expect(slackTaskOutput({ ...task, output: undefined })).toBeUndefined();
|
|
35
|
+
});
|
|
36
|
+
});
|
package/src/tests/status.test.ts
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
updateAgentCredStatus,
|
|
32
32
|
} from "../be/db";
|
|
33
33
|
import { storeOAuthTokens, upsertOAuthApp } from "../be/db-queries/oauth";
|
|
34
|
+
import { resetEmbeddingProvider } from "../be/memory";
|
|
34
35
|
import { validateProviderCredentials } from "../commands/provider-credentials";
|
|
35
36
|
import {
|
|
36
37
|
_resetTestConnectionCache,
|
|
@@ -123,6 +124,11 @@ function clearEnv() {
|
|
|
123
124
|
for (const k of ENV_KEYS_TO_RESET) {
|
|
124
125
|
delete process.env[k];
|
|
125
126
|
}
|
|
127
|
+
// The embedding provider memoizes its API key at construction (see
|
|
128
|
+
// OpenAIEmbeddingProvider); reset the singleton so embeddingsMilestone()
|
|
129
|
+
// (which now reads the live provider, not process.env directly) reflects
|
|
130
|
+
// whatever this test just set/cleared instead of a stale prior test's key.
|
|
131
|
+
resetEmbeddingProvider();
|
|
126
132
|
}
|
|
127
133
|
|
|
128
134
|
function restoreEnv() {
|
|
@@ -245,14 +251,20 @@ describe("setup milestones", () => {
|
|
|
245
251
|
hint: "Memory search is off. Set OPENAI_API_KEY (or EMBEDDING_API_KEY) on the API server to enable it; it is cheap.",
|
|
246
252
|
});
|
|
247
253
|
|
|
254
|
+
// The provider memoizes its API key at construction; in production a
|
|
255
|
+
// config reload resets it (see resetEmbeddingProvider in src/http/core.ts).
|
|
256
|
+
// Mirror that here after each direct env mutation.
|
|
248
257
|
process.env.OPENAI_API_KEY = "example-openai-embedding-key";
|
|
258
|
+
resetEmbeddingProvider();
|
|
249
259
|
expect(getMilestone(await buildStatusPayload(), "embeddings").state).toBe("configured");
|
|
250
260
|
|
|
251
261
|
process.env.EMBEDDING_API_KEY = "";
|
|
262
|
+
resetEmbeddingProvider();
|
|
252
263
|
expect(getMilestone(await buildStatusPayload(), "embeddings").state).toBe("unverified");
|
|
253
264
|
|
|
254
265
|
process.env.EMBEDDING_API_KEY = "example-dedicated-embedding-key";
|
|
255
266
|
delete process.env.OPENAI_API_KEY;
|
|
267
|
+
resetEmbeddingProvider();
|
|
256
268
|
expect(getMilestone(await buildStatusPayload(), "embeddings").state).toBe("configured");
|
|
257
269
|
});
|
|
258
270
|
|
|
@@ -48,12 +48,11 @@ function restoreEnv(name: string, value: string | undefined): void {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
|
-
*
|
|
52
|
-
* is simply the default: unset the flag for the duration of the callback.
|
|
51
|
+
* Exercise an explicit opt-out without depending on the deployment default.
|
|
53
52
|
*/
|
|
54
53
|
async function withSteeringDisabled<T>(run: () => Promise<T> | T): Promise<T> {
|
|
55
54
|
const previous = process.env.STEERING_ENABLED;
|
|
56
|
-
|
|
55
|
+
process.env.STEERING_ENABLED = "false";
|
|
57
56
|
try {
|
|
58
57
|
return await run();
|
|
59
58
|
} finally {
|
|
@@ -116,8 +115,7 @@ async function api(
|
|
|
116
115
|
const originalSteeringEnabled = process.env.STEERING_ENABLED;
|
|
117
116
|
|
|
118
117
|
beforeAll(async () => {
|
|
119
|
-
//
|
|
120
|
-
// default); withSteeringDisabled() drops the flag to exercise the default.
|
|
118
|
+
// Pin both branches explicitly so inherited configuration cannot affect them.
|
|
121
119
|
process.env.STEERING_ENABLED = "true";
|
|
122
120
|
await removeTestDb();
|
|
123
121
|
process.env.DATABASE_PATH = TEST_DB_PATH;
|
|
@@ -147,9 +145,9 @@ afterAll(async () => {
|
|
|
147
145
|
await removeTestDb();
|
|
148
146
|
});
|
|
149
147
|
|
|
150
|
-
describe("STEERING_ENABLED
|
|
151
|
-
test("steering
|
|
152
|
-
expect(isSteeringEnabled({})).toBe(
|
|
148
|
+
describe("STEERING_ENABLED defaults and overrides", () => {
|
|
149
|
+
test("steering defaults on and accepts explicit opt-outs", () => {
|
|
150
|
+
expect(isSteeringEnabled({})).toBe(true);
|
|
153
151
|
expect(isSteeringEnabled({ STEERING_ENABLED: "false" })).toBe(false);
|
|
154
152
|
expect(isSteeringEnabled({ STEERING_ENABLED: "0" })).toBe(false);
|
|
155
153
|
expect(isSteeringEnabled({ STEERING_ENABLED: "true" })).toBe(true);
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* org/drive and credentials
|
|
18
18
|
* 4. missing files and wrong drives both block attachment + task writes
|
|
19
19
|
*/
|
|
20
|
-
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
20
|
+
import { afterAll, beforeAll, describe, expect, spyOn, test } from "bun:test";
|
|
21
21
|
import crypto from "node:crypto";
|
|
22
22
|
import { unlink } from "node:fs/promises";
|
|
23
23
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
upsertSwarmConfig,
|
|
35
35
|
} from "../be/db";
|
|
36
36
|
import { registerStoreProgressTool } from "../tools/store-progress";
|
|
37
|
+
import malformedOutput from "./fixtures/store-progress-malformed-output.json";
|
|
37
38
|
|
|
38
39
|
const TEST_DB_PATH = "./test-store-progress-attachments-handler.sqlite";
|
|
39
40
|
const ORIGINAL_FETCH = globalThis.fetch;
|
|
@@ -137,6 +138,104 @@ describe("store-progress handler — attachments insert path", () => {
|
|
|
137
138
|
};
|
|
138
139
|
}
|
|
139
140
|
|
|
141
|
+
test("salvages the literal 59811b57 output and registers both URL attachments", async () => {
|
|
142
|
+
const task = await createTaskExtended("salvage leaked tool parameters", { agentId });
|
|
143
|
+
await startTask(task.id);
|
|
144
|
+
const result = (await buildServer().handler(
|
|
145
|
+
{ taskId: task.id, status: "completed", output: malformedOutput.output },
|
|
146
|
+
buildMeta(),
|
|
147
|
+
)) as StoreProgressResult;
|
|
148
|
+
|
|
149
|
+
expect(result.structuredContent.success).toBe(true);
|
|
150
|
+
const saved = await getTaskById(task.id);
|
|
151
|
+
expect(saved?.output).toBe(malformedOutput.output.split("</output>")[0]);
|
|
152
|
+
expect(saved?.output?.endsWith("reposted clean.")).toBe(true);
|
|
153
|
+
const attachments = await getTaskAttachments(task.id);
|
|
154
|
+
expect(attachments).toHaveLength(2);
|
|
155
|
+
expect(attachments.every((a) => a.kind === "url")).toBe(true);
|
|
156
|
+
expect(attachments.map((a) => a.url).sort()).toEqual([
|
|
157
|
+
"https://api.desplega.agent-swarm.dev/p/556b11781dbb40bf9b1eb1b3315adae6",
|
|
158
|
+
"https://x.com/desplegalabs/status/2100595056939827590",
|
|
159
|
+
]);
|
|
160
|
+
expect(attachments.find((a) => a.isPrimary)?.name).toBe("X reply in the Jev thread");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("preserves clean output byte-for-byte", async () => {
|
|
164
|
+
const output = "The output is ready: 2 < 3.\n Keep this whitespace. ";
|
|
165
|
+
const task = await createTaskExtended("clean output control", { agentId });
|
|
166
|
+
await startTask(task.id);
|
|
167
|
+
const result = (await buildServer().handler(
|
|
168
|
+
{ taskId: task.id, status: "completed", output },
|
|
169
|
+
buildMeta(),
|
|
170
|
+
)) as StoreProgressResult;
|
|
171
|
+
expect(result.structuredContent.success).toBe(true);
|
|
172
|
+
expect((await getTaskById(task.id))?.output).toBe(output);
|
|
173
|
+
expect(await getTaskAttachments(task.id)).toHaveLength(0);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test.each([
|
|
177
|
+
["invalid JSON", '</output>\n<parameter name="attachments">[broken'],
|
|
178
|
+
["invalid attachment schema", '</output><parameter name="attachments">[{"kind":"url"}]'],
|
|
179
|
+
["unknown parameter", '</output><parameter name="status">"failed"'],
|
|
180
|
+
["alternate closing tag", '</parameter>\n<parameter name="progress">Not JSON'],
|
|
181
|
+
["bare opener", '<parameter name="progress">Not JSON'],
|
|
182
|
+
["non-array JSON", '</output><parameter name="attachments">null'],
|
|
183
|
+
])("strips %s without losing the clean result", async (_name, tail) => {
|
|
184
|
+
const task = await createTaskExtended("unrecoverable tail", { agentId });
|
|
185
|
+
await startTask(task.id);
|
|
186
|
+
const warning = spyOn(console, "warn").mockImplementation(() => {});
|
|
187
|
+
try {
|
|
188
|
+
const result = (await buildServer().handler(
|
|
189
|
+
{ taskId: task.id, status: "completed", output: `Clean result.${tail}` },
|
|
190
|
+
buildMeta(),
|
|
191
|
+
)) as StoreProgressResult;
|
|
192
|
+
expect(result.structuredContent.success).toBe(true);
|
|
193
|
+
expect((await getTaskById(task.id))?.output).toBe("Clean result.");
|
|
194
|
+
expect((await getTaskById(task.id))?.status).toBe("completed");
|
|
195
|
+
expect(await getTaskAttachments(task.id)).toHaveLength(0);
|
|
196
|
+
expect(warning).toHaveBeenCalledTimes(1);
|
|
197
|
+
expect(warning.mock.calls[0]?.[0]).not.toContain(tail);
|
|
198
|
+
} finally {
|
|
199
|
+
warning.mockRestore();
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("appends recovered pointers to explicit attachments through the normal dedupe path", async () => {
|
|
204
|
+
const task = await createTaskExtended("mixed explicit and leaked attachments", { agentId });
|
|
205
|
+
await startTask(task.id);
|
|
206
|
+
const attachment = { kind: "url", name: "link", url: "https://example.com/link" };
|
|
207
|
+
const result = (await buildServer().handler(
|
|
208
|
+
{
|
|
209
|
+
taskId: task.id,
|
|
210
|
+
status: "completed",
|
|
211
|
+
output: `Done.<parameter name="attachments">${JSON.stringify([attachment])}</parameter>`,
|
|
212
|
+
attachments: [attachment],
|
|
213
|
+
},
|
|
214
|
+
buildMeta(),
|
|
215
|
+
)) as StoreProgressResult;
|
|
216
|
+
expect(result.structuredContent.success).toBe(true);
|
|
217
|
+
expect((await getTaskById(task.id))?.output).toBe("Done.");
|
|
218
|
+
expect(await getTaskAttachments(task.id)).toHaveLength(1);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("recovered agent-fs pointers still require ordinary scope verification", async () => {
|
|
222
|
+
const task = await createTaskExtended("verify recovered pointer", { agentId });
|
|
223
|
+
await startTask(task.id);
|
|
224
|
+
const result = (await buildServer().handler(
|
|
225
|
+
{
|
|
226
|
+
taskId: task.id,
|
|
227
|
+
status: "completed",
|
|
228
|
+
output:
|
|
229
|
+
'Done.</output><parameter name="attachments">[{"kind":"agent-fs","name":"missing.md","path":"/missing.md"}]',
|
|
230
|
+
},
|
|
231
|
+
buildMeta(),
|
|
232
|
+
)) as StoreProgressResult;
|
|
233
|
+
expect(result.structuredContent.success).toBe(false);
|
|
234
|
+
expect(result.structuredContent.message).toContain("both orgId and driveId must resolve");
|
|
235
|
+
expect((await getTaskById(task.id))?.status).toBe("in_progress");
|
|
236
|
+
expect(await getTaskAttachments(task.id)).toHaveLength(0);
|
|
237
|
+
});
|
|
238
|
+
|
|
140
239
|
test("inserts attachment row on an in-progress task (baseline)", async () => {
|
|
141
240
|
const task = await createTaskExtended("handler in-progress baseline", {
|
|
142
241
|
agentId,
|
|
@@ -134,7 +134,12 @@ describe("swarm-config reserved keys guard", () => {
|
|
|
134
134
|
await unlink(`${TEST_DB_PATH}-shm`).catch(() => {});
|
|
135
135
|
});
|
|
136
136
|
|
|
137
|
-
for (const key of [
|
|
137
|
+
for (const key of [
|
|
138
|
+
"CORS_ALLOW_ANY_ORIGIN",
|
|
139
|
+
"cors_allow_any_origin",
|
|
140
|
+
"EXTENSION_ALLOW_LEAD_ACTIVATION",
|
|
141
|
+
"extension_allow_lead_activation",
|
|
142
|
+
]) {
|
|
138
143
|
test(`rejects deployment-only ${key} through DB, HTTP, and MCP`, async () => {
|
|
139
144
|
const config = { scope: "global" as const, key, value: "true" };
|
|
140
145
|
await expect(upsertSwarmConfig(config)).rejects.toThrow(EXPECTED_MESSAGE(key));
|
|
@@ -73,11 +73,14 @@ async function pins(client: Client): Promise<string[]> {
|
|
|
73
73
|
.sort();
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
test("wire response is default-
|
|
76
|
+
test("wire response is default-on, per-task, additive, and fixed for one MCP session", async () => {
|
|
77
77
|
const task = await createTaskExtended("review fixture", { agentId, taskType: "review" });
|
|
78
78
|
const other = await createTaskExtended("other fixture", { agentId, taskType: "other" });
|
|
79
79
|
const foreign = await createTaskExtended("unowned", { taskType: "review" });
|
|
80
80
|
await configure("TASK_TOOL_MANIFESTS", '{"taskTypes":{"review":["script-run","get-tasks"]}}');
|
|
81
|
+
const defaultOn = await connect(task.id);
|
|
82
|
+
expect(await pins(defaultOn)).toEqual(["get-tasks", "script-run"]);
|
|
83
|
+
await configure("TASK_TOOL_PRELOAD_ENABLED", "false");
|
|
81
84
|
const off = await connect(task.id);
|
|
82
85
|
const baseline = (await off.listTools()).tools;
|
|
83
86
|
expect(baseline.some((tool) => tool.name === "script-run")).toBe(true);
|
|
@@ -378,7 +378,7 @@ describe("Tool Annotations & Classification", () => {
|
|
|
378
378
|
// Includes 11 skill tools, 7 MCP server tools, reusable script tools, and the
|
|
379
379
|
// native Kapso/WhatsApp tools (register/unregister number + send/reply message).
|
|
380
380
|
expect(count).toBeGreaterThanOrEqual(45);
|
|
381
|
-
expect(count).toBeLessThanOrEqual(
|
|
381
|
+
expect(count).toBeLessThanOrEqual(147);
|
|
382
382
|
});
|
|
383
383
|
|
|
384
384
|
test("core tools are fewer than deferred tools", () => {
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import * as z from "zod";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
createToolRegistrar,
|
|
6
|
+
finalizeSwarmToolResult,
|
|
7
|
+
markExtensionRequestOrigin,
|
|
8
|
+
} from "../tools/utils";
|
|
5
9
|
|
|
6
10
|
describe("createToolRegistrar with no inputSchema", () => {
|
|
7
11
|
test("handler receives requestInfo from meta when no inputSchema is defined", async () => {
|
|
@@ -117,4 +121,41 @@ describe("createToolRegistrar with no inputSchema", () => {
|
|
|
117
121
|
});
|
|
118
122
|
expect(result).toBeDefined();
|
|
119
123
|
});
|
|
124
|
+
|
|
125
|
+
test("server-marked extension calls carry extension origin", async () => {
|
|
126
|
+
const server = new McpServer({ name: "test-extension-origin", version: "1.0.0" });
|
|
127
|
+
let callOrigin: string | undefined;
|
|
128
|
+
createToolRegistrar(server)(
|
|
129
|
+
"test-extension-origin-tool",
|
|
130
|
+
{
|
|
131
|
+
inputSchema: z.object({}),
|
|
132
|
+
outputSchema: z.looseObject({}),
|
|
133
|
+
},
|
|
134
|
+
async (_args, requestInfo) => {
|
|
135
|
+
callOrigin = requestInfo.callOrigin;
|
|
136
|
+
return { ok: true, message: "ok" };
|
|
137
|
+
},
|
|
138
|
+
);
|
|
139
|
+
const registeredTools = (server as Record<string, unknown>)._registeredTools as Record<
|
|
140
|
+
string,
|
|
141
|
+
{ handler: (args: unknown, extra: unknown) => Promise<unknown> }
|
|
142
|
+
>;
|
|
143
|
+
const extra = markExtensionRequestOrigin({
|
|
144
|
+
sessionId: "extension",
|
|
145
|
+
requestInfo: { headers: { "x-agent-id": "extension-agent" } },
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
await registeredTools["test-extension-origin-tool"]!.handler({}, extra);
|
|
149
|
+
expect(callOrigin).toBe("extension");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("extension calls bypass the model context ceiling", async () => {
|
|
153
|
+
const result = await finalizeSwarmToolResult(
|
|
154
|
+
"large-extension-result",
|
|
155
|
+
{ ok: true, message: "ok", data: { value: "x".repeat(1_100_000) } },
|
|
156
|
+
{ agentId: "extension-agent", callOrigin: "extension" },
|
|
157
|
+
);
|
|
158
|
+
expect(result.structuredContent?.truncation).toBeUndefined();
|
|
159
|
+
expect((result.structuredContent?.value as string).length).toBe(1_100_000);
|
|
160
|
+
});
|
|
120
161
|
});
|
package/src/tools/defer-task.ts
CHANGED
|
@@ -169,7 +169,7 @@ export const registerDeferTaskTool = (server: McpServer) => {
|
|
|
169
169
|
})
|
|
170
170
|
.optional()
|
|
171
171
|
.describe(
|
|
172
|
-
"Wake early on a task event. Provide taskId or nonempty, unique taskIds. mode defaults to all (every member must match); any wakes on the first match. settled covers completed or
|
|
172
|
+
"Wake early on a task event. Provide taskId or nonempty, unique taskIds. mode defaults to all (every member must match); any wakes on the first match. settled covers completed, failed, or cancelled. Deferred and superseded members follow their continuations; a superseded member without a resume child holds to the ceiling. Any already-terminal member rejects the request; one delayMs/runAt ceiling is still required for the whole set.",
|
|
173
173
|
),
|
|
174
174
|
summary: z
|
|
175
175
|
.string()
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
import { createToolRegistrar, toolOk } from "@/tools/utils";
|
|
4
|
+
import {
|
|
5
|
+
coerceExtensionSummary,
|
|
6
|
+
extensionToolOutputSchema,
|
|
7
|
+
proxyExtensionsApi,
|
|
8
|
+
} from "./extension-common";
|
|
9
|
+
|
|
10
|
+
export const registerExtensionActivateVersionTool = (server: McpServer) => {
|
|
11
|
+
createToolRegistrar(server)(
|
|
12
|
+
"extension-activate-version",
|
|
13
|
+
{
|
|
14
|
+
title: "Extension Activate Version",
|
|
15
|
+
description:
|
|
16
|
+
"Activate a stored extension version, reloading it if enabled. Requires a lead, operator, or dashboard user.",
|
|
17
|
+
annotations: { openWorldHint: false },
|
|
18
|
+
inputSchema: z
|
|
19
|
+
.object({
|
|
20
|
+
id: z.string().uuid().describe("Installed extension ID from extension-list."),
|
|
21
|
+
version: z.number().int().min(1).describe("Stored version number to activate."),
|
|
22
|
+
})
|
|
23
|
+
.strict(),
|
|
24
|
+
outputSchema: extensionToolOutputSchema,
|
|
25
|
+
},
|
|
26
|
+
async (args, requestInfo) =>
|
|
27
|
+
proxyExtensionsApi({
|
|
28
|
+
method: "POST",
|
|
29
|
+
path: `/api/extensions/${args.id}/activate-version`,
|
|
30
|
+
body: { version: args.version },
|
|
31
|
+
requestInfo,
|
|
32
|
+
success: (data) =>
|
|
33
|
+
toolOk("Extension activated.", {
|
|
34
|
+
data: coerceExtensionSummary((data as { extension?: unknown }).extension),
|
|
35
|
+
}),
|
|
36
|
+
}),
|
|
37
|
+
);
|
|
38
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
import { proxySwarmApi } from "./script-common";
|
|
3
|
+
import { type RequestInfo, type SwarmToolResult, swarmToolOutputSchema, toolErr } from "./utils";
|
|
4
|
+
|
|
5
|
+
export const EXTENSION_TRANSPORT_ERROR =
|
|
6
|
+
"extension_* tools require HTTP MCP transport because agent identity is unavailable over stdio. Use MCP_BASE_URL=http://... or invoke the HTTP API directly.";
|
|
7
|
+
|
|
8
|
+
const extensionListItemSchema = z.looseObject({
|
|
9
|
+
id: z.string().optional(),
|
|
10
|
+
name: z.string().optional(),
|
|
11
|
+
version: z.number().optional(),
|
|
12
|
+
activeVersion: z.number().optional(),
|
|
13
|
+
enabled: z.boolean().optional(),
|
|
14
|
+
status: z.string().optional(),
|
|
15
|
+
priority: z.number().optional(),
|
|
16
|
+
consecutiveFailures: z.number().optional(),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export const extensionToolOutputSchema = swarmToolOutputSchema({
|
|
20
|
+
id: z.string().optional(),
|
|
21
|
+
name: z.string().optional(),
|
|
22
|
+
version: z.number().optional(),
|
|
23
|
+
activeVersion: z.number().optional(),
|
|
24
|
+
enabled: z.boolean().optional(),
|
|
25
|
+
status: z.string().optional(),
|
|
26
|
+
priority: z.number().optional(),
|
|
27
|
+
consecutiveFailures: z.number().optional(),
|
|
28
|
+
contentDeduped: z.boolean().optional(),
|
|
29
|
+
deleted: z.boolean().optional(),
|
|
30
|
+
extensions: z.array(extensionListItemSchema).optional(),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
34
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function coerceExtensionSummary(entry: unknown) {
|
|
38
|
+
const extension = isRecord(entry) ? entry : {};
|
|
39
|
+
return {
|
|
40
|
+
id: typeof extension.id === "string" ? extension.id : undefined,
|
|
41
|
+
name: typeof extension.name === "string" ? extension.name : undefined,
|
|
42
|
+
version: typeof extension.version === "number" ? extension.version : undefined,
|
|
43
|
+
activeVersion:
|
|
44
|
+
typeof extension.activeVersion === "number" ? extension.activeVersion : undefined,
|
|
45
|
+
enabled: typeof extension.enabled === "boolean" ? extension.enabled : undefined,
|
|
46
|
+
status: typeof extension.status === "string" ? extension.status : undefined,
|
|
47
|
+
priority: typeof extension.priority === "number" ? extension.priority : undefined,
|
|
48
|
+
consecutiveFailures:
|
|
49
|
+
typeof extension.consecutiveFailures === "number" ? extension.consecutiveFailures : undefined,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function errorMessage(data: unknown, status: number): string {
|
|
54
|
+
if (isRecord(data) && typeof data.error === "string" && data.error) return data.error;
|
|
55
|
+
return `Extensions API request failed with ${status}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function renderDiagnostics(data: unknown): string | undefined {
|
|
59
|
+
if (!isRecord(data) || !Array.isArray(data.diagnostics)) return undefined;
|
|
60
|
+
const diagnostics = data.diagnostics.map(String).filter(Boolean);
|
|
61
|
+
return diagnostics.length > 0 ? diagnostics.join("\n") : undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function proxyExtensionsApi<T>(args: {
|
|
65
|
+
method: "GET" | "POST" | "DELETE";
|
|
66
|
+
path: string;
|
|
67
|
+
body?: unknown;
|
|
68
|
+
requestInfo: RequestInfo;
|
|
69
|
+
success: (data: unknown) => SwarmToolResult<T & Record<string, unknown>>;
|
|
70
|
+
}): Promise<SwarmToolResult<T & Record<string, unknown>>> {
|
|
71
|
+
return proxySwarmApi({
|
|
72
|
+
...args,
|
|
73
|
+
transportError: EXTENSION_TRANSPORT_ERROR,
|
|
74
|
+
respond: (data, response) => {
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
const diagnostics = renderDiagnostics(data);
|
|
77
|
+
if (response.status === 400 && diagnostics) {
|
|
78
|
+
return toolErr("Extension rejected by typecheck.", { details: diagnostics });
|
|
79
|
+
}
|
|
80
|
+
return toolErr(errorMessage(data, response.status), { details: diagnostics });
|
|
81
|
+
}
|
|
82
|
+
return args.success(data);
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
import { createToolRegistrar, toolOk } from "@/tools/utils";
|
|
4
|
+
import { extensionToolOutputSchema, proxyExtensionsApi } from "./extension-common";
|
|
5
|
+
|
|
6
|
+
export const registerExtensionDeleteTool = (server: McpServer) => {
|
|
7
|
+
createToolRegistrar(server)(
|
|
8
|
+
"extension-delete",
|
|
9
|
+
{
|
|
10
|
+
title: "Extension Delete",
|
|
11
|
+
description:
|
|
12
|
+
"Uninstall a disabled extension and delete its stored history. Disable it first with extension-disable. Requires a lead, operator, or dashboard user.",
|
|
13
|
+
annotations: { openWorldHint: false, destructiveHint: true },
|
|
14
|
+
inputSchema: z
|
|
15
|
+
.object({
|
|
16
|
+
id: z.string().uuid().describe("Installed extension ID from extension-list."),
|
|
17
|
+
})
|
|
18
|
+
.strict(),
|
|
19
|
+
outputSchema: extensionToolOutputSchema,
|
|
20
|
+
},
|
|
21
|
+
async (args, requestInfo) =>
|
|
22
|
+
proxyExtensionsApi({
|
|
23
|
+
method: "DELETE",
|
|
24
|
+
path: `/api/extensions/${args.id}`,
|
|
25
|
+
requestInfo,
|
|
26
|
+
success: () => toolOk("Extension deleted.", { data: { id: args.id, deleted: true } }),
|
|
27
|
+
}),
|
|
28
|
+
);
|
|
29
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
import { createToolRegistrar, toolOk } from "@/tools/utils";
|
|
4
|
+
import {
|
|
5
|
+
coerceExtensionSummary,
|
|
6
|
+
extensionToolOutputSchema,
|
|
7
|
+
proxyExtensionsApi,
|
|
8
|
+
} from "./extension-common";
|
|
9
|
+
|
|
10
|
+
export const registerExtensionDisableTool = (server: McpServer) => {
|
|
11
|
+
createToolRegistrar(server)(
|
|
12
|
+
"extension-disable",
|
|
13
|
+
{
|
|
14
|
+
title: "Extension Disable",
|
|
15
|
+
description:
|
|
16
|
+
"Unload and disable an installed extension. Requires a lead, operator, or dashboard user.",
|
|
17
|
+
annotations: { openWorldHint: false },
|
|
18
|
+
inputSchema: z
|
|
19
|
+
.object({
|
|
20
|
+
id: z.string().uuid().describe("Installed extension ID from extension-list."),
|
|
21
|
+
})
|
|
22
|
+
.strict(),
|
|
23
|
+
outputSchema: extensionToolOutputSchema,
|
|
24
|
+
},
|
|
25
|
+
async (args, requestInfo) =>
|
|
26
|
+
proxyExtensionsApi({
|
|
27
|
+
method: "POST",
|
|
28
|
+
path: `/api/extensions/${args.id}/disable`,
|
|
29
|
+
requestInfo,
|
|
30
|
+
success: (data) =>
|
|
31
|
+
toolOk("Extension disabled.", {
|
|
32
|
+
data: coerceExtensionSummary((data as { extension?: unknown }).extension),
|
|
33
|
+
}),
|
|
34
|
+
}),
|
|
35
|
+
);
|
|
36
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
import { createToolRegistrar, toolOk } from "@/tools/utils";
|
|
4
|
+
import {
|
|
5
|
+
coerceExtensionSummary,
|
|
6
|
+
extensionToolOutputSchema,
|
|
7
|
+
proxyExtensionsApi,
|
|
8
|
+
} from "./extension-common";
|
|
9
|
+
|
|
10
|
+
export const registerExtensionEnableTool = (server: McpServer) => {
|
|
11
|
+
createToolRegistrar(server)(
|
|
12
|
+
"extension-enable",
|
|
13
|
+
{
|
|
14
|
+
title: "Extension Enable",
|
|
15
|
+
description:
|
|
16
|
+
"Load and enable an installed extension. Requires a lead, operator, or dashboard user.",
|
|
17
|
+
annotations: { openWorldHint: false },
|
|
18
|
+
inputSchema: z
|
|
19
|
+
.object({
|
|
20
|
+
id: z.string().uuid().describe("Installed extension ID from extension-list."),
|
|
21
|
+
})
|
|
22
|
+
.strict(),
|
|
23
|
+
outputSchema: extensionToolOutputSchema,
|
|
24
|
+
},
|
|
25
|
+
async (args, requestInfo) =>
|
|
26
|
+
proxyExtensionsApi({
|
|
27
|
+
method: "POST",
|
|
28
|
+
path: `/api/extensions/${args.id}/enable`,
|
|
29
|
+
requestInfo,
|
|
30
|
+
success: (data) =>
|
|
31
|
+
toolOk("Extension enabled.", {
|
|
32
|
+
data: coerceExtensionSummary((data as { extension?: unknown }).extension),
|
|
33
|
+
}),
|
|
34
|
+
}),
|
|
35
|
+
);
|
|
36
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
import { createToolRegistrar, toolOk } from "@/tools/utils";
|
|
4
|
+
import { ExtensionManifestSchema } from "@/types";
|
|
5
|
+
import {
|
|
6
|
+
coerceExtensionSummary,
|
|
7
|
+
extensionToolOutputSchema,
|
|
8
|
+
proxyExtensionsApi,
|
|
9
|
+
} from "./extension-common";
|
|
10
|
+
|
|
11
|
+
export const EXTENSION_INSTALL_DESCRIPTION =
|
|
12
|
+
"Validate and store a TypeScript extension bundle (manifest plus a files map with one hooks.ts). Fetch the hook contract first: GET /api/extensions/type-defs returns swarm-extension.d.ts with every event and modify shape. The extension remains disabled until a lead, operator, or dashboard user enables it with extension-enable.";
|
|
13
|
+
|
|
14
|
+
function extensionInstallResult(data: unknown, fallbackName: string) {
|
|
15
|
+
const body = data as {
|
|
16
|
+
extension?: unknown;
|
|
17
|
+
contentDeduped?: unknown;
|
|
18
|
+
};
|
|
19
|
+
const extension = coerceExtensionSummary(body.extension);
|
|
20
|
+
const name = extension.name ?? fallbackName;
|
|
21
|
+
const version = typeof extension.version === "number" ? ` v${extension.version}` : "";
|
|
22
|
+
const deduped = body.contentDeduped === true ? " Content was unchanged." : "";
|
|
23
|
+
const enableInstruction =
|
|
24
|
+
"A lead, operator, or dashboard user can enable it with `extension-enable` or `POST /api/extensions/{id}/enable`.";
|
|
25
|
+
|
|
26
|
+
return toolOk(`Extension \`${name}\`${version} installed.${deduped} ${enableInstruction}`, {
|
|
27
|
+
data: {
|
|
28
|
+
...extension,
|
|
29
|
+
name,
|
|
30
|
+
contentDeduped: body.contentDeduped === true,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const registerExtensionInstallTool = (server: McpServer) => {
|
|
36
|
+
createToolRegistrar(server)(
|
|
37
|
+
"extension-install",
|
|
38
|
+
{
|
|
39
|
+
title: "Extension Install",
|
|
40
|
+
description: EXTENSION_INSTALL_DESCRIPTION,
|
|
41
|
+
annotations: { openWorldHint: false },
|
|
42
|
+
inputSchema: z
|
|
43
|
+
.object({
|
|
44
|
+
manifest: ExtensionManifestSchema.describe("Extension bundle manifest."),
|
|
45
|
+
files: z.record(z.string(), z.string()).describe("Bundle files keyed by relative path."),
|
|
46
|
+
priority: z
|
|
47
|
+
.number()
|
|
48
|
+
.int()
|
|
49
|
+
.optional()
|
|
50
|
+
.describe("Handler priority. Lower values run first."),
|
|
51
|
+
config: z.record(z.string(), z.unknown()).optional().describe("Extension configuration."),
|
|
52
|
+
})
|
|
53
|
+
.strict(),
|
|
54
|
+
outputSchema: extensionToolOutputSchema,
|
|
55
|
+
},
|
|
56
|
+
async (args, requestInfo) =>
|
|
57
|
+
proxyExtensionsApi({
|
|
58
|
+
method: "POST",
|
|
59
|
+
path: "/api/extensions/install",
|
|
60
|
+
body: args,
|
|
61
|
+
requestInfo,
|
|
62
|
+
success: (data) => extensionInstallResult(data, args.manifest.name),
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
};
|