@desplega.ai/agent-swarm 1.151.0 → 1.152.1
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 +3 -1
- package/dist/{acp-adapter-ex8hzjfc.js → acp-adapter-4s7q25ft.js} +1 -1
- package/dist/{actions-2bgpyhww.js → actions-cs6rhmvc.js} +13 -13
- package/dist/{app-1706zhr1.js → app-j54aq0dj.js} +3 -3
- package/dist/{assistant-bavke5p7.js → assistant-w1pgn7vf.js} +14 -14
- package/dist/{boot-reembed-3cmrrmpa.js → boot-reembed-42vgkrrj.js} +4 -4
- package/dist/{boot-reembed-nafcmmmc.js → boot-reembed-smy30z32.js} +3 -3
- package/dist/{boot-scrub-logs-cd6a2sq0.js → boot-scrub-logs-b6s72fsw.js} +2 -2
- package/dist/{claude-adapter-wfp2gzvz.js → claude-adapter-fh7rmcj9.js} +3 -3
- package/dist/{claude-sdk-session-bht9jjak.js → claude-sdk-session-xhynatn9.js} +3 -3
- package/dist/{cli-f1wky6zp.js → cli-3g61md6v.js} +1 -1
- package/dist/{cli-a1z3h107.js → cli-3pft2z6r.js} +4 -4
- package/dist/{cli-5tpgq60f.js → cli-72tgtzr1.js} +152 -23
- package/dist/{cli-0qb1z6s2.js → cli-7bwaerjt.js} +1 -1
- package/dist/{cli-czjtgfys.js → cli-b4dyetqt.js} +2 -2
- package/dist/{cli-s14agm5s.js → cli-bdt7w021.js} +12 -3
- package/dist/{cli-sjaae6ff.js → cli-dsyg66vt.js} +3 -3
- package/dist/{cli-2tbvf1z1.js → cli-ftdjz1mk.js} +8 -8
- package/dist/{cli-zkxymas5.js → cli-gfez0qmk.js} +1 -1
- package/dist/{cli-q88st9aj.js → cli-gxf2ebyz.js} +2 -2
- package/dist/{cli-ppm4975n.js → cli-jtvs5m8q.js} +1 -1
- package/dist/{cli-7tgcstn2.js → cli-jytf5mw9.js} +2 -2
- package/dist/{cli-g69fdc9g.js → cli-kf8qp2vf.js} +2 -2
- package/dist/{cli-5jd2z7av.js → cli-ks8a1cgq.js} +6 -2
- package/dist/{cli-gjyvhmmh.js → cli-ng4q6a9t.js} +5 -5
- package/dist/{cli-3v9xhjcn.js → cli-sakxd2rg.js} +2 -2
- package/dist/{cli-frcta2w4.js → cli-sqmh7jj1.js} +1 -1
- package/dist/{cli-vwm416m6.js → cli-v9sk4dzw.js} +1 -1
- package/dist/{cli-j9xczhaq.js → cli-vbk025fm.js} +1 -1
- package/dist/{cli-r2q6pevn.js → cli-wfve107r.js} +43 -21
- package/dist/{cli-9004vfdf.js → cli-y8tayczk.js} +2 -2
- package/dist/{cli-gbfwcd3k.js → cli-zfzznn7c.js} +7 -6
- package/dist/cli.js +11 -11
- package/dist/{codex-adapter-jjjk8w4m.js → codex-adapter-k6yq95sn.js} +2 -2
- package/dist/{codex-session-runner-nd6ayxkx.js → codex-session-runner-3rp84pk8.js} +2 -2
- package/dist/{commands-9v4psyq1.js → commands-pbr5jzy5.js} +2 -2
- package/dist/{db-wnt58tpe.js → db-z0xaa59a.js} +2 -2
- package/dist/{dispatcher-05jh1aqk.js → dispatcher-pwpgsj0x.js} +12 -12
- package/dist/{handlers-tw90pkh5.js → handlers-5mzk4mbg.js} +15 -15
- package/dist/{hook-sxe3r349.js → hook-8bz33zee.js} +1 -1
- package/dist/{hook-123e2scb.js → hook-kbh84zb1.js} +2 -2
- package/dist/{http-ecsar8ez.js → http-pppxg5vr.js} +100 -36
- package/dist/{index-hw26vj40.js → index-68bd9b5f.js} +12 -12
- package/dist/{index-16js521z.js → index-k1hq1qt6.js} +8 -8
- package/dist/{index-zj518gf9.js → index-mjpxsr6k.js} +13 -13
- package/dist/{index-ymn7em0p.js → index-y0te09pn.js} +3 -3
- package/dist/{index-nnrbpcjp.js → index-z0zwjtkn.js} +12 -12
- package/dist/{keepalive-asqtcm4y.js → keepalive-xvmwnh1d.js} +4 -4
- package/dist/{lead-xbfr7fvt.js → lead-pf8nvxt8.js} +16 -16
- package/dist/{maintenance-qma55qvw.js → maintenance-jk4q0wgg.js} +4 -4
- package/dist/{oauth-refresh-sweep-3s04c6qw.js → oauth-refresh-sweep-6207x9sj.js} +3 -3
- package/dist/{onboard-drskd386.js → onboard-b0csgm0q.js} +2 -2
- package/dist/{otel-impl-943c96er.js → otel-impl-b6rfehth.js} +1 -1
- package/dist/{pricing-refresh-mx51az05.js → pricing-refresh-4e201wtd.js} +4 -4
- package/dist/{rbac-roles-wcmrg6sv.js → rbac-roles-mkc5n5qa.js} +4 -4
- package/dist/{rbac-roles-78baecqd.js → rbac-roles-zp6e4geh.js} +3 -3
- package/dist/{render-v2-8zjntgf5.js → render-v2-4fazz9tf.js} +3 -3
- package/dist/{seed-pricing-jqjs6cws.js → seed-pricing-thxpn9vn.js} +3 -3
- package/dist/{setup-4pks0vfk.js → setup-wb0z8cat.js} +2 -2
- package/dist/{work-objects-wangt792.js → work-objects-h3498kwe.js} +18 -17
- package/dist/{worker-p2mr422x.js → worker-2m2tzxp3.js} +16 -16
- package/openapi.json +5 -5
- package/package.json +1 -1
- package/src/agentmail/inbound-archive.ts +71 -0
- package/src/agentmail/types.ts +2 -0
- package/src/be/db.ts +1 -1
- package/src/be/scripts/typecheck.ts +4 -0
- package/src/be/swarm-config-guard.ts +7 -1
- package/src/extensions/lifecycle.ts +26 -4
- package/src/http/extensions.ts +8 -3
- package/src/http/webhooks.ts +13 -5
- package/src/rbac/legacy-policy.ts +13 -1
- package/src/scripts-runtime/sdk-allowlist.ts +4 -0
- package/src/scripts-runtime/types/stdlib.d.ts +4 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +4 -0
- package/src/server.ts +12 -0
- package/src/slack/render-v2.ts +10 -4
- package/src/slack/responses.ts +7 -4
- 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/tests/agentmail-inbound-archive.test.ts +418 -0
- package/src/tests/extensions-example-mcp-install.test.ts +2 -2
- package/src/tests/extensions-http.test.ts +84 -3
- package/src/tests/extensions-lead-equivalence.test.ts +21 -0
- package/src/tests/extensions-mcp-tools.test.ts +111 -3
- package/src/tests/fixtures/store-progress-malformed-output.json +4 -0
- package/src/tests/rbac-admission-e2e.test.ts +35 -4
- package/src/tests/rbac-e2e-helpers.ts +18 -0
- package/src/tests/rbac-engine.test.ts +2 -2
- package/src/tests/rbac-mcp-admission-e2e.test.ts +22 -4
- package/src/tests/reload-config.test.ts +17 -0
- package/src/tests/scripts-only-gating.test.ts +4 -0
- package/src/tests/slack-render-v2.test.ts +24 -2
- package/src/tests/slack-task-output.test.ts +36 -0
- 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/tool-annotations.test.ts +1 -1
- package/src/tools/extension-activate-version.ts +38 -0
- package/src/tools/extension-common.ts +2 -1
- 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 +2 -2
- package/src/tools/store-progress.ts +30 -0
- package/src/tools/tool-config.ts +5 -1
- package/templates/skills/agent-browser/SKILL.md +84 -0
- package/templates/skills/agent-browser/content.md +21 -0
|
@@ -4,11 +4,16 @@ import type { IncomingMessage, ServerResponse } from "node:http";
|
|
|
4
4
|
import { Readable } from "node:stream";
|
|
5
5
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
6
|
import { closeDb, createAgent, getDbClient, initDb } from "../be/db";
|
|
7
|
-
import { setExtensionState } from "../be/extensions/db";
|
|
7
|
+
import { getExtensionById, setExtensionState } from "../be/extensions/db";
|
|
8
8
|
import { typecheckScript } from "../be/scripts/typecheck";
|
|
9
|
+
import { stopExtensionRuntime } from "../extensions/lifecycle";
|
|
9
10
|
import { handleCore } from "../http/core";
|
|
10
11
|
import { handleExtensions } from "../http/extensions";
|
|
11
12
|
import { getPathSegments, parseQueryParams } from "../http/utils";
|
|
13
|
+
import { registerExtensionActivateVersionTool } from "../tools/extension-activate-version";
|
|
14
|
+
import { registerExtensionDeleteTool } from "../tools/extension-delete";
|
|
15
|
+
import { registerExtensionDisableTool } from "../tools/extension-disable";
|
|
16
|
+
import { registerExtensionEnableTool } from "../tools/extension-enable";
|
|
12
17
|
import { registerExtensionInstallTool } from "../tools/extension-install";
|
|
13
18
|
import { registerExtensionListTool } from "../tools/extension-list";
|
|
14
19
|
import { refreshSecretScrubberCache } from "../utils/secret-scrubber";
|
|
@@ -40,11 +45,22 @@ async function removeDbFiles(): Promise<void> {
|
|
|
40
45
|
|
|
41
46
|
function buildToolServer() {
|
|
42
47
|
const server = new McpServer({ name: "extensions-mcp-tools", version: "1" });
|
|
48
|
+
registerExtensionDeleteTool(server);
|
|
49
|
+
registerExtensionEnableTool(server);
|
|
50
|
+
registerExtensionDisableTool(server);
|
|
51
|
+
registerExtensionActivateVersionTool(server);
|
|
43
52
|
registerExtensionInstallTool(server);
|
|
44
53
|
registerExtensionListTool(server);
|
|
45
54
|
const tools = (server as unknown as { _registeredTools: Record<string, RegisteredTool> })
|
|
46
55
|
._registeredTools;
|
|
47
|
-
return {
|
|
56
|
+
return {
|
|
57
|
+
install: tools["extension-install"]!,
|
|
58
|
+
list: tools["extension-list"]!,
|
|
59
|
+
enable: tools["extension-enable"]!,
|
|
60
|
+
disable: tools["extension-disable"]!,
|
|
61
|
+
activate: tools["extension-activate-version"]!,
|
|
62
|
+
delete: tools["extension-delete"]!,
|
|
63
|
+
};
|
|
48
64
|
}
|
|
49
65
|
|
|
50
66
|
function meta(agentId: string) {
|
|
@@ -128,6 +144,7 @@ beforeAll(async () => {
|
|
|
128
144
|
});
|
|
129
145
|
|
|
130
146
|
afterAll(async () => {
|
|
147
|
+
await stopExtensionRuntime();
|
|
131
148
|
globalThis.fetch = savedFetch;
|
|
132
149
|
closeDb();
|
|
133
150
|
await removeDbFiles();
|
|
@@ -142,11 +159,12 @@ afterAll(async () => {
|
|
|
142
159
|
});
|
|
143
160
|
|
|
144
161
|
beforeEach(async () => {
|
|
162
|
+
await stopExtensionRuntime();
|
|
145
163
|
await getDbClient().run("DELETE FROM extensions");
|
|
146
164
|
});
|
|
147
165
|
|
|
148
166
|
describe("extension MCP HTTP proxy tools", () => {
|
|
149
|
-
test("script SDK typechecks
|
|
167
|
+
test("script SDK typechecks the full extension lifecycle", async () => {
|
|
150
168
|
const result = await typecheckScript(`
|
|
151
169
|
import type { ScriptContext } from "swarm-sdk";
|
|
152
170
|
export default async (_args: unknown, ctx: ScriptContext) => {
|
|
@@ -154,6 +172,11 @@ describe("extension MCP HTTP proxy tools", () => {
|
|
|
154
172
|
manifest: {},
|
|
155
173
|
files: { "hooks.ts": "export default () => {}" },
|
|
156
174
|
});
|
|
175
|
+
const id = "00000000-0000-4000-8000-000000000001";
|
|
176
|
+
await ctx.swarm.extension_enable({ id });
|
|
177
|
+
await ctx.swarm.extension_activate_version({ id, version: 1 });
|
|
178
|
+
await ctx.swarm.extension_disable({ id });
|
|
179
|
+
await ctx.swarm.extension_delete({ id });
|
|
157
180
|
return await ctx.swarm.extension_list({ enabledOnly: true });
|
|
158
181
|
};
|
|
159
182
|
`);
|
|
@@ -243,3 +266,88 @@ describe("extension MCP HTTP proxy tools", () => {
|
|
|
243
266
|
expect(enabledOnly.structuredContent).toMatchObject({ success: true, extensions: [] });
|
|
244
267
|
});
|
|
245
268
|
});
|
|
269
|
+
|
|
270
|
+
describe("extension lifecycle MCP tools", () => {
|
|
271
|
+
test("lead enables, activates, disables, and deletes through the shared HTTP routes", async () => {
|
|
272
|
+
const tools = buildToolServer();
|
|
273
|
+
const installed = (await tools.install.handler(
|
|
274
|
+
await loadBundleFixture("minimal"),
|
|
275
|
+
meta(leadId),
|
|
276
|
+
)) as ToolResult;
|
|
277
|
+
const id = String(installed.structuredContent.id);
|
|
278
|
+
const enabled = (await tools.enable.handler({ id }, meta(leadId))) as ToolResult;
|
|
279
|
+
expect(enabled.structuredContent).toMatchObject({
|
|
280
|
+
success: true,
|
|
281
|
+
enabled: true,
|
|
282
|
+
activeVersion: 1,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const rejected = await dispatchExtensionsApi(`${API_URL}/api/extensions/${id}`, {
|
|
286
|
+
method: "DELETE",
|
|
287
|
+
headers: { "x-agent-id": leadId, authorization: `Bearer ${API_KEY}` },
|
|
288
|
+
});
|
|
289
|
+
expect(rejected.status).toBe(409);
|
|
290
|
+
const blocked = (await tools.delete.handler({ id }, meta(leadId))) as ToolResult;
|
|
291
|
+
expect(blocked.isError).toBe(true);
|
|
292
|
+
expect(blocked.structuredContent).toMatchObject({ success: false });
|
|
293
|
+
expect(blocked.content[0]?.text).toContain("Disable");
|
|
294
|
+
expect(await getExtensionById(id)).not.toBeNull();
|
|
295
|
+
|
|
296
|
+
const changed = await loadBundleFixture("minimal");
|
|
297
|
+
changed.files["hooks.ts"] += "\n// next lifecycle version\n";
|
|
298
|
+
await tools.install.handler(changed, meta(leadId));
|
|
299
|
+
const activated = (await tools.activate.handler(
|
|
300
|
+
{ id, version: 2 },
|
|
301
|
+
meta(leadId),
|
|
302
|
+
)) as ToolResult;
|
|
303
|
+
expect(activated.structuredContent).toMatchObject({
|
|
304
|
+
success: true,
|
|
305
|
+
activeVersion: 2,
|
|
306
|
+
enabled: true,
|
|
307
|
+
});
|
|
308
|
+
const disabled = (await tools.disable.handler({ id }, meta(leadId))) as ToolResult;
|
|
309
|
+
expect(disabled.structuredContent).toMatchObject({ success: true, enabled: false });
|
|
310
|
+
const deleted = (await tools.delete.handler({ id }, meta(leadId))) as ToolResult;
|
|
311
|
+
expect(deleted.isError).toBe(false);
|
|
312
|
+
expect(deleted.structuredContent).toMatchObject({ success: true, id, deleted: true });
|
|
313
|
+
expect(await getExtensionById(id)).toBeNull();
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test("workers cannot mutate the extension lifecycle", async () => {
|
|
317
|
+
const tools = buildToolServer();
|
|
318
|
+
const installed = (await tools.install.handler(
|
|
319
|
+
await loadBundleFixture("minimal"),
|
|
320
|
+
meta(leadId),
|
|
321
|
+
)) as ToolResult;
|
|
322
|
+
const id = String(installed.structuredContent.id);
|
|
323
|
+
for (const tool of [tools.enable, tools.disable, tools.activate, tools.delete]) {
|
|
324
|
+
const result = (await tool.handler({ id, version: 1 }, meta(workerId))) as ToolResult;
|
|
325
|
+
expect(result.isError).toBe(true);
|
|
326
|
+
expect(String(result.structuredContent.message)).toContain("Forbidden");
|
|
327
|
+
}
|
|
328
|
+
expect(await getExtensionById(id)).toMatchObject({ enabled: false, activeVersion: 1 });
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test("missing extensions and versions return error envelopes", async () => {
|
|
332
|
+
const tools = buildToolServer();
|
|
333
|
+
const id = crypto.randomUUID();
|
|
334
|
+
for (const tool of [tools.enable, tools.disable, tools.activate, tools.delete]) {
|
|
335
|
+
const result = (await tool.handler({ id, version: 1 }, meta(leadId))) as ToolResult;
|
|
336
|
+
expect(result.isError).toBe(true);
|
|
337
|
+
expect(result.structuredContent.success).toBe(false);
|
|
338
|
+
}
|
|
339
|
+
const installed = (await tools.install.handler(
|
|
340
|
+
await loadBundleFixture("minimal"),
|
|
341
|
+
meta(leadId),
|
|
342
|
+
)) as ToolResult;
|
|
343
|
+
const result = (await tools.activate.handler(
|
|
344
|
+
{ id: installed.structuredContent.id, version: 99 },
|
|
345
|
+
meta(leadId),
|
|
346
|
+
)) as ToolResult;
|
|
347
|
+
expect(result.isError).toBe(true);
|
|
348
|
+
expect(await getExtensionById(String(installed.structuredContent.id))).toMatchObject({
|
|
349
|
+
activeVersion: 1,
|
|
350
|
+
enabled: false,
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
});
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
{
|
|
2
|
+
"sourceTaskId": "59811b57-05d4-47d1-9691-ad9013698a0a",
|
|
3
|
+
"output": "Replied in the thread: https://x.com/desplegalabs/status/2100595056939827590\n\nBuilt what Guli said nobody had shown and Jan asked to use: `jev-lead-qualifier`, a global swarm script anyone can drop into their own swarm, plus a live demo — https://api.desplega.agent-swarm.dev/p/556b11781dbb40bf9b1eb1b3315adae6\n\nIt scores ICP fit / intent / authority / urgency as calibrated Jev probabilities and abstains to human_review when genuinely unsure. Money shot: the student \"free trial\" lead reads 0.63 intent, 0.03 fit — eager, not a customer.\n\nOne cosmetic miss: X auto-prepends thread handles, so @ironcarbs and @GuliMoreno render twice. Not deleting a live post over it; say the word if you want it reposted clean.</output>\n<parameter name=\"attachments\">[{\"kind\": \"url\", \"url\": \"https://x.com/desplegalabs/status/2100595056939827590\", \"name\": \"X reply in the Jev thread\", \"description\": \"The @desplegalabs reply to @voidisomorphism tagging @ironcarbs and @GuliMoreno\", \"intent\": \"The deliverable Taras asked for: reply in that thread @ jan and guli\", \"isPrimary\": true}, {\"kind\": \"url\", \"url\": \"https://api.desplega.agent-swarm.dev/p/556b11781dbb40bf9b1eb1b3315adae6\", \"name\": \"Lead qualifier demo page\", \"description\": \"Public demo: 8 leads scored live, abstain band explained, copy-paste block to run it in your own swarm\", \"intent\": \"The demo Jan asked to see and the link the tweet points at\"}]"
|
|
4
|
+
}
|
|
@@ -13,13 +13,12 @@ import { join } from "node:path";
|
|
|
13
13
|
import {
|
|
14
14
|
api,
|
|
15
15
|
makeScratchDir,
|
|
16
|
-
readAuditRows,
|
|
17
16
|
registerAgent,
|
|
18
17
|
removeScratchDir,
|
|
19
18
|
type SwarmServer,
|
|
20
19
|
spawnSwarmServer,
|
|
21
20
|
WORKER_A,
|
|
22
|
-
|
|
21
|
+
waitForAuditRows,
|
|
23
22
|
} from "./rbac-e2e-helpers";
|
|
24
23
|
|
|
25
24
|
setDefaultTimeout(120_000);
|
|
@@ -358,8 +357,40 @@ describe("RBAC admission over real HTTP", () => {
|
|
|
358
357
|
});
|
|
359
358
|
expect(operatorCreate.status).toBe(201);
|
|
360
359
|
|
|
361
|
-
|
|
362
|
-
|
|
360
|
+
const userHttpRows = (
|
|
361
|
+
await waitForAuditRows(dbPath, (rows) => {
|
|
362
|
+
const userRows = rows.filter(
|
|
363
|
+
(row) =>
|
|
364
|
+
row.principalType === "user" &&
|
|
365
|
+
row.principalId === userId &&
|
|
366
|
+
row.source === "http" &&
|
|
367
|
+
row.resourceType === "http-route",
|
|
368
|
+
);
|
|
369
|
+
return (
|
|
370
|
+
[
|
|
371
|
+
"GET /api/agents/{id}/mcp-servers",
|
|
372
|
+
"GET /api/scripts/{id}/apis/{endpointId}/secret",
|
|
373
|
+
"PATCH /api/assets/app/{id}/key",
|
|
374
|
+
"POST /api/scripts/{id}/apis",
|
|
375
|
+
"POST /api/tasks",
|
|
376
|
+
].every((resourceId) =>
|
|
377
|
+
userRows.some((row) => row.resourceId === resourceId && row.decision === "deny"),
|
|
378
|
+
) &&
|
|
379
|
+
userRows.some(
|
|
380
|
+
(row) =>
|
|
381
|
+
row.resourceId === "PUT /api/favorites" &&
|
|
382
|
+
row.verb === "favorite.write.own" &&
|
|
383
|
+
row.decision === "allow",
|
|
384
|
+
) &&
|
|
385
|
+
userRows.some(
|
|
386
|
+
(row) =>
|
|
387
|
+
row.resourceId === "PATCH /api/assets/app/{id}/key" &&
|
|
388
|
+
row.verb === "app.manage" &&
|
|
389
|
+
row.decision === "allow",
|
|
390
|
+
)
|
|
391
|
+
);
|
|
392
|
+
})
|
|
393
|
+
).filter(
|
|
363
394
|
(row) =>
|
|
364
395
|
row.principalType === "user" &&
|
|
365
396
|
row.principalId === userId &&
|
|
@@ -314,3 +314,21 @@ export async function waitForAuditCount(
|
|
|
314
314
|
}
|
|
315
315
|
return n;
|
|
316
316
|
}
|
|
317
|
+
|
|
318
|
+
/** Poll until audit rows satisfy `predicate` (writer flushes every 2s). */
|
|
319
|
+
export async function waitForAuditRows(
|
|
320
|
+
dbPath: string,
|
|
321
|
+
predicate: (rows: AuditRow[]) => boolean,
|
|
322
|
+
deadlineMs = 8_000,
|
|
323
|
+
): Promise<AuditRow[]> {
|
|
324
|
+
const start = Date.now();
|
|
325
|
+
let rows = readAuditRows(dbPath);
|
|
326
|
+
while (!predicate(rows)) {
|
|
327
|
+
if (Date.now() - start >= deadlineMs) {
|
|
328
|
+
throw new Error(`audit rows did not satisfy predicate within ${deadlineMs}ms`);
|
|
329
|
+
}
|
|
330
|
+
await Bun.sleep(250);
|
|
331
|
+
rows = readAuditRows(dbPath);
|
|
332
|
+
}
|
|
333
|
+
return rows;
|
|
334
|
+
}
|
|
@@ -81,8 +81,8 @@ const LEAD_ONLY_VERBS: PermissionVerb[] = [
|
|
|
81
81
|
"script.api.delete",
|
|
82
82
|
];
|
|
83
83
|
|
|
84
|
-
const OPERATOR_ONLY_VERBS: PermissionVerb[] = [
|
|
85
|
-
const LEAD_OR_OPERATOR_VERBS: PermissionVerb[] = ["extension.write"];
|
|
84
|
+
const OPERATOR_ONLY_VERBS: PermissionVerb[] = [];
|
|
85
|
+
const LEAD_OR_OPERATOR_VERBS: PermissionVerb[] = ["extension.write", "extension.activate"];
|
|
86
86
|
|
|
87
87
|
const LEAD_OR_RESOURCE_OWNER_VERBS: PermissionVerb[] = [
|
|
88
88
|
"memory.edit.any",
|
|
@@ -12,12 +12,11 @@ import {
|
|
|
12
12
|
countAuditRows,
|
|
13
13
|
LEAD,
|
|
14
14
|
makeScratchDir,
|
|
15
|
-
readAuditRows,
|
|
16
15
|
registerAgent,
|
|
17
16
|
removeScratchDir,
|
|
18
17
|
type SwarmServer,
|
|
19
18
|
spawnSwarmServer,
|
|
20
|
-
|
|
19
|
+
waitForAuditRows,
|
|
21
20
|
} from "./rbac-e2e-helpers";
|
|
22
21
|
|
|
23
22
|
setDefaultTimeout(120_000);
|
|
@@ -312,8 +311,27 @@ describe("RBAC admission over /mcp-user", () => {
|
|
|
312
311
|
}),
|
|
313
312
|
);
|
|
314
313
|
|
|
315
|
-
|
|
316
|
-
|
|
314
|
+
const mcpRows = (
|
|
315
|
+
await waitForAuditRows(enabledDbPath, (rows) => {
|
|
316
|
+
const auditRows = rows.filter((row) => row.source === "mcp");
|
|
317
|
+
return (
|
|
318
|
+
auditRows.some(
|
|
319
|
+
(row) =>
|
|
320
|
+
row.principalId === requester.userId &&
|
|
321
|
+
row.resourceType === "mcp-tool" &&
|
|
322
|
+
row.resourceId === "send-task" &&
|
|
323
|
+
row.verb === "task.create.own" &&
|
|
324
|
+
row.decision === "allow",
|
|
325
|
+
) &&
|
|
326
|
+
["task.create.own", "task.cancel.own", "task.action.own"].every((verb) =>
|
|
327
|
+
auditRows.some(
|
|
328
|
+
(row) =>
|
|
329
|
+
row.principalId === empty.userId && row.decision === "deny" && row.verb === verb,
|
|
330
|
+
),
|
|
331
|
+
)
|
|
332
|
+
);
|
|
333
|
+
})
|
|
334
|
+
).filter((row) => row.source === "mcp");
|
|
317
335
|
expect(
|
|
318
336
|
mcpRows.some(
|
|
319
337
|
(row) =>
|
|
@@ -227,6 +227,23 @@ describe("reload-config", () => {
|
|
|
227
227
|
}
|
|
228
228
|
});
|
|
229
229
|
|
|
230
|
+
test("stored lead activation gate cannot override the deployment at boot or reload", async () => {
|
|
231
|
+
const key = "EXTENSION_ALLOW_LEAD_ACTIVATION";
|
|
232
|
+
const original = process.env[key];
|
|
233
|
+
const rowId = await insertLegacyReservedRow(key, "true");
|
|
234
|
+
try {
|
|
235
|
+
process.env[key] = "false";
|
|
236
|
+
for (const override of [false, true]) {
|
|
237
|
+
expect(await loadGlobalConfigsIntoEnv(override)).not.toContain(key);
|
|
238
|
+
expect(process.env[key]).toBe("false");
|
|
239
|
+
}
|
|
240
|
+
} finally {
|
|
241
|
+
await deleteSwarmConfig(rowId);
|
|
242
|
+
if (original === undefined) delete process.env[key];
|
|
243
|
+
else process.env[key] = original;
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
230
247
|
test("loadGlobalConfigsIntoEnv skips unreadable reserved secret rows before decrypting them", async () => {
|
|
231
248
|
const id = await insertUnreadableReservedSecretRow("SECRETS_ENCRYPTION_KEY");
|
|
232
249
|
|
|
@@ -17,6 +17,10 @@ import { listenOnFreePort } from "./test-net";
|
|
|
17
17
|
|
|
18
18
|
const TEST_DB_PATH = "./test-scripts-only-gating.sqlite";
|
|
19
19
|
const SCRIPT_TOOL_NAMES = [
|
|
20
|
+
"extension-delete",
|
|
21
|
+
"extension-enable",
|
|
22
|
+
"extension-disable",
|
|
23
|
+
"extension-activate-version",
|
|
20
24
|
"extension-install",
|
|
21
25
|
"extension-list",
|
|
22
26
|
"get-script-run",
|
|
@@ -1789,7 +1789,7 @@ describe("Slack renderer v2", () => {
|
|
|
1789
1789
|
expect(outcome?.finalizedAt).toBeDefined();
|
|
1790
1790
|
});
|
|
1791
1791
|
|
|
1792
|
-
test("
|
|
1792
|
+
test("omits the deferral ETA even when the agent already sent a slack-reply", async () => {
|
|
1793
1793
|
const lead = await createAgent({ name: "Deferring Lead", isLead: true, status: "idle" });
|
|
1794
1794
|
const { channelId, threadTs } = uniqueSlackAddress("C_RENDER_DEFER_AFTER_REPLY");
|
|
1795
1795
|
const ask = await createTaskExtended("defer after replying by hand", {
|
|
@@ -1814,10 +1814,32 @@ describe("Slack renderer v2", () => {
|
|
|
1814
1814
|
await processSlackRenderV2();
|
|
1815
1815
|
|
|
1816
1816
|
const started = calls.find((call) => call.method === "chat.startStream");
|
|
1817
|
-
expect(started?.payload.markdown_text).toBe(
|
|
1817
|
+
expect(started?.payload.markdown_text).toBe("✅\n\nPending: checking the new defer card");
|
|
1818
|
+
expect(JSON.stringify(calls)).not.toContain("17:30:19");
|
|
1819
|
+
expect(JSON.stringify(calls)).not.toContain("Deferred until");
|
|
1820
|
+
expect((await getTaskById(ask.id))?.output).toBe(deferralLine);
|
|
1818
1821
|
expect(started?.payload.markdown_text).not.toBe(`✅ ${lead.name} completed`);
|
|
1819
1822
|
});
|
|
1820
1823
|
|
|
1824
|
+
test("does not post an ETA-only deferral notice", async () => {
|
|
1825
|
+
const { channelId, threadTs } = uniqueSlackAddress("C_ETA_ONLY");
|
|
1826
|
+
const ask = await createTaskExtended("wait", {
|
|
1827
|
+
source: "slack",
|
|
1828
|
+
slackChannelId: channelId,
|
|
1829
|
+
slackThreadTs: threadTs,
|
|
1830
|
+
});
|
|
1831
|
+
await startTask(ask.id);
|
|
1832
|
+
await completeTask(
|
|
1833
|
+
ask.id,
|
|
1834
|
+
"Deferred until today 17:30 UTC ([12345678](https://example.com/schedules/12345678)) -> ",
|
|
1835
|
+
{ addTags: ["deferred"] },
|
|
1836
|
+
);
|
|
1837
|
+
calls.length = 0;
|
|
1838
|
+
await processSlackRenderV2();
|
|
1839
|
+
expect(calls.filter((call) => call.method === "chat.startStream")).toHaveLength(0);
|
|
1840
|
+
expect(JSON.stringify(calls)).not.toContain("17:30");
|
|
1841
|
+
});
|
|
1842
|
+
|
|
1821
1843
|
test("refreshes a stream started with stale content before finalizing it", async () => {
|
|
1822
1844
|
const lead = await createAgent({ name: "Refresh Lead", isLead: true, status: "idle" });
|
|
1823
1845
|
const { channelId, threadTs } = uniqueSlackAddress("C_RENDER_REFRESH_STALE");
|
|
@@ -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
|
+
});
|
|
@@ -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));
|
|
@@ -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", () => {
|
|
@@ -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
|
+
};
|
|
@@ -26,6 +26,7 @@ export const extensionToolOutputSchema = swarmToolOutputSchema({
|
|
|
26
26
|
priority: z.number().optional(),
|
|
27
27
|
consecutiveFailures: z.number().optional(),
|
|
28
28
|
contentDeduped: z.boolean().optional(),
|
|
29
|
+
deleted: z.boolean().optional(),
|
|
29
30
|
extensions: z.array(extensionListItemSchema).optional(),
|
|
30
31
|
});
|
|
31
32
|
|
|
@@ -61,7 +62,7 @@ function renderDiagnostics(data: unknown): string | undefined {
|
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
export async function proxyExtensionsApi<T>(args: {
|
|
64
|
-
method: "GET" | "POST";
|
|
65
|
+
method: "GET" | "POST" | "DELETE";
|
|
65
66
|
path: string;
|
|
66
67
|
body?: unknown;
|
|
67
68
|
requestInfo: RequestInfo;
|
|
@@ -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
|
+
};
|