@desplega.ai/agent-swarm 1.162.0 → 1.164.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 +2 -1
- package/dist/{acp-adapter-74m73ae9.js → acp-adapter-8t22d18g.js} +2 -2
- package/dist/{actions-4sr14gnt.js → actions-49xrhww7.js} +32 -29
- package/dist/amp-adapter-p230tajt.js +34 -0
- package/dist/{anthropic-messages-8xxrzky9.js → anthropic-messages-hst62ws9.js} +3 -3
- package/dist/{app-pt68fbwp.js → app-tgzw3f3d.js} +11 -10
- package/dist/{assistant-cv3jytks.js → assistant-dgfjm07j.js} +33 -30
- package/dist/{azure-openai-responses-f5cqsyj8.js → azure-openai-responses-mh8tvf78.js} +9 -68
- package/dist/{boot-reembed-4ht4gqth.js → boot-reembed-0qzs614r.js} +13 -12
- package/dist/{boot-reembed-nxpcvpj9.js → boot-reembed-dgp6peyx.js} +12 -11
- package/dist/{boot-scrub-logs-k93kyw8k.js → boot-scrub-logs-fxnk021a.js} +10 -9
- package/dist/{boot-seeds-1skcrs0e.js → boot-seeds-xe7adjvm.js} +14 -13
- package/dist/{claude-adapter-r5g0tzge.js → claude-adapter-hx0n8a31.js} +22 -20
- package/dist/{claude-managed-adapter-yxkfxg8j.js → claude-managed-adapter-hskapeen.js} +3 -3
- package/dist/{claude-managed-setup-pkyrgshb.js → claude-managed-setup-p7mt6dfq.js} +1 -1
- package/dist/{claude-sdk-session-jxyxnrwd.js → claude-sdk-session-d61a9c8x.js} +22 -20
- package/dist/{cli-twrh04ap.js → cli-0y30kqz4.js} +3 -3
- package/dist/{cli-5xhpb3kv.js → cli-0z9dj97t.js} +9 -9
- package/dist/{cli-t845n9xk.js → cli-1nzb8ftr.js} +2 -2
- package/dist/{cli-kdkwd3yr.js → cli-1wd1n2ya.js} +38 -10
- package/dist/{cli-vxe6qa4h.js → cli-23az412e.js} +1 -1
- package/dist/{cli-bsky8g7w.js → cli-26x4d89j.js} +1 -1
- package/dist/{cli-swfmp1fx.js → cli-2ar0x7cz.js} +9 -9
- package/dist/cli-2ksgp01n.js +933 -0
- package/dist/{cli-rnyzn44p.js → cli-2zhhnz2h.js} +1 -1
- package/dist/{cli-vc9bp061.js → cli-3qaw32we.js} +5 -5
- package/dist/{cli-vy3ed88x.js → cli-402m8btc.js} +31 -3
- package/dist/{cli-5bcd7sfy.js → cli-43fh8fzw.js} +3 -3
- package/dist/{cli-5k6k8ta3.js → cli-4f7s6k3q.js} +13 -5
- package/dist/{cli-sqvfa090.js → cli-4ntg6tkp.js} +6 -5
- package/dist/{cli-kh04eqwe.js → cli-4y9bnpvy.js} +1 -1
- package/dist/{cli-qjjvzwzd.js → cli-5c2j5yvx.js} +1 -1
- package/dist/{cli-2zy95vhm.js → cli-6891ft0y.js} +2 -2
- package/dist/{cli-tre4bdgq.js → cli-6behs2bt.js} +1 -1
- package/dist/cli-7mm2r1kp.js +17 -0
- package/dist/cli-7xcgj20z.js +68 -0
- package/dist/{cli-4cwxn7gp.js → cli-812vt3ht.js} +2 -2
- package/dist/{cli-mbssg1xv.js → cli-8kt0bmgx.js} +1 -1
- package/dist/{cli-wjgs5wxe.js → cli-8rpq35c0.js} +7 -7
- package/dist/{cli-jxs2ygaw.js → cli-9m8fk2v7.js} +74 -29
- package/dist/{cli-pkd6s2wy.js → cli-9q3wt23f.js} +2 -2
- package/dist/{cli-s3hwpt9v.js → cli-9s6fsg07.js} +30 -10
- package/dist/{cli-xqpk3rp7.js → cli-9x3bwfd3.js} +28 -5
- package/dist/{cli-qrgt5bqf.js → cli-agamgm0p.js} +1 -1
- package/dist/{cli-13hw4z91.js → cli-b0f70z24.js} +1 -1
- package/dist/{cli-gpqpmns7.js → cli-cgrvsz2t.js} +2 -2
- package/dist/{cli-2hhfhfq5.js → cli-cn53e7vc.js} +8 -1
- package/dist/{cli-p0r8sxj5.js → cli-crjazzsg.js} +222 -4
- package/dist/{cli-tcen9p5w.js → cli-d37n8b7c.js} +5 -5
- package/dist/{cli-wxkkd0e0.js → cli-dgh781z9.js} +3 -3
- package/dist/cli-dsdg2wrk.js +516 -0
- package/dist/{cli-q24re23n.js → cli-e2xnc4m5.js} +4 -4
- package/dist/{cli-f7kvq46p.js → cli-erxeg64j.js} +20 -0
- package/dist/{cli-8ev9xqnx.js → cli-fajbc7sa.js} +17 -219
- package/dist/{cli-g739zqzt.js → cli-fj86xr3c.js} +2 -2
- package/dist/{cli-zvhd3f64.js → cli-fpbvcf55.js} +3 -3
- package/dist/{cli-kp42yek0.js → cli-fyra5y8k.js} +424 -71
- package/dist/{cli-jyy6pm2f.js → cli-g3qgrzck.js} +15 -3
- package/dist/{cli-eg0b3p0k.js → cli-g5erf0zt.js} +23 -14
- package/dist/{cli-cvatsam7.js → cli-gcvzvq3e.js} +4 -4
- package/dist/{cli-89s4ngw3.js → cli-gvcgmx69.js} +1 -1
- package/dist/{cli-7tre70zp.js → cli-j261c831.js} +138 -128
- package/dist/{cli-sptrj5f5.js → cli-j3me0jw2.js} +2 -2
- package/dist/{cli-zxqx25nz.js → cli-k6w44yvq.js} +6 -6
- package/dist/cli-ndx5yz5n.js +3299 -0
- package/dist/{cli-ykk5jaax.js → cli-nexwh8zg.js} +34 -29
- package/dist/{cli-rfjw9jw2.js → cli-ng97fjq7.js} +2 -2
- package/dist/{cli-4x0csdjq.js → cli-ngne1dng.js} +2 -2
- package/dist/{cli-4dcpaefp.js → cli-pfzrhb8n.js} +2 -2
- package/dist/{cli-hb5c2x5y.js → cli-q38n4bq0.js} +2 -2
- package/dist/{cli-cebmgwsc.js → cli-r109nq7n.js} +4 -4
- package/dist/{cli-nq7yqw7b.js → cli-t7g0vmtc.js} +4 -4
- package/dist/{cli-jf7wmv1r.js → cli-tsvj33y0.js} +58 -19
- package/dist/{cli-7af8yhav.js → cli-vbr859rz.js} +7 -4
- package/dist/{cli-nt8hecg9.js → cli-wk3v6h55.js} +67 -38
- package/dist/{cli-vyqs9ya8.js → cli-wwvk0tsy.js} +7 -7
- package/dist/{cli-rj0gqwwp.js → cli-y2de3bzp.js} +1 -1
- package/dist/{cli-hmsw1gpy.js → cli-y74170pb.js} +4 -4
- package/dist/cli-ydt3zkwv.js +70 -0
- package/dist/{cli-xk79303e.js → cli-yj790e19.js} +9 -1
- package/dist/cli-zt3w1cn1.js +137 -0
- package/dist/{cli-trc24wk2.js → cli-zxg80zs9.js} +1 -1
- package/dist/cli.js +23 -22
- package/dist/{cloudflare-workers-ai-system-one-k8ddqjkq.js → cloudflare-workers-ai-system-one-tt484t61.js} +2 -2
- package/dist/{codex-adapter-esvmnhn7.js → codex-adapter-e3pky8aj.js} +14 -13
- package/dist/{codex-hook-m73jmqgx.js → codex-hook-b1fb7c2k.js} +4 -3
- package/dist/{codex-session-runner-ban9excr.js → codex-session-runner-1gq27w2t.js} +14 -13
- package/dist/{commands-75413p2n.js → commands-q07ezjzn.js} +10 -9
- package/dist/cursor-adapter-50cz7yba.js +32 -0
- package/dist/{db-7f5pecwg.js → db-wwt0sq9q.js} +18 -9
- package/dist/{dispatcher-fh9at8nz.js → dispatcher-wrg35vdd.js} +31 -28
- package/dist/{dsh-adapter-wpze65d4.js → dsh-adapter-sa6rxfpc.js} +6 -6
- package/dist/{e2b-gjgdjp91.js → e2b-v5np73yj.js} +4 -4
- package/dist/{easter-egg-3d-3w5c2n4c.js → easter-egg-3d-h2hx9b64.js} +2 -2
- package/dist/{event-bus-sfx8dx3t.js → event-bus-vkbhzt4n.js} +1 -1
- package/dist/{execute-f50wg9nh.js → execute-6gczxy8v.js} +42 -13
- package/dist/{google-generative-ai-jxxpnegy.js → google-generative-ai-d6nphg3q.js} +3 -3
- package/dist/{google-vertex-x7q1angm.js → google-vertex-cpshkref.js} +3 -3
- package/dist/{handlers-w13787bb.js → handlers-rv3gxvsp.js} +34 -31
- package/dist/{hook-46g1jrvx.js → hook-bcf5m80y.js} +15 -13
- package/dist/{hook-4tpts2ry.js → hook-z8b1jm2d.js} +16 -14
- package/dist/{http-19pzbfrt.js → http-56f7yt97.js} +932 -134
- package/dist/{index-scptvdxg.js → index-0kncjnrm.js} +30 -23
- package/dist/{index-dqmp15bq.js → index-1e6cg96n.js} +31 -28
- package/dist/{index-qb4q1y3e.js → index-8xgdb3k8.js} +31 -28
- package/dist/index-adw4zr9j.js +1020 -0
- package/dist/{index-h5gs261z.js → index-etdrp7yp.js} +140 -140
- package/dist/{index-8fd4dm4h.js → index-f9zscmft.js} +32 -29
- package/dist/index-fdk1t4d6.js +91297 -0
- package/dist/{index-s75wc4wd.js → index-jvb37h77.js} +12 -11
- package/dist/{keepalive-xa5ts9cd.js → keepalive-rp8daj4s.js} +12 -11
- package/dist/{lead-qp4xkvjq.js → lead-dfp77erc.js} +45 -40
- package/dist/{maintenance-rm5885hm.js → maintenance-wtmw3jqv.js} +13 -12
- package/dist/{mistral-conversations-rw7fbt22.js → mistral-conversations-8445ddjn.js} +2 -2
- package/dist/{oauth-refresh-sweep-1csdfebf.js → oauth-refresh-sweep-yhzqt95v.js} +11 -10
- package/dist/{onboard-r6b78cs4.js → onboard-9z8rwbcy.js} +2 -2
- package/dist/{openai-codex-responses-efvrgrq3.js → openai-codex-responses-9ynm88zy.js} +3 -3
- package/dist/{openai-completions-bbbrrz4z.js → openai-completions-nhqwjmaw.js} +2 -2
- package/dist/{openai-responses-9jjyrcm1.js → openai-responses-xx1c4kgm.js} +3 -3
- package/dist/{opencode-adapter-kb3g6pkf.js → opencode-adapter-1nxb6555.js} +5 -5
- package/dist/{otel-impl-0y9knpky.js → otel-impl-b83gaxw2.js} +1 -1
- package/dist/{pi-mono-adapter-z6s1bme2.js → pi-mono-adapter-ddxz8pjp.js} +18 -17
- package/dist/{pricing-refresh-mth26tgp.js → pricing-refresh-tmjvazb6.js} +12 -11
- package/dist/{rbac-roles-kwntb23q.js → rbac-roles-pa0gzjws.js} +11 -10
- package/dist/{rbac-roles-vpk4ebm8.js → rbac-roles-rhexa9s2.js} +12 -11
- package/dist/{render-v2-hcm7h00y.js → render-v2-ask84n8y.js} +11 -10
- package/dist/{runtime-vfjsfsc5.js → runtime-ckjnj3vf.js} +4 -4
- package/dist/{setup-hbyzd65h.js → setup-5ga5gdsy.js} +2 -2
- package/dist/{typesafe-system-one-2ef30pec.js → typesafe-system-one-datfr82p.js} +2 -2
- package/dist/{virtual-modules-9bv7ry91.js → virtual-modules-bmp3zwjf.js} +12 -11
- package/dist/{work-objects-m09sxnhe.js → work-objects-snnh0t7j.js} +34 -31
- package/dist/{worker-0h35rvk4.js → worker-rxyymrhb.js} +45 -40
- package/openapi.json +997 -39
- package/package.json +6 -5
- package/src/be/db/active-sessions.ts +30 -0
- package/src/be/db/approvals.ts +54 -0
- package/src/be/db.ts +25 -4
- package/src/be/mcp-server-checked-update.ts +34 -0
- package/src/be/mcp-server-stdio-gate.ts +34 -0
- package/src/be/memory/key-browser.ts +304 -0
- package/src/be/memory/providers/sqlite-store.ts +16 -3
- package/src/be/migrations/194_memory_orphan_chunks.sql +15 -0
- package/src/be/migrations/195_approval_request_approvals.sql +6 -0
- package/src/be/model-validation.ts +12 -2
- package/src/be/multi-runtime.ts +4 -3
- package/src/be/pricing-normalize.ts +11 -0
- package/src/be/repo-checked-update.ts +37 -0
- package/src/be/repo-merge-policy.ts +15 -0
- package/src/be/seed-pricing.ts +48 -1
- package/src/be/swarm-config-guard.ts +1 -0
- package/src/commands/provider-credentials.ts +44 -2
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +100 -0
- package/src/extensions/dispatcher.ts +19 -0
- package/src/extensions/post-bridge.ts +191 -0
- package/src/github/handlers.ts +128 -2
- package/src/gitlab/workflow-action-name.ts +15 -0
- package/src/heartbeat/heartbeat.ts +4 -3
- package/src/http/active-sessions.ts +6 -5
- package/src/http/agents.ts +10 -1
- package/src/http/approval-requests.ts +271 -35
- package/src/http/index.ts +1 -1
- package/src/http/mcp-servers.ts +74 -50
- package/src/http/memory.ts +275 -6
- package/src/http/repos.ts +58 -18
- package/src/http/request-principal.ts +19 -0
- package/src/http/session-cost-recompute.ts +64 -0
- package/src/http/session-data.ts +11 -0
- package/src/http/tasks.ts +9 -1
- package/src/http/webhooks.ts +17 -5
- package/src/prompts/base-prompt.ts +9 -4
- package/src/prompts/resolver.ts +1 -1
- package/src/prompts/session-templates.ts +28 -0
- package/src/providers/amp-adapter.ts +1232 -0
- package/src/providers/cursor-adapter.ts +638 -0
- package/src/providers/index.ts +9 -1
- package/src/providers/pricing-sources.md +39 -0
- package/src/providers/reasoning-effort.ts +15 -1
- package/src/providers/types.ts +11 -1
- package/src/rbac/legacy-policy.ts +23 -8
- package/src/rbac/permissions.ts +27 -1
- package/src/scripts-runtime/types/swarm-extension.d.ts +119 -0
- package/src/tests/amp-adapter.test.ts +822 -0
- package/src/tests/amp-budget-export-failed.test.ts +133 -0
- package/src/tests/amp-pricing-helpers.ts +16 -0
- package/src/tests/amp-pricing.test.ts +218 -0
- package/src/tests/api-drain-worker-e2e.test.ts +18 -7
- package/src/tests/approval-respond-auth.test.ts +371 -0
- package/src/tests/base-prompt.test.ts +13 -0
- package/src/tests/claude-adapter-binary.test.ts +24 -0
- package/src/tests/claude-adapter.test.ts +8 -0
- package/src/tests/cursor-adapter.test.ts +690 -0
- package/src/tests/db-task-read-contract.test.ts +5 -0
- package/src/tests/db-task-write-contract.test.ts +5 -0
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/extensions-post-bridge.test.ts +669 -0
- package/src/tests/extensions-run-context.test.ts +61 -0
- package/src/tests/fixtures/fake-amp.ts +304 -0
- package/src/tests/github-handlers-inline-comments.test.ts +147 -1
- package/src/tests/gitlab-workflow-event-names.test.ts +136 -0
- package/src/tests/mcp-input-ergonomics.test.ts +15 -10
- package/src/tests/mcp-server-stdio-gate.test.ts +438 -0
- package/src/tests/memory-http-access.test.ts +221 -0
- package/src/tests/memory-keys-endpoint.test.ts +406 -0
- package/src/tests/memory-store.test.ts +166 -0
- package/src/tests/model-tier-resolution.test.ts +3 -3
- package/src/tests/pricing-refresh.test.ts +5 -4
- package/src/tests/prompt-template-session.test.ts +5 -3
- package/src/tests/provider-process-group.test.ts +41 -0
- package/src/tests/provider-registration.test.ts +3 -1
- package/src/tests/rbac-charact-misc-tools.test.ts +18 -3
- package/src/tests/rbac-engine.test.ts +15 -11
- package/src/tests/rbac-wire-e2e.test.ts +4 -9
- package/src/tests/repo-allow-merge-gate.test.ts +382 -0
- package/src/tests/script-connections.test.ts +4 -0
- package/src/tests/send-task-requester-gate.test.ts +260 -0
- package/src/tests/session-activity-liveness.test.ts +248 -0
- package/src/tests/session-costs-golden.test.ts +28 -0
- package/src/tests/webhooks-task-blocked.test.ts +4 -4
- package/src/tests/write-gate.ts +68 -0
- package/src/tests/x402-client.test.ts +124 -0
- package/src/tests/x402-spending-tracker.test.ts +66 -0
- package/src/tools/mcp-servers/mcp-server-create.ts +23 -1
- package/src/tools/mcp-servers/mcp-server-update.ts +46 -28
- package/src/tools/memory-delete.ts +3 -3
- package/src/tools/repos/update-repo.ts +26 -5
- package/src/tools/send-task.ts +24 -1
- package/src/tools/steer-task.ts +1 -1
- package/src/types.ts +37 -2
- package/src/utils/amp-models.ts +114 -0
- package/src/utils/credentials.ts +8 -0
- package/src/utils/process-group.ts +79 -0
- package/src/workflows/event-bus.ts +28 -0
- package/src/x402/client.ts +36 -14
- package/src/x402/spending-tracker.ts +64 -3
- package/templates/skills/agent-browser/SKILL.md +5 -0
- package/templates/skills/agent-browser/content.md +5 -0
- package/templates/skills/heartbeat-runbook/content.md +1 -0
- package/templates/skills/swarm-extensions/content.md +4 -0
- package/dist/cli-y89t4nca.js +0 -67
|
@@ -28,6 +28,7 @@ import type { SwarmToolResult } from "../tools/utils";
|
|
|
28
28
|
|
|
29
29
|
const agentId = "aaaa0000-0000-4000-8000-000000000901";
|
|
30
30
|
const otherId = "bbbb0000-0000-4000-8000-000000000902";
|
|
31
|
+
const leadId = "cccc0000-0000-4000-8000-000000000903";
|
|
31
32
|
const server = new McpServer({ name: "input-regressions", version: "1.0.0" });
|
|
32
33
|
for (const register of [
|
|
33
34
|
registerAcceptSteerTool,
|
|
@@ -82,6 +83,7 @@ beforeAll(async () => {
|
|
|
82
83
|
initDb(":memory:");
|
|
83
84
|
await createAgent({ id: agentId, name: "Schema worker", isLead: false, status: "idle" });
|
|
84
85
|
await createAgent({ id: otherId, name: "Other schema worker", isLead: false, status: "idle" });
|
|
86
|
+
await createAgent({ id: leadId, name: "Schema lead", isLead: true, status: "idle" });
|
|
85
87
|
});
|
|
86
88
|
afterAll(() => {
|
|
87
89
|
closeDb();
|
|
@@ -312,11 +314,13 @@ test("send-task accepts actual registry user IDs without UUID hyphens", async ()
|
|
|
312
314
|
email: "schema-requester@example.test",
|
|
313
315
|
});
|
|
314
316
|
expect(user.id).toMatch(/^[a-f0-9]{32}$/);
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
allowDuplicate: true,
|
|
319
|
-
|
|
317
|
+
// Naming a requester is the lead's call; the worker rule is covered in send-task-requester-gate.
|
|
318
|
+
const result = await call(
|
|
319
|
+
"send-task",
|
|
320
|
+
{ task: "requester preservation", requestedByUserId: user.id, allowDuplicate: true },
|
|
321
|
+
undefined,
|
|
322
|
+
leadId,
|
|
323
|
+
);
|
|
320
324
|
expect(result.structuredContent?.success).toBe(true);
|
|
321
325
|
const created = result.structuredContent?.task as { id: string };
|
|
322
326
|
expect((await getTaskById(created.id))?.requestedByUserId).toBe(user.id);
|
|
@@ -326,11 +330,12 @@ test("send-task rejects unknown requester IDs without creating a task", async ()
|
|
|
326
330
|
const before = await getDbClient().get<{ count: number }>(
|
|
327
331
|
"SELECT COUNT(*) AS count FROM agent_tasks",
|
|
328
332
|
);
|
|
329
|
-
const result = await call(
|
|
330
|
-
task
|
|
331
|
-
requestedByUserId: "f".repeat(32),
|
|
332
|
-
|
|
333
|
-
|
|
333
|
+
const result = await call(
|
|
334
|
+
"send-task",
|
|
335
|
+
{ task: "unknown requester", requestedByUserId: "f".repeat(32), allowDuplicate: true },
|
|
336
|
+
undefined,
|
|
337
|
+
leadId,
|
|
338
|
+
);
|
|
334
339
|
expect(result.structuredContent?.success).toBe(false);
|
|
335
340
|
expect(result.structuredContent?.message).toContain("existing registered user");
|
|
336
341
|
expect(
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An agent-scope stdio MCP server runs an arbitrary command on the worker, so creating or
|
|
3
|
+
* changing one needs the same authorization as a swarm-scope server. Remote http/sse servers
|
|
4
|
+
* at agent scope stay open to every agent. Covers the MCP tools and the HTTP update route.
|
|
5
|
+
*/
|
|
6
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
7
|
+
import { unlink } from "node:fs/promises";
|
|
8
|
+
import { createServer, type Server } from "node:http";
|
|
9
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
|
+
import {
|
|
11
|
+
closeDb,
|
|
12
|
+
createAgent,
|
|
13
|
+
createMcpServer,
|
|
14
|
+
getAgentMcpServers,
|
|
15
|
+
getMcpServerById,
|
|
16
|
+
initDb,
|
|
17
|
+
listMcpServers,
|
|
18
|
+
} from "../be/db";
|
|
19
|
+
import { handleMcpServers } from "../http/mcp-servers";
|
|
20
|
+
import { registerMcpServerCreateTool, registerMcpServerUpdateTool } from "../tools/mcp-servers";
|
|
21
|
+
import { interleave } from "./write-gate";
|
|
22
|
+
|
|
23
|
+
const TEST_DB_PATH = "./test-mcp-server-stdio-gate.sqlite";
|
|
24
|
+
|
|
25
|
+
const LEAD_ID = "aaaa2000-0000-4000-8000-000000000001";
|
|
26
|
+
const WORKER_ID = "bbbb2000-0000-4000-8000-000000000002";
|
|
27
|
+
|
|
28
|
+
type ToolResult = {
|
|
29
|
+
content: Array<{ type: string; text: string }>;
|
|
30
|
+
structuredContent: { success: boolean; message: string; [key: string]: unknown };
|
|
31
|
+
isError: boolean;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
let mcp: McpServer;
|
|
35
|
+
let routeServer: Server;
|
|
36
|
+
let routeBaseUrl: string;
|
|
37
|
+
|
|
38
|
+
async function callTool(
|
|
39
|
+
name: string,
|
|
40
|
+
callerAgentId: string,
|
|
41
|
+
args: Record<string, unknown>,
|
|
42
|
+
): Promise<ToolResult> {
|
|
43
|
+
// biome-ignore lint/complexity/noBannedTypes: accessing internal MCP SDK type for test
|
|
44
|
+
const tools = (mcp as unknown as { _registeredTools: Record<string, { handler: Function }> })
|
|
45
|
+
._registeredTools;
|
|
46
|
+
const handler = tools[name]?.handler;
|
|
47
|
+
if (!handler) throw new Error(`Tool not registered: ${name}`);
|
|
48
|
+
const extra = {
|
|
49
|
+
sessionId: "test-session",
|
|
50
|
+
requestInfo: { headers: { "x-agent-id": callerAgentId } },
|
|
51
|
+
};
|
|
52
|
+
return (await handler(args, extra)) as ToolResult;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function putServer(
|
|
56
|
+
id: string,
|
|
57
|
+
agentId: string,
|
|
58
|
+
body: Record<string, unknown>,
|
|
59
|
+
): Promise<{ status: number; body: unknown }> {
|
|
60
|
+
const response = await fetch(`${routeBaseUrl}/api/mcp-servers/${id}`, {
|
|
61
|
+
method: "PUT",
|
|
62
|
+
headers: { "X-Agent-ID": agentId, "Content-Type": "application/json" },
|
|
63
|
+
body: JSON.stringify(body),
|
|
64
|
+
});
|
|
65
|
+
return { status: response.status, body: await response.json() };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function postServer(
|
|
69
|
+
agentId: string,
|
|
70
|
+
body: Record<string, unknown>,
|
|
71
|
+
): Promise<{ status: number; body: unknown }> {
|
|
72
|
+
const response = await fetch(`${routeBaseUrl}/api/mcp-servers`, {
|
|
73
|
+
method: "POST",
|
|
74
|
+
headers: { "X-Agent-ID": agentId, "Content-Type": "application/json" },
|
|
75
|
+
body: JSON.stringify(body),
|
|
76
|
+
});
|
|
77
|
+
return { status: response.status, body: await response.json() };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function removeDbFiles(): Promise<void> {
|
|
81
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
82
|
+
try {
|
|
83
|
+
await unlink(TEST_DB_PATH + suffix);
|
|
84
|
+
} catch {
|
|
85
|
+
// not there
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const DENIED = "Only lead agents can create or change stdio MCP servers.";
|
|
91
|
+
|
|
92
|
+
beforeAll(async () => {
|
|
93
|
+
await removeDbFiles();
|
|
94
|
+
closeDb();
|
|
95
|
+
initDb(TEST_DB_PATH);
|
|
96
|
+
await createAgent({ id: LEAD_ID, name: "Stdio Gate Lead", isLead: true, status: "idle" });
|
|
97
|
+
await createAgent({ id: WORKER_ID, name: "Stdio Gate Worker", isLead: false, status: "idle" });
|
|
98
|
+
|
|
99
|
+
mcp = new McpServer({ name: "test-mcp-server-stdio-gate", version: "1.0.0" });
|
|
100
|
+
registerMcpServerCreateTool(mcp);
|
|
101
|
+
registerMcpServerUpdateTool(mcp);
|
|
102
|
+
|
|
103
|
+
routeServer = createServer(async (req, res) => {
|
|
104
|
+
const url = req.url ?? "/";
|
|
105
|
+
const pathEnd = url.indexOf("?");
|
|
106
|
+
const path = pathEnd === -1 ? url : url.slice(0, pathEnd);
|
|
107
|
+
const handled = await handleMcpServers(
|
|
108
|
+
req,
|
|
109
|
+
res,
|
|
110
|
+
path.split("/").filter(Boolean),
|
|
111
|
+
new URLSearchParams(pathEnd === -1 ? "" : url.slice(pathEnd + 1)),
|
|
112
|
+
);
|
|
113
|
+
if (!handled) res.writeHead(404).end();
|
|
114
|
+
});
|
|
115
|
+
await new Promise<void>((resolve) => routeServer.listen(0, "127.0.0.1", resolve));
|
|
116
|
+
const address = routeServer.address();
|
|
117
|
+
if (!address || typeof address === "string") throw new Error("No TCP address");
|
|
118
|
+
routeBaseUrl = `http://127.0.0.1:${address.port}`;
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
afterAll(async () => {
|
|
122
|
+
await new Promise<void>((resolve, reject) =>
|
|
123
|
+
routeServer.close((error) => (error ? reject(error) : resolve())),
|
|
124
|
+
);
|
|
125
|
+
closeDb();
|
|
126
|
+
await removeDbFiles();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("mcp-server-create: agent-scope stdio", () => {
|
|
130
|
+
test("a worker cannot create an agent-scope stdio server, and nothing is stored or installed", async () => {
|
|
131
|
+
const result = await callTool("mcp-server-create", WORKER_ID, {
|
|
132
|
+
name: "gate-worker-stdio",
|
|
133
|
+
transport: "stdio",
|
|
134
|
+
command: "sh",
|
|
135
|
+
args: JSON.stringify(["-c", "true"]),
|
|
136
|
+
scope: "agent",
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
expect(result.isError).toBe(true);
|
|
140
|
+
expect(result.structuredContent.message).toBe(DENIED);
|
|
141
|
+
expect(await listMcpServers({ search: "gate-worker-stdio" })).toHaveLength(0);
|
|
142
|
+
expect((await getAgentMcpServers(WORKER_ID)).map((s) => s.name)).not.toContain(
|
|
143
|
+
"gate-worker-stdio",
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("scope defaults to agent, so omitting it does not bypass the gate", async () => {
|
|
148
|
+
const result = await callTool("mcp-server-create", WORKER_ID, {
|
|
149
|
+
name: "gate-worker-stdio-default-scope",
|
|
150
|
+
transport: "stdio",
|
|
151
|
+
command: "sh",
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
expect(result.isError).toBe(true);
|
|
155
|
+
expect(result.structuredContent.message).toBe(DENIED);
|
|
156
|
+
expect(await listMcpServers({ search: "gate-worker-stdio-default-scope" })).toHaveLength(0);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("a worker can still create agent-scope http and sse servers", async () => {
|
|
160
|
+
for (const transport of ["http", "sse"] as const) {
|
|
161
|
+
const result = await callTool("mcp-server-create", WORKER_ID, {
|
|
162
|
+
name: `gate-worker-${transport}`,
|
|
163
|
+
transport,
|
|
164
|
+
url: "https://mcp.example.com/endpoint",
|
|
165
|
+
scope: "agent",
|
|
166
|
+
});
|
|
167
|
+
expect(result.isError, `${transport}: ${result.structuredContent.message}`).toBe(false);
|
|
168
|
+
expect(result.structuredContent.message).toContain("Created and installed MCP server");
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("a lead can create an agent-scope stdio server", async () => {
|
|
173
|
+
const result = await callTool("mcp-server-create", LEAD_ID, {
|
|
174
|
+
name: "gate-lead-stdio",
|
|
175
|
+
transport: "stdio",
|
|
176
|
+
command: "echo",
|
|
177
|
+
scope: "agent",
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
expect(result.isError).toBe(false);
|
|
181
|
+
expect(result.structuredContent.message).toContain("Created and installed MCP server");
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe("mcp-server-update: reaching stdio through an update", () => {
|
|
186
|
+
test("an owner cannot turn its own http server into a stdio server", async () => {
|
|
187
|
+
const created = await createMcpServer({
|
|
188
|
+
name: "gate-update-http-to-stdio",
|
|
189
|
+
transport: "http",
|
|
190
|
+
url: "https://mcp.example.com/endpoint",
|
|
191
|
+
scope: "agent",
|
|
192
|
+
ownerAgentId: WORKER_ID,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const result = await callTool("mcp-server-update", WORKER_ID, {
|
|
196
|
+
id: created.id,
|
|
197
|
+
transport: "stdio",
|
|
198
|
+
command: "sh",
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
expect(result.isError).toBe(true);
|
|
202
|
+
expect(result.structuredContent.message).toBe(DENIED);
|
|
203
|
+
const after = await getMcpServerById(created.id);
|
|
204
|
+
expect(after?.transport).toBe("http");
|
|
205
|
+
expect(after?.command).toBeNull();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("an owner cannot change the command, args or env of a stdio server it owns", async () => {
|
|
209
|
+
const created = await createMcpServer({
|
|
210
|
+
name: "gate-update-stdio-command",
|
|
211
|
+
transport: "stdio",
|
|
212
|
+
command: "echo",
|
|
213
|
+
scope: "agent",
|
|
214
|
+
ownerAgentId: WORKER_ID,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
for (const change of [
|
|
218
|
+
{ command: "sh" },
|
|
219
|
+
{ args: JSON.stringify(["-c", "true"]) },
|
|
220
|
+
{ envConfigKeys: JSON.stringify({ NODE_OPTIONS: "some-config-key" }) },
|
|
221
|
+
]) {
|
|
222
|
+
const result = await callTool("mcp-server-update", WORKER_ID, { id: created.id, ...change });
|
|
223
|
+
expect(result.isError, JSON.stringify(change)).toBe(true);
|
|
224
|
+
expect(result.structuredContent.message).toBe(DENIED);
|
|
225
|
+
}
|
|
226
|
+
expect((await getMcpServerById(created.id))?.command).toBe("echo");
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("an owner can still rename, describe and toggle a stdio server, and edit an http one", async () => {
|
|
230
|
+
const stdio = await createMcpServer({
|
|
231
|
+
name: "gate-update-stdio-benign",
|
|
232
|
+
transport: "stdio",
|
|
233
|
+
command: "echo",
|
|
234
|
+
scope: "agent",
|
|
235
|
+
ownerAgentId: WORKER_ID,
|
|
236
|
+
});
|
|
237
|
+
const renamed = await callTool("mcp-server-update", WORKER_ID, {
|
|
238
|
+
id: stdio.id,
|
|
239
|
+
description: "new description",
|
|
240
|
+
isEnabled: false,
|
|
241
|
+
});
|
|
242
|
+
expect(renamed.isError).toBe(false);
|
|
243
|
+
|
|
244
|
+
const http = await createMcpServer({
|
|
245
|
+
name: "gate-update-http-benign",
|
|
246
|
+
transport: "http",
|
|
247
|
+
url: "https://mcp.example.com/endpoint",
|
|
248
|
+
scope: "agent",
|
|
249
|
+
ownerAgentId: WORKER_ID,
|
|
250
|
+
});
|
|
251
|
+
const retargeted = await callTool("mcp-server-update", WORKER_ID, {
|
|
252
|
+
id: http.id,
|
|
253
|
+
url: "https://mcp.example.com/other",
|
|
254
|
+
});
|
|
255
|
+
expect(retargeted.isError).toBe(false);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("an owner cannot switch a stdio server back on, but can resubmit unchanged values", async () => {
|
|
259
|
+
const created = await createMcpServer({
|
|
260
|
+
name: "gate-update-reenable",
|
|
261
|
+
transport: "stdio",
|
|
262
|
+
command: "echo",
|
|
263
|
+
scope: "agent",
|
|
264
|
+
ownerAgentId: WORKER_ID,
|
|
265
|
+
});
|
|
266
|
+
const disabled = await callTool("mcp-server-update", LEAD_ID, {
|
|
267
|
+
id: created.id,
|
|
268
|
+
isEnabled: false,
|
|
269
|
+
});
|
|
270
|
+
expect(disabled.isError).toBe(false);
|
|
271
|
+
|
|
272
|
+
const reenable = await callTool("mcp-server-update", WORKER_ID, {
|
|
273
|
+
id: created.id,
|
|
274
|
+
isEnabled: true,
|
|
275
|
+
});
|
|
276
|
+
expect(reenable.isError).toBe(true);
|
|
277
|
+
expect(reenable.structuredContent.message).toBe(DENIED);
|
|
278
|
+
expect((await getMcpServerById(created.id))?.isEnabled).toBe(false);
|
|
279
|
+
|
|
280
|
+
const unchanged = await callTool("mcp-server-update", WORKER_ID, {
|
|
281
|
+
id: created.id,
|
|
282
|
+
transport: "stdio",
|
|
283
|
+
command: "echo",
|
|
284
|
+
description: "same command, new text",
|
|
285
|
+
});
|
|
286
|
+
expect(unchanged.isError).toBe(false);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("a lead can change the command of a worker's stdio server", async () => {
|
|
290
|
+
const created = await createMcpServer({
|
|
291
|
+
name: "gate-update-lead",
|
|
292
|
+
transport: "stdio",
|
|
293
|
+
command: "echo",
|
|
294
|
+
scope: "agent",
|
|
295
|
+
ownerAgentId: WORKER_ID,
|
|
296
|
+
});
|
|
297
|
+
const result = await callTool("mcp-server-update", LEAD_ID, { id: created.id, command: "ls" });
|
|
298
|
+
expect(result.isError).toBe(false);
|
|
299
|
+
expect((await getMcpServerById(created.id))?.command).toBe("ls");
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
describe("HTTP routes with an X-Agent-ID", () => {
|
|
304
|
+
test("POST /api/mcp-servers keeps refusing a worker's stdio server", async () => {
|
|
305
|
+
const result = await postServer(WORKER_ID, {
|
|
306
|
+
name: "gate-http-create-stdio",
|
|
307
|
+
transport: "stdio",
|
|
308
|
+
command: "sh",
|
|
309
|
+
scope: "agent",
|
|
310
|
+
ownerAgentId: WORKER_ID,
|
|
311
|
+
});
|
|
312
|
+
expect(result.status).toBe(403);
|
|
313
|
+
expect(await listMcpServers({ search: "gate-http-create-stdio" })).toHaveLength(0);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test("PUT /api/mcp-servers/:id refuses an owner's command change, and allows a lead's", async () => {
|
|
317
|
+
const created = await createMcpServer({
|
|
318
|
+
name: "gate-http-update-stdio",
|
|
319
|
+
transport: "stdio",
|
|
320
|
+
command: "echo",
|
|
321
|
+
scope: "agent",
|
|
322
|
+
ownerAgentId: WORKER_ID,
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const denied = await putServer(created.id, WORKER_ID, { command: "sh" });
|
|
326
|
+
expect(denied.status).toBe(403);
|
|
327
|
+
expect((await getMcpServerById(created.id))?.command).toBe("echo");
|
|
328
|
+
|
|
329
|
+
const toStdio = await createMcpServer({
|
|
330
|
+
name: "gate-http-update-http-to-stdio",
|
|
331
|
+
transport: "http",
|
|
332
|
+
url: "https://mcp.example.com/endpoint",
|
|
333
|
+
scope: "agent",
|
|
334
|
+
ownerAgentId: WORKER_ID,
|
|
335
|
+
});
|
|
336
|
+
const deniedSwitch = await putServer(toStdio.id, WORKER_ID, {
|
|
337
|
+
transport: "stdio",
|
|
338
|
+
command: "sh",
|
|
339
|
+
});
|
|
340
|
+
expect(deniedSwitch.status).toBe(403);
|
|
341
|
+
expect((await getMcpServerById(toStdio.id))?.transport).toBe("http");
|
|
342
|
+
|
|
343
|
+
const allowed = await putServer(created.id, LEAD_ID, { command: "ls" });
|
|
344
|
+
expect(allowed.status).toBe(200);
|
|
345
|
+
expect((await getMcpServerById(created.id))?.command).toBe("ls");
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test("PUT /api/mcp-servers/:id still lets an owner edit non-executable fields", async () => {
|
|
349
|
+
const created = await createMcpServer({
|
|
350
|
+
name: "gate-http-update-benign",
|
|
351
|
+
transport: "stdio",
|
|
352
|
+
command: "echo",
|
|
353
|
+
scope: "agent",
|
|
354
|
+
ownerAgentId: WORKER_ID,
|
|
355
|
+
});
|
|
356
|
+
const result = await putServer(created.id, WORKER_ID, { description: "edited by owner" });
|
|
357
|
+
expect(result.status).toBe(200);
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* An update authorizes against the server as stored, so a concurrent edit that turns the server
|
|
363
|
+
* into a stdio one must not let a decision taken before it land after it. Each case holds one
|
|
364
|
+
* request just before its write and commits the other meanwhile. The assertions hold for either
|
|
365
|
+
* serial order: an http server's command is inert until a lead makes it stdio, and the lead's
|
|
366
|
+
* own command then wins.
|
|
367
|
+
*/
|
|
368
|
+
describe("concurrent updates keep the stdio gate", () => {
|
|
369
|
+
const UPDATE = /^UPDATE mcp_servers/;
|
|
370
|
+
const leadMakesStdio = { transport: "stdio", command: "vetted-command" };
|
|
371
|
+
const ownerSetsCommand = { command: "owner-command" };
|
|
372
|
+
|
|
373
|
+
const newHttpServer = (name: string) =>
|
|
374
|
+
createMcpServer({
|
|
375
|
+
name,
|
|
376
|
+
transport: "http",
|
|
377
|
+
url: "https://mcp.example.com/endpoint",
|
|
378
|
+
scope: "agent",
|
|
379
|
+
ownerAgentId: WORKER_ID,
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
async function expectLeadCommandOnly(id: string) {
|
|
383
|
+
const after = await getMcpServerById(id);
|
|
384
|
+
expect(after?.transport).toBe("stdio");
|
|
385
|
+
expect(after?.command).toBe("vetted-command");
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
test("mcp-server-update: an owner decision taken before a lead's switch to stdio cannot write after it", async () => {
|
|
389
|
+
const server = await newHttpServer("race-tool-stale-owner");
|
|
390
|
+
const { second } = await interleave(
|
|
391
|
+
UPDATE,
|
|
392
|
+
() => callTool("mcp-server-update", WORKER_ID, { id: server.id, ...ownerSetsCommand }),
|
|
393
|
+
() => callTool("mcp-server-update", LEAD_ID, { id: server.id, ...leadMakesStdio }),
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
expect(second.isError).toBe(false);
|
|
397
|
+
await expectLeadCommandOnly(server.id);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
test("mcp-server-update: an owner update queued behind a lead's switch to stdio is refused", async () => {
|
|
401
|
+
const server = await newHttpServer("race-tool-queued-owner");
|
|
402
|
+
const { first, second } = await interleave(
|
|
403
|
+
UPDATE,
|
|
404
|
+
() => callTool("mcp-server-update", LEAD_ID, { id: server.id, ...leadMakesStdio }),
|
|
405
|
+
() => callTool("mcp-server-update", WORKER_ID, { id: server.id, ...ownerSetsCommand }),
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
expect(first.isError).toBe(false);
|
|
409
|
+
expect(second.isError).toBe(true);
|
|
410
|
+
expect(second.structuredContent.message).toBe(DENIED);
|
|
411
|
+
await expectLeadCommandOnly(server.id);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test("PUT /api/mcp-servers/:id: an owner decision taken before a lead's switch to stdio cannot write after it", async () => {
|
|
415
|
+
const server = await newHttpServer("race-http-stale-owner");
|
|
416
|
+
const { second } = await interleave(
|
|
417
|
+
UPDATE,
|
|
418
|
+
() => putServer(server.id, WORKER_ID, ownerSetsCommand),
|
|
419
|
+
() => putServer(server.id, LEAD_ID, leadMakesStdio),
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
expect(second.status).toBe(200);
|
|
423
|
+
await expectLeadCommandOnly(server.id);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
test("PUT /api/mcp-servers/:id: an owner update queued behind a lead's switch to stdio is refused", async () => {
|
|
427
|
+
const server = await newHttpServer("race-http-queued-owner");
|
|
428
|
+
const { first, second } = await interleave(
|
|
429
|
+
UPDATE,
|
|
430
|
+
() => putServer(server.id, LEAD_ID, leadMakesStdio),
|
|
431
|
+
() => putServer(server.id, WORKER_ID, ownerSetsCommand),
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
expect(first.status).toBe(200);
|
|
435
|
+
expect(second.status).toBe(403);
|
|
436
|
+
await expectLeadCommandOnly(server.id);
|
|
437
|
+
});
|
|
438
|
+
});
|