@desplega.ai/agent-swarm 1.103.0 → 1.104.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 -0
- package/dist/{actions-7txktrdj.js → actions-2expzt47.js} +6 -6
- package/dist/{app-xm3xsdye.js → app-dr8hwx7z.js} +3 -3
- package/dist/{assistant-qpv63wk6.js → assistant-9rhgsw7v.js} +9 -10
- package/dist/{boot-reembed-chxhrwv8.js → boot-reembed-3q1aywnz.js} +5 -4
- package/dist/{boot-reembed-9cxqka3e.js → boot-reembed-9kt97g1z.js} +4 -3
- package/dist/{boot-scrub-logs-8qtfkcz8.js → boot-scrub-logs-vskq108e.js} +2 -2
- package/dist/{cli-fedn86nz.js → cli-0jw6j755.js} +8 -3
- package/dist/{cli-yeemwevj.js → cli-23zk7xqy.js} +20 -16
- package/dist/{cli-v9283nma.js → cli-2g9jf9ek.js} +1 -1
- package/dist/{cli-rvae030h.js → cli-2hap19dv.js} +3 -3
- package/dist/{cli-wjyyv518.js → cli-3j01d33e.js} +9 -14
- package/dist/{cli-8hyv6c2v.js → cli-54h5daha.js} +41 -23
- package/dist/{cli-fygaw191.js → cli-5pf0b84v.js} +2 -2
- package/dist/{cli-m8f6qzck.js → cli-7v4y3hyd.js} +1 -1
- package/dist/{cli-fwxdt7kt.js → cli-d4z1csms.js} +30 -5
- package/dist/{cli-jt9d9fjw.js → cli-e7bwjntj.js} +1 -1
- package/dist/{cli-s14sb64w.js → cli-etvgz5x8.js} +5 -7
- package/dist/{cli-2qynerth.js → cli-gj2n2ec1.js} +1 -1
- package/dist/{cli-dz3d6zjg.js → cli-nzc0h54v.js} +121 -2
- package/dist/{cli-8w95v19d.js → cli-p4cj4vhc.js} +351 -24
- package/dist/{cli-xmtsan9k.js → cli-q5tshxd6.js} +1 -1
- package/dist/{http-zts4haq7.js → cli-qvzgncd8.js} +83966 -76422
- package/dist/{cli-4fnp2tc3.js → cli-s42f54ja.js} +1 -1
- package/dist/{cli-krvxq8bc.js → cli-t9v14cnd.js} +23 -4
- package/dist/{cli-gzepjz54.js → cli-v1syqkcb.js} +900 -352
- package/dist/{cli-ksq38x4n.js → cli-w1nfskkg.js} +2 -2
- package/dist/{cli-ns0r7mkr.js → cli-y9p3e94h.js} +3 -3
- package/dist/{cli-85pf4z38.js → cli-yjt8a36k.js} +3 -3
- package/dist/{cli-yfwwjeft.js → cli-yvektv79.js} +1 -1
- package/dist/{cli-kk51f3tf.js → cli-zaxksbgj.js} +5 -5
- package/dist/{cli-rttgde5f.js → cli-zghef0em.js} +1 -1
- package/dist/cli.js +10 -10
- package/dist/{commands-aqn63mcw.js → commands-wfrkef57.js} +2 -2
- package/dist/{db-azrvwsnj.js → db-c42pxt7h.js} +2 -2
- package/dist/{handlers-hf61har3.js → handlers-n36zkfvx.js} +10 -12
- package/dist/{hook-306p3yz4.js → hook-gw3vhv6s.js} +7 -5
- package/dist/http-59ygpn2h.js +17263 -0
- package/dist/{index-h2yqjyk0.js → index-881c1baf.js} +9 -8
- package/dist/{index-29vg51x6.js → index-jca7jdpt.js} +23 -10
- package/dist/{index-bfh9hgek.js → index-xf6fp4y1.js} +5 -5
- package/dist/{index-3c651yfk.js → index-xz8vaa5c.js} +14 -11
- package/dist/{keepalive-a0dg5pds.js → keepalive-mj4302vw.js} +4 -4
- package/dist/{lead-7mcxedkc.js → lead-qm0xjvkv.js} +19 -19
- package/dist/{maintenance-mrzqd53t.js → maintenance-xppxbe4t.js} +5 -4
- package/dist/{onboard-4dkpv9wa.js → onboard-ejx6yxh8.js} +2 -2
- package/dist/{otel-impl-jt7gpyp9.js → otel-impl-zs5exkpk.js} +1 -1
- package/dist/{pricing-refresh-xqnzh8dt.js → pricing-refresh-nqyf8bkb.js} +4 -4
- package/dist/{seed-pricing-56tjm1fj.js → seed-pricing-w3x550cj.js} +3 -3
- package/dist/{setup-tbwk8j10.js → setup-7729ktfj.js} +2 -2
- package/dist/{worker-e03za92t.js → worker-4ttemwhw.js} +19 -19
- package/openapi.json +90 -4
- package/package.json +3 -3
- package/src/be/memory/constants.ts +6 -0
- package/src/be/memory/providers/sqlite-store.ts +468 -71
- package/src/be/memory/raters/retrieval.ts +4 -2
- package/src/be/memory/reranker.ts +5 -2
- package/src/be/memory/retrieval-store.ts +2 -0
- package/src/be/memory/types.ts +46 -0
- package/src/be/migrations/099_memory_structured_key_versioning.sql +41 -0
- package/src/be/migrations/100_memory_retrieval_source.sql +8 -0
- package/src/be/migrations/101_script_connections.sql +67 -0
- package/src/be/script-connections.ts +744 -0
- package/src/be/script-credential-broker.ts +38 -0
- package/src/be/scripts/typecheck.ts +23 -2
- package/src/be/seed-scripts/catalog/boot-triage.inline.ts +16 -4
- package/src/be/seed-scripts/catalog/boot-triage.ts +16 -4
- package/src/commands/runner.ts +8 -6
- package/src/heartbeat/heartbeat.ts +42 -1
- package/src/heartbeat/index.ts +7 -1
- package/src/http/memory.ts +127 -14
- package/src/http/scripts.ts +15 -14
- package/src/scripts-runtime/api-client.ts +100 -0
- package/src/scripts-runtime/api-types.ts +45 -0
- package/src/scripts-runtime/credential-broker/README.md +40 -0
- package/src/scripts-runtime/credential-broker/broker.ts +51 -0
- package/src/scripts-runtime/credential-broker/default-bindings.ts +19 -0
- package/src/scripts-runtime/credential-broker/fetch-patch.ts +102 -0
- package/src/scripts-runtime/credential-broker/index.ts +17 -0
- package/src/scripts-runtime/credential-broker/store.ts +72 -0
- package/src/scripts-runtime/credential-broker/types.ts +54 -0
- package/src/scripts-runtime/ctx.ts +11 -1
- package/src/scripts-runtime/egress-secrets.ts +14 -76
- package/src/scripts-runtime/eval-harness.ts +1 -1
- package/src/scripts-runtime/executors/types.ts +5 -5
- package/src/scripts-runtime/loader.ts +6 -1
- package/src/scripts-runtime/sdk-allowlist.ts +1 -0
- package/src/scripts-runtime/types/stdlib.d.ts +11 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +11 -0
- package/src/server.ts +6 -0
- package/src/tests/credential-broker.test.ts +281 -0
- package/src/tests/heartbeat.test.ts +239 -0
- package/src/tests/memory-edit.test.ts +177 -0
- package/src/tests/memory-hybrid.test.ts +212 -0
- package/src/tests/runner-repo-autostash.test.ts +39 -8
- package/src/tests/script-connections.test.ts +360 -0
- package/src/tests/scripts-http.test.ts +16 -10
- package/src/tests/scripts-runtime-secret-egress.test.ts +26 -5
- package/src/tests/swarm-config-reserved-keys.test.ts +23 -0
- package/src/tests/update-schedule-mcp-tool.test.ts +83 -4
- package/src/tools/credential-bindings/index.ts +1 -0
- package/src/tools/credential-bindings/tool.ts +238 -0
- package/src/tools/memory-edit.ts +148 -0
- package/src/tools/memory-search.ts +17 -10
- package/src/tools/schedules/delete-schedule.ts +18 -8
- package/src/tools/schedules/update-schedule.ts +4 -9
- package/src/tools/script-connections/index.ts +1 -0
- package/src/tools/script-connections/tool.ts +167 -0
- package/src/tools/swarm-config/delete-config.ts +5 -0
- package/src/tools/swarm-config/set-config.ts +23 -1
- package/src/tools/tool-config.ts +6 -1
- package/src/types.ts +6 -0
- package/src/workflows/executors/swarm-script.ts +2 -0
- package/templates/community/dora-metrics/PLAYBOOK.md +294 -0
- package/templates/community/dora-metrics/README.md +39 -0
- package/templates/community/dora-metrics/lead-prompt.md +56 -0
- package/templates/community/dora-metrics/report.mjs +393 -0
- package/templates/community/dora-metrics/run.sh +113 -0
- package/templates/schedules/weekly-dora-metrics/config.json +13 -0
- package/templates/schedules/weekly-dora-metrics/content.md +61 -0
- package/dist/cli-2307phk1.js +0 -24223
- package/dist/cli-2deg0cen.js +0 -120
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { createAgent, deleteSwarmConfig, getDb, upsertSwarmConfig } from "../be/db";
|
|
4
|
+
import {
|
|
5
|
+
getScriptApiConnectionDescriptors,
|
|
6
|
+
upsertCredentialBinding,
|
|
7
|
+
upsertScriptConnection,
|
|
8
|
+
} from "../be/script-connections";
|
|
9
|
+
import { buildScriptCredentialBindings } from "../be/script-credential-broker";
|
|
10
|
+
import { typecheckScript } from "../be/scripts/typecheck";
|
|
11
|
+
import { runScript } from "../scripts-runtime/loader";
|
|
12
|
+
import { registerScriptConnectionsTool } from "../tools/script-connections";
|
|
13
|
+
|
|
14
|
+
const createdBindingIds: string[] = [];
|
|
15
|
+
const createdConnectionIds: string[] = [];
|
|
16
|
+
const createdConfigIds: string[] = [];
|
|
17
|
+
const originalFetch = globalThis.fetch;
|
|
18
|
+
const savedEnv = { ...process.env };
|
|
19
|
+
const resources = { memoryMb: 2048, cpuTimeSec: 20, maxStdoutBytes: 1_048_576 };
|
|
20
|
+
|
|
21
|
+
type RegisteredTool = {
|
|
22
|
+
handler: (args: unknown, extra: unknown) => Promise<unknown>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function scriptConnectionsTool() {
|
|
26
|
+
const server = new McpServer({ name: "script-connections-test", version: "1.0.0" });
|
|
27
|
+
registerScriptConnectionsTool(server);
|
|
28
|
+
const registered = (server as unknown as { _registeredTools: Record<string, RegisteredTool> })
|
|
29
|
+
._registeredTools;
|
|
30
|
+
const tool = registered["script-connections"];
|
|
31
|
+
if (!tool) throw new Error("script-connections tool not registered");
|
|
32
|
+
return tool;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function meta(agentId: string) {
|
|
36
|
+
return {
|
|
37
|
+
sessionId: "script-connections-test-session",
|
|
38
|
+
requestInfo: { headers: { "x-agent-id": agentId } },
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const openapiSpec = JSON.stringify({
|
|
43
|
+
openapi: "3.1.0",
|
|
44
|
+
info: { title: "Vendor", version: "1.0.0" },
|
|
45
|
+
paths: {
|
|
46
|
+
"/repos/{owner}/{repo}": {
|
|
47
|
+
get: {
|
|
48
|
+
operationId: "getRepo",
|
|
49
|
+
parameters: [
|
|
50
|
+
{ name: "owner", in: "path", required: true, schema: { type: "string" } },
|
|
51
|
+
{ name: "repo", in: "path", required: true, schema: { type: "string" } },
|
|
52
|
+
{ name: "include", in: "query", schema: { type: "string" } },
|
|
53
|
+
],
|
|
54
|
+
responses: {
|
|
55
|
+
"200": {
|
|
56
|
+
description: "repo",
|
|
57
|
+
content: {
|
|
58
|
+
"application/json": {
|
|
59
|
+
schema: {
|
|
60
|
+
type: "object",
|
|
61
|
+
required: ["full_name"],
|
|
62
|
+
properties: {
|
|
63
|
+
full_name: { type: "string" },
|
|
64
|
+
private: { type: "boolean" },
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
"/repos": {
|
|
74
|
+
post: {
|
|
75
|
+
operationId: "createRepo",
|
|
76
|
+
requestBody: {
|
|
77
|
+
required: true,
|
|
78
|
+
content: {
|
|
79
|
+
"application/json": {
|
|
80
|
+
schema: {
|
|
81
|
+
type: "object",
|
|
82
|
+
required: ["name"],
|
|
83
|
+
properties: {
|
|
84
|
+
name: { type: "string" },
|
|
85
|
+
private: { type: "boolean" },
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
responses: {
|
|
92
|
+
"201": {
|
|
93
|
+
description: "created repo",
|
|
94
|
+
content: {
|
|
95
|
+
"application/json": {
|
|
96
|
+
schema: {
|
|
97
|
+
type: "object",
|
|
98
|
+
required: ["full_name"],
|
|
99
|
+
properties: {
|
|
100
|
+
full_name: { type: "string" },
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
beforeEach(() => {
|
|
113
|
+
process.env.AGENT_SWARM_API_KEY = "script-connections-test-key";
|
|
114
|
+
delete process.env.API_KEY;
|
|
115
|
+
process.env.MCP_BASE_URL = "http://localhost:3013";
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
afterEach(() => {
|
|
119
|
+
globalThis.fetch = originalFetch;
|
|
120
|
+
for (const key of Object.keys(process.env)) {
|
|
121
|
+
if (!(key in savedEnv)) delete process.env[key];
|
|
122
|
+
}
|
|
123
|
+
for (const [key, value] of Object.entries(savedEnv)) {
|
|
124
|
+
if (value === undefined) delete process.env[key];
|
|
125
|
+
else process.env[key] = value;
|
|
126
|
+
}
|
|
127
|
+
const db = getDb();
|
|
128
|
+
for (const id of createdConnectionIds.splice(0)) {
|
|
129
|
+
db.run("DELETE FROM script_connections WHERE id = ?", id);
|
|
130
|
+
}
|
|
131
|
+
for (const id of createdBindingIds.splice(0)) {
|
|
132
|
+
db.run("DELETE FROM script_credential_bindings WHERE id = ?", id);
|
|
133
|
+
}
|
|
134
|
+
for (const id of createdConfigIds.splice(0)) {
|
|
135
|
+
deleteSwarmConfig(id);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe("script connections", () => {
|
|
140
|
+
test("relational credential bindings are resolved before legacy JSON config", () => {
|
|
141
|
+
const binding = upsertCredentialBinding({
|
|
142
|
+
configKey: "REL_VENDOR_KEY",
|
|
143
|
+
allowedHosts: ["api.vendor.test"],
|
|
144
|
+
headerTemplate: "Authorization: Bearer [REDACTED:REL_VENDOR_KEY]",
|
|
145
|
+
});
|
|
146
|
+
createdBindingIds.push(binding.id);
|
|
147
|
+
const secretConfig = upsertSwarmConfig({
|
|
148
|
+
scope: "global",
|
|
149
|
+
key: "REL_VENDOR_KEY",
|
|
150
|
+
value: "rel-secret",
|
|
151
|
+
isSecret: true,
|
|
152
|
+
});
|
|
153
|
+
createdConfigIds.push(secretConfig.id);
|
|
154
|
+
|
|
155
|
+
const egressSecrets = buildScriptCredentialBindings({});
|
|
156
|
+
|
|
157
|
+
expect(egressSecrets).toContainEqual(
|
|
158
|
+
expect.objectContaining({
|
|
159
|
+
configKey: "REL_VENDOR_KEY",
|
|
160
|
+
allowedHosts: ["api.vendor.test"],
|
|
161
|
+
value: "rel-secret",
|
|
162
|
+
}),
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("credential binding upsert is idempotent by binding identity", () => {
|
|
167
|
+
const first = upsertCredentialBinding({
|
|
168
|
+
configKey: "IDEMPOTENT_VENDOR_KEY",
|
|
169
|
+
allowedHosts: ["old.vendor.test"],
|
|
170
|
+
headerTemplate: "Authorization: Bearer [REDACTED:IDEMPOTENT_VENDOR_KEY]",
|
|
171
|
+
});
|
|
172
|
+
createdBindingIds.push(first.id);
|
|
173
|
+
|
|
174
|
+
const second = upsertCredentialBinding({
|
|
175
|
+
configKey: "IDEMPOTENT_VENDOR_KEY",
|
|
176
|
+
allowedHosts: ["new.vendor.test"],
|
|
177
|
+
headerTemplate: "Authorization: Bearer [REDACTED:IDEMPOTENT_VENDOR_KEY]",
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
expect(second.id).toBe(first.id);
|
|
181
|
+
expect(second.allowedHosts).toEqual(["new.vendor.test"]);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("OpenAPI connections generate full ctx.api method, args, and response types", () => {
|
|
185
|
+
const binding = upsertCredentialBinding({
|
|
186
|
+
configKey: "TYPE_VENDOR_KEY",
|
|
187
|
+
allowedHosts: ["api.vendor.test"],
|
|
188
|
+
headerTemplate: "Authorization: Bearer [REDACTED:TYPE_VENDOR_KEY]",
|
|
189
|
+
});
|
|
190
|
+
createdBindingIds.push(binding.id);
|
|
191
|
+
const connection = upsertScriptConnection({
|
|
192
|
+
slug: "vendorApi",
|
|
193
|
+
kind: "openapi",
|
|
194
|
+
baseUrl: "https://api.vendor.test",
|
|
195
|
+
credentialBindingId: binding.id,
|
|
196
|
+
openapiSpecJson: openapiSpec,
|
|
197
|
+
});
|
|
198
|
+
createdConnectionIds.push(connection.id);
|
|
199
|
+
|
|
200
|
+
expect(connection.generationError).toBeNull();
|
|
201
|
+
const source = `
|
|
202
|
+
import type { ScriptMain } from "swarm-sdk";
|
|
203
|
+
const main: ScriptMain = async (_args, ctx) => {
|
|
204
|
+
const repo = await ctx.api.vendorApi.getRepo({
|
|
205
|
+
path: { owner: "desplega-ai", repo: "agent-swarm" },
|
|
206
|
+
query: { include: "stats" },
|
|
207
|
+
});
|
|
208
|
+
const created = await ctx.api.vendorApi.createRepo({
|
|
209
|
+
body: { name: "agent-swarm", private: false },
|
|
210
|
+
});
|
|
211
|
+
const name: string = repo.full_name;
|
|
212
|
+
const createdName: string = created.full_name;
|
|
213
|
+
const isPrivate: boolean | undefined = repo.private;
|
|
214
|
+
return { name, createdName, isPrivate };
|
|
215
|
+
};
|
|
216
|
+
export default main;
|
|
217
|
+
`;
|
|
218
|
+
|
|
219
|
+
expect(typecheckScript(source)).toEqual({ ok: true });
|
|
220
|
+
const descriptor = getScriptApiConnectionDescriptors().find(
|
|
221
|
+
(candidate) => candidate.slug === "vendorApi",
|
|
222
|
+
);
|
|
223
|
+
const getRepo = descriptor?.operations.find((operation) => operation.name === "getRepo");
|
|
224
|
+
expect(getRepo?.parameters).toContainEqual({
|
|
225
|
+
name: "owner",
|
|
226
|
+
in: "path",
|
|
227
|
+
required: true,
|
|
228
|
+
schema: { type: "string" },
|
|
229
|
+
});
|
|
230
|
+
expect(getRepo?.responseSchema).toEqual({
|
|
231
|
+
type: "object",
|
|
232
|
+
required: ["full_name"],
|
|
233
|
+
properties: {
|
|
234
|
+
full_name: { type: "string" },
|
|
235
|
+
private: { type: "boolean" },
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
const createRepo = descriptor?.operations.find((operation) => operation.name === "createRepo");
|
|
239
|
+
expect(createRepo?.requestBodySchema).toEqual({
|
|
240
|
+
type: "object",
|
|
241
|
+
required: ["name"],
|
|
242
|
+
properties: {
|
|
243
|
+
name: { type: "string" },
|
|
244
|
+
private: { type: "boolean" },
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("script-connections tool registers OpenAPI connections from an agent header without FK failures", async () => {
|
|
250
|
+
const suffix = crypto.randomUUID().replace(/-/g, "");
|
|
251
|
+
const slug = `agentHeaderVendor${suffix}`;
|
|
252
|
+
const configKey = `AGENT_HEADER_VENDOR_KEY_${suffix}`;
|
|
253
|
+
const lead = createAgent({
|
|
254
|
+
name: `script-connections-lead-${crypto.randomUUID()}`,
|
|
255
|
+
isLead: true,
|
|
256
|
+
status: "idle",
|
|
257
|
+
});
|
|
258
|
+
const tool = scriptConnectionsTool();
|
|
259
|
+
|
|
260
|
+
const result = (await tool.handler(
|
|
261
|
+
{
|
|
262
|
+
action: "upsert-openapi",
|
|
263
|
+
slug,
|
|
264
|
+
displayName: "Agent Header Vendor",
|
|
265
|
+
baseUrl: "https://api.vendor.test",
|
|
266
|
+
allowedHosts: ["api.vendor.test"],
|
|
267
|
+
configKey,
|
|
268
|
+
openapiSpecJson: openapiSpec,
|
|
269
|
+
},
|
|
270
|
+
meta(lead.id),
|
|
271
|
+
)) as {
|
|
272
|
+
structuredContent: { success: boolean; message: string; connections: Array<{ id: string }> };
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
expect(result.structuredContent.success).toBe(true);
|
|
276
|
+
|
|
277
|
+
const db = getDb();
|
|
278
|
+
const connectionRow = db
|
|
279
|
+
.prepare<
|
|
280
|
+
{
|
|
281
|
+
id: string;
|
|
282
|
+
credential_binding_id: string | null;
|
|
283
|
+
created_by: string | null;
|
|
284
|
+
updated_by: string | null;
|
|
285
|
+
},
|
|
286
|
+
[string]
|
|
287
|
+
>(
|
|
288
|
+
"SELECT id, credential_binding_id, created_by, updated_by FROM script_connections WHERE slug = ?",
|
|
289
|
+
)
|
|
290
|
+
.get(slug);
|
|
291
|
+
expect(connectionRow).toBeDefined();
|
|
292
|
+
expect(connectionRow?.created_by).toBeNull();
|
|
293
|
+
expect(connectionRow?.updated_by).toBeNull();
|
|
294
|
+
createdConnectionIds.push(connectionRow!.id);
|
|
295
|
+
|
|
296
|
+
const bindingRow = db
|
|
297
|
+
.prepare<{ id: string; created_by: string | null; updated_by: string | null }, [string]>(
|
|
298
|
+
"SELECT id, created_by, updated_by FROM script_credential_bindings WHERE config_key = ?",
|
|
299
|
+
)
|
|
300
|
+
.get(configKey);
|
|
301
|
+
expect(bindingRow).toBeDefined();
|
|
302
|
+
expect(bindingRow?.id).toBe(connectionRow?.credential_binding_id);
|
|
303
|
+
expect(bindingRow?.created_by).toBeNull();
|
|
304
|
+
expect(bindingRow?.updated_by).toBeNull();
|
|
305
|
+
createdBindingIds.push(bindingRow!.id);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("ctx.api runtime emits plain fetch with credential placeholders", async () => {
|
|
309
|
+
let observed: { url: string; authorization: string | null } | null = null;
|
|
310
|
+
const server = Bun.serve({
|
|
311
|
+
port: 0,
|
|
312
|
+
fetch(req) {
|
|
313
|
+
observed = {
|
|
314
|
+
url: req.url,
|
|
315
|
+
authorization: req.headers.get("authorization"),
|
|
316
|
+
};
|
|
317
|
+
return Response.json({ full_name: "desplega-ai/agent-swarm", private: false });
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
const binding = upsertCredentialBinding({
|
|
321
|
+
configKey: "RUNTIME_VENDOR_KEY",
|
|
322
|
+
allowedHosts: [`127.0.0.1:${server.port}`],
|
|
323
|
+
headerTemplate: "Authorization: Bearer [REDACTED:RUNTIME_VENDOR_KEY]",
|
|
324
|
+
});
|
|
325
|
+
createdBindingIds.push(binding.id);
|
|
326
|
+
const connection = upsertScriptConnection({
|
|
327
|
+
slug: "runtimeVendor",
|
|
328
|
+
kind: "openapi",
|
|
329
|
+
baseUrl: `http://127.0.0.1:${server.port}`,
|
|
330
|
+
credentialBindingId: binding.id,
|
|
331
|
+
openapiSpecJson: openapiSpec,
|
|
332
|
+
});
|
|
333
|
+
createdConnectionIds.push(connection.id);
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
const output = await runScript({
|
|
337
|
+
agentId: "agent-1",
|
|
338
|
+
resources,
|
|
339
|
+
apiConnections: getScriptApiConnectionDescriptors(),
|
|
340
|
+
source: `
|
|
341
|
+
export default async (_args, ctx) => {
|
|
342
|
+
return await ctx.api.runtimeVendor.getRepo({
|
|
343
|
+
path: { owner: "desplega-ai", repo: "agent-swarm" },
|
|
344
|
+
query: { include: "stats" },
|
|
345
|
+
});
|
|
346
|
+
};
|
|
347
|
+
`,
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
expect(output.error).toBeUndefined();
|
|
351
|
+
expect(output.result).toEqual({ full_name: "desplega-ai/agent-swarm", private: false });
|
|
352
|
+
expect(observed).toEqual({
|
|
353
|
+
url: `http://127.0.0.1:${server.port}/repos/desplega-ai/agent-swarm?include=stats`,
|
|
354
|
+
authorization: "Bearer [REDACTED:RUNTIME_VENDOR_KEY]",
|
|
355
|
+
});
|
|
356
|
+
} finally {
|
|
357
|
+
server.stop(true);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
});
|
|
@@ -316,18 +316,13 @@ describe("/api/scripts HTTP", () => {
|
|
|
316
316
|
expect((await res.json()).error).toBe("typecheck_failed");
|
|
317
317
|
});
|
|
318
318
|
|
|
319
|
-
test("
|
|
320
|
-
const denied = await upsert(
|
|
321
|
-
{ name: "global-denied", scope: "global", source: validSource(2) },
|
|
322
|
-
workerId,
|
|
323
|
-
);
|
|
324
|
-
expect(denied.status).toBe(403);
|
|
325
|
-
|
|
319
|
+
test("non-lead agents can upsert and delete global scripts", async () => {
|
|
326
320
|
const allowed = await upsert(
|
|
327
|
-
{ name: "global-ok", scope: "global", source: validSource(2) },
|
|
328
|
-
|
|
321
|
+
{ name: "global-worker-ok", scope: "global", source: validSource(2) },
|
|
322
|
+
workerId,
|
|
329
323
|
);
|
|
330
324
|
expect(allowed.status).toBe(200);
|
|
325
|
+
expect(getScript({ name: "global-worker-ok", scope: "global", scopeId: null })).toBeTruthy();
|
|
331
326
|
|
|
332
327
|
const event = getDb()
|
|
333
328
|
.prepare<{ data: string }, []>(
|
|
@@ -335,7 +330,18 @@ describe("/api/scripts HTTP", () => {
|
|
|
335
330
|
)
|
|
336
331
|
.get();
|
|
337
332
|
expect(event).toBeTruthy();
|
|
338
|
-
expect(JSON.parse(event!.data)
|
|
333
|
+
expect(JSON.parse(event!.data)).toMatchObject({
|
|
334
|
+
isNew: true,
|
|
335
|
+
changedByAgentId: workerId,
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
const del = await dispatch("/api/scripts/global-worker-ok?scope=global", {
|
|
339
|
+
method: "DELETE",
|
|
340
|
+
agentId: workerId,
|
|
341
|
+
});
|
|
342
|
+
expect(del.status).toBe(200);
|
|
343
|
+
expect(await del.json()).toEqual({ deleted: true });
|
|
344
|
+
expect(getScript({ name: "global-worker-ok", scope: "global", scopeId: null })).toBeNull();
|
|
339
345
|
});
|
|
340
346
|
|
|
341
347
|
test("global upsert promotion marks isPromotion when caller has agent script with same name", async () => {
|
|
@@ -54,8 +54,13 @@ describe("egress-substitution", () => {
|
|
|
54
54
|
const secrets = buildEgressSecrets();
|
|
55
55
|
expect(secrets).toEqual([
|
|
56
56
|
{
|
|
57
|
+
configKey: "GITHUB_TOKEN",
|
|
57
58
|
placeholder: "[REDACTED:GITHUB_TOKEN]",
|
|
58
|
-
|
|
59
|
+
allowedHosts: ["api.github.com"],
|
|
60
|
+
headerTemplate: "Authorization: Bearer [REDACTED:GITHUB_TOKEN]",
|
|
61
|
+
scope: "global",
|
|
62
|
+
scopeId: null,
|
|
63
|
+
active: true,
|
|
59
64
|
value: "ghp_test1234567890abcdef",
|
|
60
65
|
},
|
|
61
66
|
]);
|
|
@@ -89,7 +94,11 @@ describe("egress-substitution", () => {
|
|
|
89
94
|
patchFetchWithEgressSubstitution([
|
|
90
95
|
{
|
|
91
96
|
placeholder: "[REDACTED:GITHUB_TOKEN]",
|
|
92
|
-
|
|
97
|
+
allowedHosts: ["api.github.com"],
|
|
98
|
+
headerTemplate: "Authorization: Bearer [REDACTED:GITHUB_TOKEN]",
|
|
99
|
+
scope: "global",
|
|
100
|
+
scopeId: null,
|
|
101
|
+
active: true,
|
|
93
102
|
value: "ghp_real_secret_value_123",
|
|
94
103
|
},
|
|
95
104
|
]);
|
|
@@ -111,7 +120,11 @@ describe("egress-substitution", () => {
|
|
|
111
120
|
patchFetchWithEgressSubstitution([
|
|
112
121
|
{
|
|
113
122
|
placeholder: "[REDACTED:GITHUB_TOKEN]",
|
|
114
|
-
|
|
123
|
+
allowedHosts: ["api.github.com"],
|
|
124
|
+
headerTemplate: "Authorization: Bearer [REDACTED:GITHUB_TOKEN]",
|
|
125
|
+
scope: "global",
|
|
126
|
+
scopeId: null,
|
|
127
|
+
active: true,
|
|
115
128
|
value: "ghp_real_secret_value_123",
|
|
116
129
|
},
|
|
117
130
|
]);
|
|
@@ -133,7 +146,11 @@ describe("egress-substitution", () => {
|
|
|
133
146
|
patchFetchWithEgressSubstitution([
|
|
134
147
|
{
|
|
135
148
|
placeholder: "[REDACTED:GITHUB_TOKEN]",
|
|
136
|
-
|
|
149
|
+
allowedHosts: ["api.github.com"],
|
|
150
|
+
headerTemplate: "Authorization: Bearer [REDACTED:GITHUB_TOKEN]",
|
|
151
|
+
scope: "global",
|
|
152
|
+
scopeId: null,
|
|
153
|
+
active: true,
|
|
137
154
|
value: "ghp_real_secret_value_123",
|
|
138
155
|
},
|
|
139
156
|
]);
|
|
@@ -155,7 +172,11 @@ describe("egress-substitution", () => {
|
|
|
155
172
|
patchFetchWithEgressSubstitution([
|
|
156
173
|
{
|
|
157
174
|
placeholder: "[REDACTED:GITHUB_TOKEN]",
|
|
158
|
-
|
|
175
|
+
allowedHosts: ["api.github.com"],
|
|
176
|
+
headerTemplate: "Authorization: Bearer [REDACTED:GITHUB_TOKEN]",
|
|
177
|
+
scope: "global",
|
|
178
|
+
scopeId: null,
|
|
179
|
+
active: true,
|
|
159
180
|
value: "ghp_real_secret_value_123",
|
|
160
181
|
},
|
|
161
182
|
]);
|
|
@@ -3,6 +3,7 @@ import { unlink } from "node:fs/promises";
|
|
|
3
3
|
import { createServer as createHttpServer, type Server } from "node:http";
|
|
4
4
|
import {
|
|
5
5
|
closeDb,
|
|
6
|
+
createAgent,
|
|
6
7
|
deleteSwarmConfig,
|
|
7
8
|
getDb,
|
|
8
9
|
getSwarmConfigById,
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
import { isReservedConfigKey, reservedKeyError } from "../be/swarm-config-guard";
|
|
14
15
|
import { handleConfig } from "../http/config";
|
|
15
16
|
import { getPathSegments, parseQueryParams } from "../http/utils";
|
|
17
|
+
import { CREDENTIAL_BINDINGS_CONFIG_KEY } from "../scripts-runtime/credential-broker";
|
|
16
18
|
import { registerDeleteConfigTool } from "../tools/swarm-config/delete-config";
|
|
17
19
|
import { registerListConfigTool } from "../tools/swarm-config/list-config";
|
|
18
20
|
import { registerSetConfigTool } from "../tools/swarm-config/set-config";
|
|
@@ -262,6 +264,27 @@ describe("swarm-config reserved keys guard", () => {
|
|
|
262
264
|
|
|
263
265
|
expect(result.structuredContent.success).toBe(true);
|
|
264
266
|
});
|
|
267
|
+
|
|
268
|
+
test("rejects SCRIPT_CREDENTIAL_BINDINGS from non-lead set-config callers", async () => {
|
|
269
|
+
const worker = createAgent({
|
|
270
|
+
name: "config-worker",
|
|
271
|
+
isLead: false,
|
|
272
|
+
status: "idle",
|
|
273
|
+
capabilities: [],
|
|
274
|
+
});
|
|
275
|
+
const handler = mcpServer.handlers.get("set-config");
|
|
276
|
+
const result = (await handler!(
|
|
277
|
+
{
|
|
278
|
+
scope: "global",
|
|
279
|
+
key: CREDENTIAL_BINDINGS_CONFIG_KEY,
|
|
280
|
+
value: JSON.stringify({ bindings: [] }),
|
|
281
|
+
},
|
|
282
|
+
makeRequestInfo(worker.id),
|
|
283
|
+
)) as { structuredContent: { success: boolean; message: string } };
|
|
284
|
+
|
|
285
|
+
expect(result.structuredContent.success).toBe(false);
|
|
286
|
+
expect(result.structuredContent.message).toContain("Only the lead can manage");
|
|
287
|
+
});
|
|
265
288
|
});
|
|
266
289
|
|
|
267
290
|
// ─── MCP tool: delete-config ──────────────────────────────────────────────
|
|
@@ -11,7 +11,15 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
|
11
11
|
import { unlink } from "node:fs/promises";
|
|
12
12
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
13
13
|
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
closeDb,
|
|
16
|
+
createAgent,
|
|
17
|
+
createScheduledTask,
|
|
18
|
+
getDb,
|
|
19
|
+
getScheduledTaskById,
|
|
20
|
+
initDb,
|
|
21
|
+
} from "../be/db";
|
|
22
|
+
import { registerDeleteScheduleTool } from "../tools/schedules/delete-schedule";
|
|
15
23
|
import { registerUpdateScheduleTool } from "../tools/schedules/update-schedule";
|
|
16
24
|
|
|
17
25
|
const TEST_DB_PATH = "./test-update-schedule-mcp-tool.sqlite";
|
|
@@ -23,24 +31,34 @@ type RegisteredTool = {
|
|
|
23
31
|
function buildServer(): McpServer {
|
|
24
32
|
const server = new McpServer({ name: "update-schedule-mcp-test", version: "1.0.0" });
|
|
25
33
|
registerUpdateScheduleTool(server);
|
|
34
|
+
registerDeleteScheduleTool(server);
|
|
26
35
|
return server;
|
|
27
36
|
}
|
|
28
37
|
|
|
29
|
-
function
|
|
38
|
+
function callTool(
|
|
30
39
|
server: McpServer,
|
|
40
|
+
toolName: string,
|
|
31
41
|
args: Record<string, unknown>,
|
|
32
42
|
callerAgentId: string,
|
|
33
43
|
): Promise<CallToolResult> {
|
|
34
44
|
const tools = (server as unknown as { _registeredTools: Record<string, RegisteredTool> })
|
|
35
45
|
._registeredTools;
|
|
36
|
-
const tool = tools[
|
|
37
|
-
if (!tool) throw new Error(
|
|
46
|
+
const tool = tools[toolName];
|
|
47
|
+
if (!tool) throw new Error(`${toolName} not registered`);
|
|
38
48
|
return tool.handler(args, {
|
|
39
49
|
sessionId: "test-session",
|
|
40
50
|
requestInfo: { headers: { "x-agent-id": callerAgentId } },
|
|
41
51
|
});
|
|
42
52
|
}
|
|
43
53
|
|
|
54
|
+
function callUpdateSchedule(
|
|
55
|
+
server: McpServer,
|
|
56
|
+
args: Record<string, unknown>,
|
|
57
|
+
callerAgentId: string,
|
|
58
|
+
): Promise<CallToolResult> {
|
|
59
|
+
return callTool(server, "update-schedule", args, callerAgentId);
|
|
60
|
+
}
|
|
61
|
+
|
|
44
62
|
type ScheduleOutput = {
|
|
45
63
|
success: boolean;
|
|
46
64
|
message: string;
|
|
@@ -57,6 +75,7 @@ function structured(result: CallToolResult): ScheduleOutput {
|
|
|
57
75
|
}
|
|
58
76
|
|
|
59
77
|
let creatorId: string;
|
|
78
|
+
let otherAgentId: string;
|
|
60
79
|
|
|
61
80
|
beforeAll(async () => {
|
|
62
81
|
for (const suffix of ["", "-wal", "-shm"]) {
|
|
@@ -70,7 +89,13 @@ beforeAll(async () => {
|
|
|
70
89
|
isLead: false,
|
|
71
90
|
status: "idle",
|
|
72
91
|
});
|
|
92
|
+
const otherAgent = createAgent({
|
|
93
|
+
name: "update-schedule-mcp-other",
|
|
94
|
+
isLead: false,
|
|
95
|
+
status: "idle",
|
|
96
|
+
});
|
|
73
97
|
creatorId = creator.id;
|
|
98
|
+
otherAgentId = otherAgent.id;
|
|
74
99
|
});
|
|
75
100
|
|
|
76
101
|
afterAll(async () => {
|
|
@@ -83,6 +108,60 @@ afterAll(async () => {
|
|
|
83
108
|
});
|
|
84
109
|
|
|
85
110
|
describe("update-schedule MCP tool", () => {
|
|
111
|
+
test("non-lead, non-creator agent can update a schedule", async () => {
|
|
112
|
+
const server = buildServer();
|
|
113
|
+
const schedule = createScheduledTask({
|
|
114
|
+
name: `mcp-noncreator-update-${Date.now()}`,
|
|
115
|
+
intervalMs: 60000,
|
|
116
|
+
taskTemplate: "owned by creator",
|
|
117
|
+
createdByAgentId: creatorId,
|
|
118
|
+
timezone: "UTC",
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const result = await callUpdateSchedule(
|
|
122
|
+
server,
|
|
123
|
+
{ scheduleId: schedule.id, intervalMs: 120000 },
|
|
124
|
+
otherAgentId,
|
|
125
|
+
);
|
|
126
|
+
const sc = structured(result);
|
|
127
|
+
|
|
128
|
+
expect(sc.success).toBe(true);
|
|
129
|
+
expect(sc.schedule?.intervalMs).toBe(120000);
|
|
130
|
+
expect(getScheduledTaskById(schedule.id)?.intervalMs).toBe(120000);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("non-lead, non-creator agent can delete a schedule and writes an audit event", async () => {
|
|
134
|
+
const server = buildServer();
|
|
135
|
+
const schedule = createScheduledTask({
|
|
136
|
+
name: `mcp-noncreator-delete-${Date.now()}`,
|
|
137
|
+
intervalMs: 60000,
|
|
138
|
+
taskTemplate: "owned by creator",
|
|
139
|
+
createdByAgentId: creatorId,
|
|
140
|
+
timezone: "UTC",
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const result = await callTool(
|
|
144
|
+
server,
|
|
145
|
+
"delete-schedule",
|
|
146
|
+
{ scheduleId: schedule.id },
|
|
147
|
+
otherAgentId,
|
|
148
|
+
);
|
|
149
|
+
const sc = result.structuredContent as { success: boolean };
|
|
150
|
+
|
|
151
|
+
expect(sc.success).toBe(true);
|
|
152
|
+
expect(getScheduledTaskById(schedule.id)).toBeNull();
|
|
153
|
+
const event = getDb()
|
|
154
|
+
.prepare<{ data: string }, []>(
|
|
155
|
+
"SELECT data FROM events WHERE event = 'schedule.deleted' ORDER BY createdAt DESC LIMIT 1",
|
|
156
|
+
)
|
|
157
|
+
.get();
|
|
158
|
+
expect(JSON.parse(event!.data)).toMatchObject({
|
|
159
|
+
scheduleId: schedule.id,
|
|
160
|
+
deletedByAgentId: otherAgentId,
|
|
161
|
+
createdByAgentId: creatorId,
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
86
165
|
test("regression: { cronExpression: null, intervalMs: null, enabled: false } returns success:false and leaves DB row unchanged", async () => {
|
|
87
166
|
const server = buildServer();
|
|
88
167
|
const schedule = createScheduledTask({
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { registerCredentialBindingsTool } from "./tool";
|