@desplega.ai/agent-swarm 1.113.0 → 1.115.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 -2
- package/dist/{actions-q4n7cz6e.js → actions-17ya5wde.js} +8 -9
- package/dist/{app-es4nzc71.js → app-4f718yk0.js} +6 -6
- package/dist/{assistant-0x8ey0vs.js → assistant-ah7csnjz.js} +19 -18
- package/dist/{boot-reembed-69agkhv6.js → boot-reembed-4jpxhb25.js} +6 -6
- package/dist/{boot-reembed-p0ny05t8.js → boot-reembed-qata34xa.js} +5 -5
- package/dist/{boot-scrub-logs-yybrkmvr.js → boot-scrub-logs-tv48nn3z.js} +4 -4
- package/dist/{claude-adapter-mma9a1yr.js → claude-adapter-3csn5bks.js} +3 -3
- package/dist/{claude-managed-adapter-pv2nbqy4.js → claude-managed-adapter-kysmkz89.js} +2 -2
- package/dist/cli-0pygq9r1.js +429 -0
- package/dist/{cli-hsetkkd3.js → cli-12vb0xg6.js} +33 -9
- package/dist/{cli-ncfgsqbd.js → cli-15y7qhzp.js} +262 -33
- package/dist/{cli-7ga43spr.js → cli-1yjrbg1f.js} +1 -1
- package/dist/{cli-36ymmpmp.js → cli-3samkyap.js} +2 -2
- package/dist/cli-52fg7s7x.js +269 -0
- package/dist/{cli-hjhvekf4.js → cli-65ekje01.js} +11 -8
- package/dist/{cli-h3k622d0.js → cli-6aq8xhvm.js} +90 -3
- package/dist/{cli-f3qa069v.js → cli-7xje5amz.js} +1 -1
- package/dist/cli-86fdnerz.js +405 -0
- package/dist/{cli-9cy6nc5m.js → cli-8fjwym53.js} +1 -1
- package/dist/{cli-c3frqf65.js → cli-b23axgz4.js} +3 -3
- package/dist/{cli-a0t7vapf.js → cli-bc89my0z.js} +1 -1
- package/dist/{cli-bnzbn8j8.js → cli-beea4a3y.js} +73 -24
- package/dist/{cli-q21d49ac.js → cli-bw80ck94.js} +1 -1
- package/dist/{cli-pwc5e83c.js → cli-cchksf5q.js} +12 -7
- package/dist/{cli-30bbaveh.js → cli-cmw9rzy5.js} +2 -2
- package/dist/{cli-y6mrptcn.js → cli-cnwssmsr.js} +1 -1
- package/dist/{cli-r5g6ngtn.js → cli-dcca6j8m.js} +46 -5
- package/dist/{cli-tkmm5nf9.js → cli-e61fs2hp.js} +59 -2
- package/dist/{cli-6wscj9z0.js → cli-e7306swv.js} +14 -14
- package/dist/{cli-dcfyyh3t.js → cli-ftyf58xj.js} +2 -2
- package/dist/{cli-4xyj9jtq.js → cli-hrhye2rm.js} +1062 -478
- package/dist/cli-jkhwmmgg.js +136 -0
- package/dist/{cli-y4ycrnjc.js → cli-k3y4z3h5.js} +1 -1
- package/dist/{cli-6xd0bvf8.js → cli-ma04faw5.js} +1 -1
- package/dist/{cli-2fc97xm5.js → cli-mnbgv3w7.js} +4 -4
- package/dist/{cli-trmapn9k.js → cli-pet3aev6.js} +95 -10
- package/dist/{cli-z46pnksz.js → cli-q1rx7g0d.js} +175 -28
- package/dist/{cli-qwgtacd6.js → cli-r9f6ngrf.js} +1244 -336
- package/dist/{cli-gtkqc144.js → cli-shvaxw27.js} +13 -7
- package/dist/{cli-ywez6yps.js → cli-sq7f72d6.js} +1 -1
- package/dist/cli-t0vqevnm.js +393 -0
- package/dist/{cli-tbw13sx8.js → cli-tece6zgm.js} +1 -1
- package/dist/{cli-bgvskydy.js → cli-tffvrh6j.js} +1 -1
- package/dist/cli-wh91zby6.js +187222 -0
- package/dist/{cli-anrj584m.js → cli-xz9aq0rf.js} +1 -1
- package/dist/{cli-mq580e06.js → cli-zsrdty74.js} +18 -18
- package/dist/cli.js +32 -15
- package/dist/{codex-adapter-5cmy858w.js → codex-adapter-de3w0nch.js} +10 -10
- package/dist/{codex-session-runner-46q8mrba.js → codex-session-runner-bnw8cysk.js} +10 -10
- package/dist/{commands-z5dbwta3.js → commands-hch4e2k1.js} +4 -4
- package/dist/{db-bbgahh4z.js → db-p3h2j06x.js} +14 -4
- package/dist/{handlers-6b44317z.js → handlers-9er4zx9q.js} +17 -31
- package/dist/{hook-sbp5fmps.js → hook-4axs756z.js} +7 -7
- package/dist/{http-2xz43gz3.js → http-809fvtz2.js} +1934 -886
- package/dist/{index-wz1ea15p.js → index-2dgfj9av.js} +3 -3
- package/dist/{index-nhat35em.js → index-47em3d2n.js} +14 -11
- package/dist/{index-5ghxn8s6.js → index-9fqaktjr.js} +12 -9
- package/dist/{index-0gzmqj4k.js → index-9ht8gr56.js} +13 -10
- package/dist/{index-vfedgdw6.js → index-rm85cfdx.js} +368 -40
- package/dist/{index-ykvs43z4.js → index-t2snazez.js} +4 -4
- package/dist/{keepalive-62gfj91d.js → keepalive-3cps6e3e.js} +7 -6
- package/dist/{lead-ag0akn0v.js → lead-fkk95wgq.js} +36 -32
- package/dist/{maintenance-xxrrzk5x.js → maintenance-fweqh0yj.js} +6 -6
- package/dist/{mistral-conversations-26yrnbf1.js → mistral-conversations-7gqnz2wt.js} +7 -7
- package/dist/oauth-refresh-sweep-sycqtbph.js +114 -0
- package/dist/{onboard-64zyfcbs.js → onboard-cy0rkgvx.js} +2 -2
- package/dist/{opencode-adapter-mqsxr170.js → opencode-adapter-p6bxnbm4.js} +2 -2
- package/dist/{otel-zjd9zaxs.js → otel-g299k41b.js} +2 -2
- package/dist/{otel-impl-b86qseaa.js → otel-impl-wbns8hf7.js} +1 -1
- package/dist/{pi-mono-adapter-kxjxpfgy.js → pi-mono-adapter-05sck1ds.js} +17 -109
- package/dist/{pricing-refresh-cgtkffpa.js → pricing-refresh-0hvazmkf.js} +6 -6
- package/dist/rbac-roles-kfesjy9y.js +28 -0
- package/dist/rbac-roles-mwcwhbxc.js +283 -0
- package/dist/{seed-pricing-1r1y1xkq.js → seed-pricing-h2kdq4cq.js} +5 -5
- package/dist/{setup-mkcgkjf7.js → setup-8506q97w.js} +2 -2
- package/dist/{worker-351sze63.js → worker-4r5c6zyb.js} +36 -32
- package/openapi.json +1327 -20
- package/package.json +3 -3
- package/src/agentmail/handlers.ts +16 -6
- package/src/agentmail/templates.ts +25 -5
- package/src/be/audit-user.ts +28 -4
- package/src/be/db-queries/oauth.ts +42 -0
- package/src/be/db.ts +314 -28
- package/src/be/identity.ts +79 -0
- package/src/be/mcp-proxy.ts +132 -0
- package/src/be/migrations/109_rbac_roles.sql +79 -0
- package/src/be/migrations/111_oauth_credential_bindings.sql +6 -0
- package/src/be/migrations/112_script_connections_graphql.sql +96 -0
- package/src/be/migrations/113_task_routing_affinity.sql +9 -0
- package/src/be/migrations/114_backfill_gpt_5_6_pricing.sql +15 -0
- package/src/be/modelsdev-cache.json +135 -0
- package/src/be/oauth-credential-bindings.ts +35 -0
- package/src/be/oauth-refresh-sweep.ts +159 -0
- package/src/be/rbac-audit.ts +52 -13
- package/src/be/rbac-roles.ts +424 -0
- package/src/be/script-connections.ts +459 -40
- package/src/be/script-credential-broker.ts +29 -3
- package/src/be/scripts/typecheck.ts +51 -7
- package/src/be/seed-scripts/catalog/catalog-report.inline.ts +325 -24
- package/src/be/seed-scripts/catalog/catalog-report.ts +325 -24
- package/src/be/users.ts +24 -0
- package/src/cli.tsx +17 -0
- package/src/github/handlers.ts +10 -9
- package/src/github/templates.ts +45 -9
- package/src/gitlab/handlers.ts +20 -8
- package/src/gitlab/templates.ts +18 -6
- package/src/heartbeat/heartbeat.ts +143 -21
- package/src/http/all-routes.ts +3 -0
- package/src/http/approval-requests.ts +12 -0
- package/src/http/config.ts +44 -10
- package/src/http/core.ts +30 -3
- package/src/http/favorites.ts +1 -5
- package/src/http/index.ts +25 -1
- package/src/http/mcp-oauth.ts +2 -0
- package/src/http/mcp-servers.ts +44 -3
- package/src/http/oauth-generic.ts +105 -0
- package/src/http/poll.ts +20 -3
- package/src/http/script-connection-proxy.ts +116 -0
- package/src/http/script-connections.ts +1657 -0
- package/src/http/scripts.ts +27 -7
- package/src/http/skills.ts +11 -0
- package/src/http/tasks.ts +27 -13
- package/src/http/x.ts +6 -2
- package/src/integrations/kapso/inbound.ts +23 -6
- package/src/jira/sync.ts +111 -17
- package/src/jira/templates.ts +10 -2
- package/src/mcp-client/http-client.ts +194 -0
- package/src/oauth/app-validation.ts +16 -0
- package/src/oauth/ensure-token.ts +69 -14
- package/src/oauth/mcp-wrapper.ts +15 -0
- package/src/oauth/wrapper.ts +48 -20
- package/src/prompts/session-templates.ts +3 -3
- package/src/providers/codex-models.ts +100 -30
- package/src/providers/modelsdev-reasoning.json +51 -0
- package/src/providers/pi-mono-mcp-client.ts +2 -133
- package/src/providers/reasoning-effort.ts +11 -5
- package/src/rbac/admission.ts +68 -0
- package/src/rbac/index.ts +2 -0
- package/src/rbac/legacy-policy.ts +12 -0
- package/src/rbac/permissions.ts +48 -0
- package/src/scheduler/scheduler.ts +9 -1
- package/src/scripts-runtime/api-client.ts +151 -19
- package/src/scripts-runtime/api-types.ts +38 -6
- package/src/scripts-runtime/credential-broker/default-bindings.ts +1 -0
- package/src/scripts-runtime/credential-broker/types.ts +8 -0
- package/src/scripts-runtime/ctx.ts +11 -1
- package/src/scripts-runtime/eval-harness.ts +5 -1
- package/src/scripts-runtime/executors/types.ts +2 -1
- package/src/scripts-runtime/loader.ts +3 -1
- package/src/scripts-runtime/mcp-client.ts +87 -0
- package/src/scripts-runtime/sdk-allowlist.ts +1 -0
- package/src/scripts-runtime/types/stdlib.d.ts +6 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +19 -0
- package/src/server-user.ts +114 -59
- package/src/server.ts +12 -0
- package/src/slack/assistant.ts +14 -6
- package/src/slack/enrich.ts +17 -0
- package/src/slack/handlers.ts +8 -22
- package/src/slack/responses.ts +91 -1
- package/src/slack/thread-buffer.ts +6 -3
- package/src/slack/watcher.ts +9 -6
- package/src/tasks/worker-follow-up.ts +100 -3
- package/src/tests/agentmail-handlers.test.ts +32 -3
- package/src/tests/approval-requests.test.ts +61 -0
- package/src/tests/audit-user.test.ts +47 -1
- package/src/tests/codex-adapter.test.ts +46 -23
- package/src/tests/credential-broker.test.ts +5 -2
- package/src/tests/delete-page-tool.test.ts +197 -0
- package/src/tests/github-handlers.test.ts +28 -2
- package/src/tests/gitlab-handlers.test.ts +125 -0
- package/src/tests/heartbeat.test.ts +48 -3
- package/src/tests/identity.test.ts +117 -0
- package/src/tests/jira-sync.test.ts +99 -1
- package/src/tests/kapso-inbound.test.ts +7 -0
- package/src/tests/mcp-tools-user.test.ts +60 -6
- package/src/tests/model-control.test.ts +8 -2
- package/src/tests/oauth-credential-bindings.test.ts +490 -0
- package/src/tests/oauth-refresh-sweep.test.ts +160 -0
- package/src/tests/pool-affinity.test.ts +401 -0
- package/src/tests/prompt-template-remaining.test.ts +2 -2
- package/src/tests/rbac-admission-e2e.test.ts +386 -0
- package/src/tests/rbac-admission.test.ts +492 -0
- package/src/tests/rbac-e2e-helpers.ts +3 -0
- package/src/tests/rbac-engine.test.ts +50 -0
- package/src/tests/rbac-mcp-admission-e2e.test.ts +362 -0
- package/src/tests/rbac-roles.test.ts +328 -0
- package/src/tests/reasoning-effort.test.ts +15 -2
- package/src/tests/schedule-target-type.test.ts +50 -0
- package/src/tests/scheduled-tasks.test.ts +40 -0
- package/src/tests/script-apis-mcp.test.ts +37 -13
- package/src/tests/script-connections-http.test.ts +1158 -0
- package/src/tests/script-connections-mcp.test.ts +894 -0
- package/src/tests/script-connections.test.ts +754 -6
- package/src/tests/scripts-external-api.test.ts +49 -1
- package/src/tests/scripts-runtime-secret-egress.test.ts +1 -0
- package/src/tests/scripts-runtime.test.ts +291 -0
- package/src/tests/seed-scripts.test.ts +36 -0
- package/src/tests/slack-identity-resolution.test.ts +37 -3
- package/src/tests/slack-inline-output.test.ts +65 -0
- package/src/tests/slack-thread-buffer.test.ts +28 -0
- package/src/tests/tool-annotations.test.ts +1 -0
- package/src/tests/user-token-rest-auth.test.ts +140 -1
- package/src/tests/workflow-swarm-script.test.ts +44 -0
- package/src/tests/workflow-triggers-v2.test.ts +300 -1
- package/src/tools/credential-bindings/tool.ts +277 -18
- package/src/tools/delete-page.ts +145 -0
- package/src/tools/mcp-servers/mcp-server-delete.ts +7 -4
- package/src/tools/request-human-input.ts +2 -0
- package/src/tools/resolve-user.ts +77 -27
- package/src/tools/script-apis.ts +62 -3
- package/src/tools/script-connections/tool.ts +275 -31
- package/src/tools/send-task.ts +15 -1
- package/src/tools/task-action.ts +34 -2
- package/src/tools/templates.ts +53 -1
- package/src/tools/tool-config.ts +2 -1
- package/src/tools/utils.ts +2 -0
- package/src/tools/workflows/create-workflow.ts +8 -1
- package/src/tools/workflows/update-workflow.ts +11 -2
- package/src/types.ts +90 -11
- package/src/utils/scoped-resource.ts +25 -0
- package/src/workflows/executors/human-in-the-loop.ts +1 -0
- package/src/workflows/executors/swarm-script.ts +7 -1
- package/src/workflows/triggers.ts +146 -18
- package/templates/skills/swarm-scripts/content.md +33 -8
- package/dist/cli-bgef578c.js +0 -771
- package/dist/cli-c9mtm09b.js +0 -43
- package/dist/cli-ceewqajw.js +0 -1698
- package/dist/{anthropic-messages-8nc6r2dw.js → anthropic-messages-kq3pjhbp.js} +6 -6
- package/dist/{azure-openai-responses-6yfzw3qw.js → azure-openai-responses-gm1ejtmn.js} +7 -7
- package/dist/{cli-vbraamxr.js → cli-6b4wwmm8.js} +9 -9
- package/dist/{cli-2r7s8jhp.js → cli-sznberje.js} +3 -3
- package/dist/{google-generative-ai-vfq6ymz9.js → google-generative-ai-hyrra2jm.js} +3 -3
- package/dist/{google-vertex-mj5y5rfj.js → google-vertex-8bs2pzgn.js} +3 -3
- package/dist/{openai-codex-responses-8h6kvfv8.js → openai-codex-responses-3nkvje79.js} +4 -4
- package/dist/{openai-completions-x6kjnqjd.js → openai-completions-0asftkwg.js} +11 -11
- package/dist/{openai-responses-7jwyz3ds.js → openai-responses-pmatek45.js} +10 -10
|
@@ -1,8 +1,16 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
1
2
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { readdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
2
6
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
7
|
import { createAgent, deleteSwarmConfig, getDb, upsertSwarmConfig } from "../be/db";
|
|
8
|
+
import { runMigrations } from "../be/migrations/runner";
|
|
4
9
|
import {
|
|
10
|
+
fetchOpenapiSpec,
|
|
5
11
|
getScriptApiConnectionDescriptors,
|
|
12
|
+
refreshScriptConnection,
|
|
13
|
+
setOpenapiSpecFetchForTesting,
|
|
6
14
|
upsertCredentialBinding,
|
|
7
15
|
upsertScriptConnection,
|
|
8
16
|
} from "../be/script-connections";
|
|
@@ -17,6 +25,7 @@ const createdConfigIds: string[] = [];
|
|
|
17
25
|
const originalFetch = globalThis.fetch;
|
|
18
26
|
const savedEnv = { ...process.env };
|
|
19
27
|
const resources = { memoryMb: 2048, cpuTimeSec: 20, maxStdoutBytes: 1_048_576 };
|
|
28
|
+
const MIGRATION_REBUILD_DB_PATH = "./test-script-connections-graphql-migration.sqlite";
|
|
20
29
|
|
|
21
30
|
type RegisteredTool = {
|
|
22
31
|
handler: (args: unknown, extra: unknown) => Promise<unknown>;
|
|
@@ -32,6 +41,61 @@ function scriptConnectionsTool() {
|
|
|
32
41
|
return tool;
|
|
33
42
|
}
|
|
34
43
|
|
|
44
|
+
function removeDbFiles(path: string) {
|
|
45
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
46
|
+
try {
|
|
47
|
+
unlinkSync(path + suffix);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function migrationsDir() {
|
|
55
|
+
return new URL("../be/migrations", import.meta.url).pathname;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function migrationSql(file: string) {
|
|
59
|
+
return readFileSync(join(migrationsDir(), file), "utf-8");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function markMigrationsAppliedThrough(database: Database, throughVersion: number) {
|
|
63
|
+
database.run(`
|
|
64
|
+
CREATE TABLE IF NOT EXISTS _migrations (
|
|
65
|
+
version INTEGER PRIMARY KEY,
|
|
66
|
+
name TEXT NOT NULL,
|
|
67
|
+
applied_at TEXT NOT NULL,
|
|
68
|
+
checksum TEXT NOT NULL
|
|
69
|
+
)
|
|
70
|
+
`);
|
|
71
|
+
const files = readdirSync(migrationsDir())
|
|
72
|
+
.filter((file) => file.endsWith(".sql"))
|
|
73
|
+
.sort();
|
|
74
|
+
const insert = database.prepare(
|
|
75
|
+
"INSERT INTO _migrations (version, name, applied_at, checksum) VALUES (?, ?, ?, ?)",
|
|
76
|
+
);
|
|
77
|
+
for (const file of files) {
|
|
78
|
+
const version = Number.parseInt(file.split("_")[0] ?? "0", 10);
|
|
79
|
+
if (!version || version > throughVersion) continue;
|
|
80
|
+
const sql = migrationSql(file);
|
|
81
|
+
const checksum = createHash("sha256").update(sql).digest("hex");
|
|
82
|
+
insert.run(version, file.replace(".sql", ""), new Date().toISOString(), checksum);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function markMigrationApplied(database: Database, file: string) {
|
|
87
|
+
const sql = migrationSql(file);
|
|
88
|
+
const version = Number.parseInt(file.split("_")[0] ?? "0", 10);
|
|
89
|
+
database
|
|
90
|
+
.prepare("INSERT INTO _migrations (version, name, applied_at, checksum) VALUES (?, ?, ?, ?)")
|
|
91
|
+
.run(
|
|
92
|
+
version,
|
|
93
|
+
file.replace(".sql", ""),
|
|
94
|
+
new Date().toISOString(),
|
|
95
|
+
createHash("sha256").update(sql).digest("hex"),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
35
99
|
function meta(agentId: string) {
|
|
36
100
|
return {
|
|
37
101
|
sessionId: "script-connections-test-session",
|
|
@@ -39,6 +103,32 @@ function meta(agentId: string) {
|
|
|
39
103
|
};
|
|
40
104
|
}
|
|
41
105
|
|
|
106
|
+
type OpenapiSpecFixture = {
|
|
107
|
+
body: string;
|
|
108
|
+
etag: string;
|
|
109
|
+
requests: Array<{ accept: string | null; ifNoneMatch: string | null }>;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const openapiSpecFixtures = new Map<string, OpenapiSpecFixture>();
|
|
113
|
+
|
|
114
|
+
const fixtureOpenapiFetch: typeof fetch = async (input, init) => {
|
|
115
|
+
const url = new URL(input instanceof Request ? input.url : String(input));
|
|
116
|
+
const fixture = openapiSpecFixtures.get(url.toString());
|
|
117
|
+
if (!fixture) return new Response("not found", { status: 404 });
|
|
118
|
+
const headers = new Headers(input instanceof Request ? input.headers : init?.headers);
|
|
119
|
+
const ifNoneMatch = headers.get("if-none-match");
|
|
120
|
+
fixture.requests.push({
|
|
121
|
+
accept: headers.get("accept"),
|
|
122
|
+
ifNoneMatch,
|
|
123
|
+
});
|
|
124
|
+
if (ifNoneMatch === fixture.etag) {
|
|
125
|
+
return new Response(null, { status: 304, headers: { ETag: fixture.etag } });
|
|
126
|
+
}
|
|
127
|
+
return new Response(fixture.body, {
|
|
128
|
+
headers: { "Content-Type": "application/json", ETag: fixture.etag },
|
|
129
|
+
});
|
|
130
|
+
};
|
|
131
|
+
|
|
42
132
|
const openapiSpec = JSON.stringify({
|
|
43
133
|
openapi: "3.1.0",
|
|
44
134
|
info: { title: "Vendor", version: "1.0.0" },
|
|
@@ -109,13 +199,68 @@ const openapiSpec = JSON.stringify({
|
|
|
109
199
|
},
|
|
110
200
|
});
|
|
111
201
|
|
|
202
|
+
function specWithExtraOperation(operationId: string) {
|
|
203
|
+
const spec = JSON.parse(openapiSpec) as {
|
|
204
|
+
paths: Record<string, Record<string, unknown>>;
|
|
205
|
+
};
|
|
206
|
+
spec.paths["/orgs/{org}/repos"] = {
|
|
207
|
+
get: {
|
|
208
|
+
operationId,
|
|
209
|
+
parameters: [{ name: "org", in: "path", required: true, schema: { type: "string" } }],
|
|
210
|
+
responses: {
|
|
211
|
+
"200": {
|
|
212
|
+
description: "repos",
|
|
213
|
+
content: {
|
|
214
|
+
"application/json": {
|
|
215
|
+
schema: {
|
|
216
|
+
type: "array",
|
|
217
|
+
items: {
|
|
218
|
+
type: "object",
|
|
219
|
+
required: ["name"],
|
|
220
|
+
properties: { name: { type: "string" } },
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
return JSON.stringify(spec);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function serveOpenapiSpec(initialBody: string, initialEtag = '"v1"') {
|
|
233
|
+
const fixtureUrl = `http://script-openapi.test/${crypto.randomUUID()}/openapi.json`;
|
|
234
|
+
const fixture: OpenapiSpecFixture = {
|
|
235
|
+
body: initialBody,
|
|
236
|
+
etag: initialEtag,
|
|
237
|
+
requests: [],
|
|
238
|
+
};
|
|
239
|
+
openapiSpecFixtures.set(fixtureUrl, fixture);
|
|
240
|
+
return {
|
|
241
|
+
requests: fixture.requests,
|
|
242
|
+
url: fixtureUrl,
|
|
243
|
+
baseUrl: new URL(fixtureUrl).origin,
|
|
244
|
+
setBody(nextBody: string, nextEtag: string) {
|
|
245
|
+
fixture.body = nextBody;
|
|
246
|
+
fixture.etag = nextEtag;
|
|
247
|
+
},
|
|
248
|
+
stop() {
|
|
249
|
+
openapiSpecFixtures.delete(fixtureUrl);
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
112
254
|
beforeEach(() => {
|
|
113
255
|
process.env.AGENT_SWARM_API_KEY = "script-connections-test-key";
|
|
114
256
|
delete process.env.API_KEY;
|
|
115
257
|
process.env.MCP_BASE_URL = "http://localhost:3013";
|
|
258
|
+
setOpenapiSpecFetchForTesting(fixtureOpenapiFetch);
|
|
116
259
|
});
|
|
117
260
|
|
|
118
261
|
afterEach(() => {
|
|
262
|
+
setOpenapiSpecFetchForTesting(null);
|
|
263
|
+
openapiSpecFixtures.clear();
|
|
119
264
|
globalThis.fetch = originalFetch;
|
|
120
265
|
for (const key of Object.keys(process.env)) {
|
|
121
266
|
if (!(key in savedEnv)) delete process.env[key];
|
|
@@ -134,10 +279,11 @@ afterEach(() => {
|
|
|
134
279
|
for (const id of createdConfigIds.splice(0)) {
|
|
135
280
|
deleteSwarmConfig(id);
|
|
136
281
|
}
|
|
282
|
+
removeDbFiles(MIGRATION_REBUILD_DB_PATH);
|
|
137
283
|
});
|
|
138
284
|
|
|
139
285
|
describe("script connections", () => {
|
|
140
|
-
test("relational credential bindings are resolved before legacy JSON config", () => {
|
|
286
|
+
test("relational credential bindings are resolved before legacy JSON config", async () => {
|
|
141
287
|
const binding = upsertCredentialBinding({
|
|
142
288
|
configKey: "REL_VENDOR_KEY",
|
|
143
289
|
allowedHosts: ["api.vendor.test"],
|
|
@@ -152,7 +298,7 @@ describe("script connections", () => {
|
|
|
152
298
|
});
|
|
153
299
|
createdConfigIds.push(secretConfig.id);
|
|
154
300
|
|
|
155
|
-
const egressSecrets = buildScriptCredentialBindings({});
|
|
301
|
+
const egressSecrets = await buildScriptCredentialBindings({});
|
|
156
302
|
|
|
157
303
|
expect(egressSecrets).toContainEqual(
|
|
158
304
|
expect.objectContaining({
|
|
@@ -181,14 +327,14 @@ describe("script connections", () => {
|
|
|
181
327
|
expect(second.allowedHosts).toEqual(["new.vendor.test"]);
|
|
182
328
|
});
|
|
183
329
|
|
|
184
|
-
test("OpenAPI connections generate full ctx.api method, args, and response types", () => {
|
|
330
|
+
test("OpenAPI connections generate full ctx.api method, args, and response types", async () => {
|
|
185
331
|
const binding = upsertCredentialBinding({
|
|
186
332
|
configKey: "TYPE_VENDOR_KEY",
|
|
187
333
|
allowedHosts: ["api.vendor.test"],
|
|
188
334
|
headerTemplate: "Authorization: Bearer [REDACTED:TYPE_VENDOR_KEY]",
|
|
189
335
|
});
|
|
190
336
|
createdBindingIds.push(binding.id);
|
|
191
|
-
const connection = upsertScriptConnection({
|
|
337
|
+
const connection = await upsertScriptConnection({
|
|
192
338
|
slug: "vendorApi",
|
|
193
339
|
kind: "openapi",
|
|
194
340
|
baseUrl: "https://api.vendor.test",
|
|
@@ -208,15 +354,23 @@ describe("script connections", () => {
|
|
|
208
354
|
const created = await ctx.api.vendorApi.createRepo({
|
|
209
355
|
body: { name: "agent-swarm", private: false },
|
|
210
356
|
});
|
|
357
|
+
const raw = await ctx.api.vendorApi.getRepo({
|
|
358
|
+
path: { owner: "desplega-ai", repo: "agent-swarm" },
|
|
359
|
+
}, { raw: true });
|
|
211
360
|
const name: string = repo.full_name;
|
|
212
361
|
const createdName: string = created.full_name;
|
|
213
362
|
const isPrivate: boolean | undefined = repo.private;
|
|
214
|
-
|
|
363
|
+
const rawStatus: number = raw.status;
|
|
364
|
+
const rawBody: ArrayBuffer = await raw.response.arrayBuffer();
|
|
365
|
+
return { name, createdName, isPrivate, rawStatus, rawBody };
|
|
215
366
|
};
|
|
216
367
|
export default main;
|
|
217
368
|
`;
|
|
218
369
|
|
|
219
370
|
expect(typecheckScript(source)).toEqual({ ok: true });
|
|
371
|
+
expect(connection.generatedTypes).toContain(
|
|
372
|
+
"getRepo(args: VendorApiGetRepoArgs, options: ScriptApiRawOptions): Promise<ScriptApiRawResult>;",
|
|
373
|
+
);
|
|
220
374
|
const descriptor = getScriptApiConnectionDescriptors().find(
|
|
221
375
|
(candidate) => candidate.slug === "vendorApi",
|
|
222
376
|
);
|
|
@@ -246,6 +400,493 @@ describe("script connections", () => {
|
|
|
246
400
|
});
|
|
247
401
|
});
|
|
248
402
|
|
|
403
|
+
test("GET operations never generate a body even when the spec declares one", async () => {
|
|
404
|
+
// readme.io-exported specs (e.g. Notion on apis.guru) declare form-encoded
|
|
405
|
+
// requestBody on GET operations; fetch() rejects GET bodies.
|
|
406
|
+
const spec = JSON.stringify({
|
|
407
|
+
openapi: "3.0.0",
|
|
408
|
+
info: { title: "Readmeio", version: "1.0.0" },
|
|
409
|
+
paths: {
|
|
410
|
+
"/v1/users/{id}": {
|
|
411
|
+
get: {
|
|
412
|
+
operationId: "retrieveAUser",
|
|
413
|
+
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
|
|
414
|
+
requestBody: {
|
|
415
|
+
content: {
|
|
416
|
+
"application/x-www-form-urlencoded": { schema: { type: "object" } },
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
responses: { "200": { description: "ok" } },
|
|
420
|
+
},
|
|
421
|
+
post: {
|
|
422
|
+
operationId: "updateAUser",
|
|
423
|
+
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
|
|
424
|
+
requestBody: {
|
|
425
|
+
content: { "application/json": { schema: { type: "object" } } },
|
|
426
|
+
},
|
|
427
|
+
responses: { "200": { description: "ok" } },
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
});
|
|
432
|
+
const connection = await upsertScriptConnection({
|
|
433
|
+
slug: "readmeio",
|
|
434
|
+
kind: "openapi",
|
|
435
|
+
baseUrl: "https://api.readmeio.test",
|
|
436
|
+
openapiSpecJson: spec,
|
|
437
|
+
});
|
|
438
|
+
createdConnectionIds.push(connection.id);
|
|
439
|
+
|
|
440
|
+
expect(connection.generationError).toBeNull();
|
|
441
|
+
const runtime = JSON.parse(connection.generatedRuntimeJson ?? "{}") as {
|
|
442
|
+
operations: Array<{ name: string; hasBody: boolean }>;
|
|
443
|
+
};
|
|
444
|
+
const byName = new Map(runtime.operations.map((op) => [op.name, op]));
|
|
445
|
+
expect(byName.get("retrieveAUser")?.hasBody).toBe(false);
|
|
446
|
+
expect(byName.get("updateAUser")?.hasBody).toBe(true);
|
|
447
|
+
// the generated GET args type must not demand a body either
|
|
448
|
+
expect(connection.generatedTypes).not.toMatch(/RetrieveAUserArgs = \{[^}]*body/);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
test("GraphQL connections generate ctx.api descriptor and graphql method types", async () => {
|
|
452
|
+
const binding = upsertCredentialBinding({
|
|
453
|
+
configKey: "GRAPHQL_VENDOR_KEY",
|
|
454
|
+
allowedHosts: ["countries.vendor.test"],
|
|
455
|
+
headerTemplate: "Authorization: Bearer [REDACTED:GRAPHQL_VENDOR_KEY]",
|
|
456
|
+
});
|
|
457
|
+
createdBindingIds.push(binding.id);
|
|
458
|
+
|
|
459
|
+
const connection = await upsertScriptConnection({
|
|
460
|
+
slug: "countries",
|
|
461
|
+
kind: "graphql",
|
|
462
|
+
baseUrl: "https://countries.vendor.test/graphql",
|
|
463
|
+
allowedHosts: ["countries.vendor.test"],
|
|
464
|
+
credentialBindingId: binding.id,
|
|
465
|
+
});
|
|
466
|
+
createdConnectionIds.push(connection.id);
|
|
467
|
+
|
|
468
|
+
expect(connection.generationError).toBeNull();
|
|
469
|
+
expect(connection.generatedRuntimeJson).toBe(
|
|
470
|
+
JSON.stringify({
|
|
471
|
+
slug: "countries",
|
|
472
|
+
kind: "graphql",
|
|
473
|
+
baseUrl: "https://countries.vendor.test/graphql",
|
|
474
|
+
credential: {
|
|
475
|
+
configKey: "GRAPHQL_VENDOR_KEY",
|
|
476
|
+
headerTemplate: "Authorization: Bearer [REDACTED:GRAPHQL_VENDOR_KEY]",
|
|
477
|
+
},
|
|
478
|
+
}),
|
|
479
|
+
);
|
|
480
|
+
expect(connection.generatedTypes).toContain("interface CountriesApi");
|
|
481
|
+
expect(connection.generatedTypes).toContain(
|
|
482
|
+
"graphql<T = JsonValue>(query: string, variables?: Record<string, JsonValue>): Promise<T>;",
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
const descriptor = getScriptApiConnectionDescriptors().find(
|
|
486
|
+
(candidate) => candidate.slug === "countries",
|
|
487
|
+
);
|
|
488
|
+
expect(descriptor).toEqual({
|
|
489
|
+
slug: "countries",
|
|
490
|
+
kind: "graphql",
|
|
491
|
+
baseUrl: "https://countries.vendor.test/graphql",
|
|
492
|
+
credential: {
|
|
493
|
+
configKey: "GRAPHQL_VENDOR_KEY",
|
|
494
|
+
headerTemplate: "Authorization: Bearer [REDACTED:GRAPHQL_VENDOR_KEY]",
|
|
495
|
+
},
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
const source = `
|
|
499
|
+
import type { ScriptMain } from "swarm-sdk";
|
|
500
|
+
const main: ScriptMain = async (_args, ctx) => {
|
|
501
|
+
const result = await ctx.api.countries.graphql<{ country: { name: string; capital: string } }>(
|
|
502
|
+
"query Country($code: ID!) { country(code: $code) { name capital } }",
|
|
503
|
+
{ code: "UA" },
|
|
504
|
+
);
|
|
505
|
+
const name: string = result.country.name;
|
|
506
|
+
const capital: string = result.country.capital;
|
|
507
|
+
return { name, capital };
|
|
508
|
+
};
|
|
509
|
+
export default main;
|
|
510
|
+
`;
|
|
511
|
+
|
|
512
|
+
expect(typecheckScript(source)).toEqual({ ok: true });
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
test("migration 112 rebuild preserves existing script connection rows", () => {
|
|
516
|
+
removeDbFiles(MIGRATION_REBUILD_DB_PATH);
|
|
517
|
+
const database = new Database(MIGRATION_REBUILD_DB_PATH, { create: true });
|
|
518
|
+
try {
|
|
519
|
+
database.run("CREATE TABLE users (id TEXT PRIMARY KEY)");
|
|
520
|
+
database.run("CREATE TABLE mcp_servers (id TEXT PRIMARY KEY)");
|
|
521
|
+
database.run("CREATE TABLE agent_tasks (id TEXT PRIMARY KEY)");
|
|
522
|
+
database.exec(migrationSql("101_script_connections.sql"));
|
|
523
|
+
database.exec(migrationSql("111_oauth_credential_bindings.sql"));
|
|
524
|
+
markMigrationsAppliedThrough(database, 111);
|
|
525
|
+
// This fixture intentionally starts from a partial schema to exercise the
|
|
526
|
+
// 112 rebuild only; unrelated later migrations may depend on tables that
|
|
527
|
+
// do not exist in the fixture.
|
|
528
|
+
markMigrationApplied(database, "114_backfill_gpt_5_6_pricing.sql");
|
|
529
|
+
|
|
530
|
+
const id = crypto.randomUUID();
|
|
531
|
+
const now = new Date().toISOString();
|
|
532
|
+
database.run(
|
|
533
|
+
`INSERT INTO script_connections (
|
|
534
|
+
id, slug, display_name, kind, scope, scope_id, base_url, allowed_hosts_json,
|
|
535
|
+
credential_binding_id, openapi_spec_source_kind, openapi_spec_source,
|
|
536
|
+
openapi_spec_json, openapi_spec_etag, openapi_spec_fetched_at, mcp_server_id,
|
|
537
|
+
generated_types, generated_runtime_json, generated_at, generation_error, enabled,
|
|
538
|
+
version, created_at, updated_at, created_by, updated_by
|
|
539
|
+
)
|
|
540
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
541
|
+
[
|
|
542
|
+
id,
|
|
543
|
+
"preGraphql",
|
|
544
|
+
"Pre GraphQL",
|
|
545
|
+
"raw",
|
|
546
|
+
"global",
|
|
547
|
+
null,
|
|
548
|
+
"https://api.vendor.test",
|
|
549
|
+
JSON.stringify(["api.vendor.test"]),
|
|
550
|
+
null,
|
|
551
|
+
null,
|
|
552
|
+
null,
|
|
553
|
+
null,
|
|
554
|
+
null,
|
|
555
|
+
null,
|
|
556
|
+
null,
|
|
557
|
+
"export interface PreGraphqlApi {}",
|
|
558
|
+
JSON.stringify({ slug: "preGraphql", baseUrl: "https://api.vendor.test" }),
|
|
559
|
+
now,
|
|
560
|
+
null,
|
|
561
|
+
1,
|
|
562
|
+
7,
|
|
563
|
+
now,
|
|
564
|
+
now,
|
|
565
|
+
null,
|
|
566
|
+
null,
|
|
567
|
+
],
|
|
568
|
+
);
|
|
569
|
+
|
|
570
|
+
runMigrations(database);
|
|
571
|
+
|
|
572
|
+
const preserved = database
|
|
573
|
+
.prepare<
|
|
574
|
+
{
|
|
575
|
+
slug: string;
|
|
576
|
+
kind: string;
|
|
577
|
+
allowed_hosts_json: string;
|
|
578
|
+
generated_runtime_json: string | null;
|
|
579
|
+
version: number;
|
|
580
|
+
},
|
|
581
|
+
[string]
|
|
582
|
+
>(
|
|
583
|
+
`SELECT slug, kind, allowed_hosts_json, generated_runtime_json, version
|
|
584
|
+
FROM script_connections WHERE id = ?`,
|
|
585
|
+
)
|
|
586
|
+
.get(id);
|
|
587
|
+
expect(preserved).toEqual({
|
|
588
|
+
slug: "preGraphql",
|
|
589
|
+
kind: "raw",
|
|
590
|
+
allowed_hosts_json: JSON.stringify(["api.vendor.test"]),
|
|
591
|
+
generated_runtime_json: JSON.stringify({
|
|
592
|
+
slug: "preGraphql",
|
|
593
|
+
baseUrl: "https://api.vendor.test",
|
|
594
|
+
}),
|
|
595
|
+
version: 7,
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
const indexes = database
|
|
599
|
+
.prepare<{ name: string }, []>(
|
|
600
|
+
"SELECT name FROM sqlite_master WHERE type = 'index' AND tbl_name = 'script_connections'",
|
|
601
|
+
)
|
|
602
|
+
.all()
|
|
603
|
+
.map((row) => row.name);
|
|
604
|
+
expect(indexes).toContain("idx_script_connections_slug_scope");
|
|
605
|
+
expect(indexes).toContain("idx_script_connections_kind_enabled");
|
|
606
|
+
|
|
607
|
+
expect(() => {
|
|
608
|
+
database.run(
|
|
609
|
+
`INSERT INTO script_connections (
|
|
610
|
+
id, slug, kind, scope, base_url, allowed_hosts_json, generated_types,
|
|
611
|
+
generated_runtime_json, generated_at, created_at, updated_at
|
|
612
|
+
)
|
|
613
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
614
|
+
[
|
|
615
|
+
crypto.randomUUID(),
|
|
616
|
+
"postGraphql",
|
|
617
|
+
"graphql",
|
|
618
|
+
"global",
|
|
619
|
+
"https://graphql.vendor.test",
|
|
620
|
+
JSON.stringify(["graphql.vendor.test"]),
|
|
621
|
+
"export interface PostGraphqlApi {}",
|
|
622
|
+
JSON.stringify({ slug: "postGraphql", kind: "graphql" }),
|
|
623
|
+
now,
|
|
624
|
+
now,
|
|
625
|
+
now,
|
|
626
|
+
],
|
|
627
|
+
);
|
|
628
|
+
}).not.toThrow();
|
|
629
|
+
} finally {
|
|
630
|
+
database.close();
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
test("OpenAPI connections can be registered from a spec URL", async () => {
|
|
635
|
+
const server = serveOpenapiSpec(openapiSpec);
|
|
636
|
+
try {
|
|
637
|
+
const suffix = crypto.randomUUID().replace(/-/g, "");
|
|
638
|
+
const connection = await upsertScriptConnection({
|
|
639
|
+
slug: `urlVendor${suffix}`,
|
|
640
|
+
kind: "openapi",
|
|
641
|
+
baseUrl: server.baseUrl,
|
|
642
|
+
openapiSpecUrl: server.url,
|
|
643
|
+
});
|
|
644
|
+
createdConnectionIds.push(connection.id);
|
|
645
|
+
|
|
646
|
+
expect(connection.generationError).toBeNull();
|
|
647
|
+
expect(connection.openapiSpecSourceKind).toBe("url");
|
|
648
|
+
expect(connection.openapiSpecSource).toBe(server.url);
|
|
649
|
+
expect(connection.openapiSpecJson).toBe(JSON.stringify(JSON.parse(openapiSpec)));
|
|
650
|
+
expect(connection.openapiSpecEtag).toBe('"v1"');
|
|
651
|
+
expect(connection.openapiSpecFetchedAt).toBeString();
|
|
652
|
+
expect(server.requests).toEqual([
|
|
653
|
+
{
|
|
654
|
+
accept: "application/json, application/yaml;q=0.9, text/yaml;q=0.8, */*;q=0.5",
|
|
655
|
+
ifNoneMatch: null,
|
|
656
|
+
},
|
|
657
|
+
]);
|
|
658
|
+
expect(connection.generatedTypes).toContain("getRepo");
|
|
659
|
+
} finally {
|
|
660
|
+
server.stop();
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
test("OpenAPI spec fetch rejects redirects to unsafe hosts in production", async () => {
|
|
665
|
+
process.env.NODE_ENV = "production";
|
|
666
|
+
const requests: Array<{ url: string; redirect?: RequestRedirect }> = [];
|
|
667
|
+
setOpenapiSpecFetchForTesting((async (input, init) => {
|
|
668
|
+
const url = input instanceof Request ? input.url : String(input);
|
|
669
|
+
requests.push({ url, redirect: init?.redirect });
|
|
670
|
+
if (url === "https://spec.vendor.test/openapi.json") {
|
|
671
|
+
return new Response(null, {
|
|
672
|
+
status: 302,
|
|
673
|
+
headers: { location: "http://127.0.0.1/spec.json" },
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
return new Response(openapiSpec, { status: 200 });
|
|
677
|
+
}) as typeof fetch);
|
|
678
|
+
|
|
679
|
+
await expect(fetchOpenapiSpec("https://spec.vendor.test/openapi.json")).rejects.toThrow(
|
|
680
|
+
/private IPv4|insecure/,
|
|
681
|
+
);
|
|
682
|
+
expect(requests).toEqual([
|
|
683
|
+
{ url: "https://spec.vendor.test/openapi.json", redirect: "manual" },
|
|
684
|
+
]);
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
test("OpenAPI spec fetch fails closed when NODE_ENV is unset", async () => {
|
|
688
|
+
// Production images often run with NODE_ENV undefined — the SSRF guard
|
|
689
|
+
// must NOT treat that as development.
|
|
690
|
+
delete process.env.NODE_ENV;
|
|
691
|
+
delete process.env.ALLOW_PRIVATE_NETWORK_URLS;
|
|
692
|
+
setOpenapiSpecFetchForTesting((async () => {
|
|
693
|
+
throw new Error("fetch should not be reached for unsafe URLs");
|
|
694
|
+
}) as unknown as typeof fetch);
|
|
695
|
+
|
|
696
|
+
await expect(fetchOpenapiSpec("http://127.0.0.1/openapi.json")).rejects.toThrow(
|
|
697
|
+
/private IPv4|insecure/,
|
|
698
|
+
);
|
|
699
|
+
|
|
700
|
+
// ...and the explicit override re-enables local fetches for dev setups.
|
|
701
|
+
process.env.ALLOW_PRIVATE_NETWORK_URLS = "true";
|
|
702
|
+
setOpenapiSpecFetchForTesting(
|
|
703
|
+
(async () => new Response(openapiSpec, { status: 200 })) as unknown as typeof fetch,
|
|
704
|
+
);
|
|
705
|
+
const fetched = await fetchOpenapiSpec("http://127.0.0.1/openapi.json");
|
|
706
|
+
expect(fetched.status).toBe("fetched");
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
test("YAML specs are accepted from a URL and canonicalized to JSON", async () => {
|
|
710
|
+
const yamlSpec = [
|
|
711
|
+
"openapi: 3.0.0",
|
|
712
|
+
"info:",
|
|
713
|
+
" title: YamlVendor",
|
|
714
|
+
" version: 1.0.0",
|
|
715
|
+
"paths:",
|
|
716
|
+
" /things/{id}:",
|
|
717
|
+
" get:",
|
|
718
|
+
" operationId: getThing",
|
|
719
|
+
" parameters:",
|
|
720
|
+
" - name: id",
|
|
721
|
+
" in: path",
|
|
722
|
+
" required: true",
|
|
723
|
+
" schema:",
|
|
724
|
+
" type: string",
|
|
725
|
+
" responses:",
|
|
726
|
+
' "200":',
|
|
727
|
+
" description: ok",
|
|
728
|
+
"",
|
|
729
|
+
].join("\n");
|
|
730
|
+
const server = serveOpenapiSpec(yamlSpec);
|
|
731
|
+
try {
|
|
732
|
+
const suffix = crypto.randomUUID().replace(/-/g, "");
|
|
733
|
+
const connection = await upsertScriptConnection({
|
|
734
|
+
slug: `yamlVendor${suffix}`,
|
|
735
|
+
kind: "openapi",
|
|
736
|
+
baseUrl: server.baseUrl,
|
|
737
|
+
openapiSpecUrl: server.url,
|
|
738
|
+
});
|
|
739
|
+
createdConnectionIds.push(connection.id);
|
|
740
|
+
|
|
741
|
+
expect(connection.generationError).toBeNull();
|
|
742
|
+
// stored canonically as JSON regardless of the fetched format
|
|
743
|
+
expect(JSON.parse(connection.openapiSpecJson ?? "{}")).toMatchObject({
|
|
744
|
+
openapi: "3.0.0",
|
|
745
|
+
info: { title: "YamlVendor" },
|
|
746
|
+
});
|
|
747
|
+
expect(connection.generatedTypes).toContain("getThing");
|
|
748
|
+
} finally {
|
|
749
|
+
server.stop();
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
test("inline YAML specs are accepted and canonicalized to JSON", async () => {
|
|
754
|
+
const suffix = crypto.randomUUID().replace(/-/g, "");
|
|
755
|
+
const connection = await upsertScriptConnection({
|
|
756
|
+
slug: `yamlInline${suffix}`,
|
|
757
|
+
kind: "openapi",
|
|
758
|
+
baseUrl: "https://api.yaml-inline.test",
|
|
759
|
+
openapiSpecJson: [
|
|
760
|
+
"openapi: 3.0.0",
|
|
761
|
+
"info: { title: InlineYaml, version: 1.0.0 }",
|
|
762
|
+
"paths:",
|
|
763
|
+
" /ping:",
|
|
764
|
+
" get:",
|
|
765
|
+
" operationId: ping",
|
|
766
|
+
" responses:",
|
|
767
|
+
' "200": { description: ok }',
|
|
768
|
+
].join("\n"),
|
|
769
|
+
});
|
|
770
|
+
createdConnectionIds.push(connection.id);
|
|
771
|
+
|
|
772
|
+
expect(connection.generationError).toBeNull();
|
|
773
|
+
expect(JSON.parse(connection.openapiSpecJson ?? "{}")).toMatchObject({
|
|
774
|
+
info: { title: "InlineYaml" },
|
|
775
|
+
});
|
|
776
|
+
expect(connection.generatedTypes).toContain("ping");
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
test("refresh sends If-None-Match and only bumps fetched_at on 304", async () => {
|
|
780
|
+
const server = serveOpenapiSpec(openapiSpec);
|
|
781
|
+
try {
|
|
782
|
+
const suffix = crypto.randomUUID().replace(/-/g, "");
|
|
783
|
+
const connection = await upsertScriptConnection({
|
|
784
|
+
slug: `etagVendor${suffix}`,
|
|
785
|
+
kind: "openapi",
|
|
786
|
+
baseUrl: server.baseUrl,
|
|
787
|
+
openapiSpecUrl: server.url,
|
|
788
|
+
});
|
|
789
|
+
createdConnectionIds.push(connection.id);
|
|
790
|
+
await Bun.sleep(5);
|
|
791
|
+
|
|
792
|
+
const refreshed = await refreshScriptConnection(connection.id);
|
|
793
|
+
|
|
794
|
+
expect(refreshed).toBeDefined();
|
|
795
|
+
expect(refreshed?.version).toBe(connection.version);
|
|
796
|
+
expect(refreshed?.openapiSpecEtag).toBe('"v1"');
|
|
797
|
+
expect(refreshed?.generatedRuntimeJson).toBe(connection.generatedRuntimeJson);
|
|
798
|
+
expect(refreshed?.openapiSpecFetchedAt).not.toBe(connection.openapiSpecFetchedAt);
|
|
799
|
+
const specAccept = "application/json, application/yaml;q=0.9, text/yaml;q=0.8, */*;q=0.5";
|
|
800
|
+
expect(server.requests).toEqual([
|
|
801
|
+
{ accept: specAccept, ifNoneMatch: null },
|
|
802
|
+
{ accept: specAccept, ifNoneMatch: '"v1"' },
|
|
803
|
+
]);
|
|
804
|
+
} finally {
|
|
805
|
+
server.stop();
|
|
806
|
+
}
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
test("refresh updates changed URL specs and regenerates types", async () => {
|
|
810
|
+
const server = serveOpenapiSpec(openapiSpec);
|
|
811
|
+
try {
|
|
812
|
+
const suffix = crypto.randomUUID().replace(/-/g, "");
|
|
813
|
+
const connection = await upsertScriptConnection({
|
|
814
|
+
slug: `changedVendor${suffix}`,
|
|
815
|
+
kind: "openapi",
|
|
816
|
+
baseUrl: server.baseUrl,
|
|
817
|
+
openapiSpecUrl: server.url,
|
|
818
|
+
});
|
|
819
|
+
createdConnectionIds.push(connection.id);
|
|
820
|
+
expect(connection.generatedTypes).not.toContain("listOrgRepos");
|
|
821
|
+
|
|
822
|
+
server.setBody(specWithExtraOperation("listOrgRepos"), '"v2"');
|
|
823
|
+
const refreshed = await refreshScriptConnection(connection.id);
|
|
824
|
+
|
|
825
|
+
expect(refreshed?.version).toBe(connection.version + 1);
|
|
826
|
+
expect(refreshed?.openapiSpecEtag).toBe('"v2"');
|
|
827
|
+
expect(refreshed?.generationError).toBeNull();
|
|
828
|
+
expect(refreshed?.generatedTypes).toContain("listOrgRepos");
|
|
829
|
+
const descriptor = getScriptApiConnectionDescriptors().find(
|
|
830
|
+
(candidate) => candidate.slug === connection.slug,
|
|
831
|
+
);
|
|
832
|
+
expect(descriptor?.operations.some((operation) => operation.name === "listOrgRepos")).toBe(
|
|
833
|
+
true,
|
|
834
|
+
);
|
|
835
|
+
} finally {
|
|
836
|
+
server.stop();
|
|
837
|
+
}
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
test("invalid fetched OpenAPI specs are stored with generation_error", async () => {
|
|
841
|
+
const invalidSpec = JSON.stringify({
|
|
842
|
+
openapi: "3.1.0",
|
|
843
|
+
info: { title: "Invalid", version: "1.0.0" },
|
|
844
|
+
paths: {},
|
|
845
|
+
});
|
|
846
|
+
const server = serveOpenapiSpec(invalidSpec);
|
|
847
|
+
try {
|
|
848
|
+
const suffix = crypto.randomUUID().replace(/-/g, "");
|
|
849
|
+
const connection = await upsertScriptConnection({
|
|
850
|
+
slug: `invalidVendor${suffix}`,
|
|
851
|
+
kind: "openapi",
|
|
852
|
+
baseUrl: server.baseUrl,
|
|
853
|
+
openapiSpecUrl: server.url,
|
|
854
|
+
});
|
|
855
|
+
createdConnectionIds.push(connection.id);
|
|
856
|
+
|
|
857
|
+
expect(connection.openapiSpecSourceKind).toBe("url");
|
|
858
|
+
expect(connection.generationError).toContain("supported operations");
|
|
859
|
+
expect(connection.generatedTypes).toBeNull();
|
|
860
|
+
} finally {
|
|
861
|
+
server.stop();
|
|
862
|
+
}
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
test("invalid fetched spec content is rejected before storing a connection", async () => {
|
|
866
|
+
const server = serveOpenapiSpec("{not-valid-json");
|
|
867
|
+
try {
|
|
868
|
+
const suffix = crypto.randomUUID().replace(/-/g, "");
|
|
869
|
+
|
|
870
|
+
await expect(
|
|
871
|
+
upsertScriptConnection({
|
|
872
|
+
slug: `badContentVendor${suffix}`,
|
|
873
|
+
kind: "openapi",
|
|
874
|
+
baseUrl: server.baseUrl,
|
|
875
|
+
openapiSpecUrl: server.url,
|
|
876
|
+
}),
|
|
877
|
+
).rejects.toThrow(/valid JSON|valid YAML|JSON specs only/);
|
|
878
|
+
|
|
879
|
+
const stored = getDb()
|
|
880
|
+
.prepare<{ count: number }, [string]>(
|
|
881
|
+
"SELECT COUNT(*) AS count FROM script_connections WHERE slug = ?",
|
|
882
|
+
)
|
|
883
|
+
.get(`badContentVendor${suffix}`);
|
|
884
|
+
expect(stored?.count).toBe(0);
|
|
885
|
+
} finally {
|
|
886
|
+
server.stop();
|
|
887
|
+
}
|
|
888
|
+
});
|
|
889
|
+
|
|
249
890
|
test("script-connections tool registers OpenAPI connections from an agent header without FK failures", async () => {
|
|
250
891
|
const suffix = crypto.randomUUID().replace(/-/g, "");
|
|
251
892
|
const slug = `agentHeaderVendor${suffix}`;
|
|
@@ -305,6 +946,113 @@ describe("script connections", () => {
|
|
|
305
946
|
createdBindingIds.push(bindingRow!.id);
|
|
306
947
|
});
|
|
307
948
|
|
|
949
|
+
test("script-connections tool can upsert OpenAPI connections by URL and refresh them", async () => {
|
|
950
|
+
const server = serveOpenapiSpec(openapiSpec);
|
|
951
|
+
try {
|
|
952
|
+
const suffix = crypto.randomUUID().replace(/-/g, "");
|
|
953
|
+
const slug = `toolUrlVendor${suffix}`;
|
|
954
|
+
const lead = createAgent({
|
|
955
|
+
name: `script-connections-tool-url-lead-${crypto.randomUUID()}`,
|
|
956
|
+
isLead: true,
|
|
957
|
+
status: "idle",
|
|
958
|
+
});
|
|
959
|
+
const tool = scriptConnectionsTool();
|
|
960
|
+
|
|
961
|
+
const upsertResult = (await tool.handler(
|
|
962
|
+
{
|
|
963
|
+
action: "upsert-openapi",
|
|
964
|
+
slug,
|
|
965
|
+
displayName: "Tool URL Vendor",
|
|
966
|
+
baseUrl: server.baseUrl,
|
|
967
|
+
openapiSpecUrl: server.url,
|
|
968
|
+
},
|
|
969
|
+
meta(lead.id),
|
|
970
|
+
)) as {
|
|
971
|
+
structuredContent: { success: boolean; message: string };
|
|
972
|
+
};
|
|
973
|
+
|
|
974
|
+
expect(upsertResult.structuredContent.success).toBe(true);
|
|
975
|
+
const row = getDb()
|
|
976
|
+
.prepare<{ id: string }, [string]>("SELECT id FROM script_connections WHERE slug = ?")
|
|
977
|
+
.get(slug);
|
|
978
|
+
expect(row).toBeDefined();
|
|
979
|
+
createdConnectionIds.push(row!.id);
|
|
980
|
+
|
|
981
|
+
server.setBody(specWithExtraOperation("toolListOrgRepos"), '"v2"');
|
|
982
|
+
const refreshResult = (await tool.handler(
|
|
983
|
+
{ action: "refresh", id: row!.id },
|
|
984
|
+
meta(lead.id),
|
|
985
|
+
)) as {
|
|
986
|
+
structuredContent: { success: boolean; message: string };
|
|
987
|
+
};
|
|
988
|
+
|
|
989
|
+
expect(refreshResult.structuredContent.success).toBe(true);
|
|
990
|
+
const refreshed = getDb()
|
|
991
|
+
.prepare<{ openapi_spec_etag: string | null; generated_types: string | null }, [string]>(
|
|
992
|
+
"SELECT openapi_spec_etag, generated_types FROM script_connections WHERE id = ?",
|
|
993
|
+
)
|
|
994
|
+
.get(row!.id);
|
|
995
|
+
expect(refreshed?.openapi_spec_etag).toBe('"v2"');
|
|
996
|
+
expect(refreshed?.generated_types).toContain("toolListOrgRepos");
|
|
997
|
+
} finally {
|
|
998
|
+
server.stop();
|
|
999
|
+
}
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
test("script-connections tool can upsert GraphQL connections", async () => {
|
|
1003
|
+
const suffix = crypto.randomUUID().replace(/-/g, "");
|
|
1004
|
+
const slug = `toolGraphql${suffix}`;
|
|
1005
|
+
const configKey = `TOOL_GRAPHQL_KEY_${suffix}`;
|
|
1006
|
+
const lead = createAgent({
|
|
1007
|
+
name: `script-connections-tool-graphql-lead-${crypto.randomUUID()}`,
|
|
1008
|
+
isLead: true,
|
|
1009
|
+
status: "idle",
|
|
1010
|
+
});
|
|
1011
|
+
const tool = scriptConnectionsTool();
|
|
1012
|
+
|
|
1013
|
+
const result = (await tool.handler(
|
|
1014
|
+
{
|
|
1015
|
+
action: "upsert-graphql",
|
|
1016
|
+
slug,
|
|
1017
|
+
displayName: "Tool GraphQL",
|
|
1018
|
+
baseUrl: "https://graphql.vendor.test/query",
|
|
1019
|
+
allowedHosts: ["graphql.vendor.test"],
|
|
1020
|
+
configKey,
|
|
1021
|
+
},
|
|
1022
|
+
meta(lead.id),
|
|
1023
|
+
)) as {
|
|
1024
|
+
structuredContent: { success: boolean; message: string };
|
|
1025
|
+
};
|
|
1026
|
+
|
|
1027
|
+
expect(result.structuredContent.success).toBe(true);
|
|
1028
|
+
const row = getDb()
|
|
1029
|
+
.prepare<
|
|
1030
|
+
{
|
|
1031
|
+
id: string;
|
|
1032
|
+
kind: string;
|
|
1033
|
+
credential_binding_id: string | null;
|
|
1034
|
+
generated_types: string | null;
|
|
1035
|
+
},
|
|
1036
|
+
[string]
|
|
1037
|
+
>(
|
|
1038
|
+
`SELECT id, kind, credential_binding_id, generated_types
|
|
1039
|
+
FROM script_connections WHERE slug = ?`,
|
|
1040
|
+
)
|
|
1041
|
+
.get(slug);
|
|
1042
|
+
expect(row?.kind).toBe("graphql");
|
|
1043
|
+
expect(row?.generated_types).toContain("graphql<T = JsonValue>");
|
|
1044
|
+
createdConnectionIds.push(row!.id);
|
|
1045
|
+
|
|
1046
|
+
const bindingRow = getDb()
|
|
1047
|
+
.prepare<{ id: string; allowed_hosts_json: string }, [string]>(
|
|
1048
|
+
"SELECT id, allowed_hosts_json FROM script_credential_bindings WHERE config_key = ?",
|
|
1049
|
+
)
|
|
1050
|
+
.get(configKey);
|
|
1051
|
+
expect(bindingRow?.id).toBe(row?.credential_binding_id);
|
|
1052
|
+
expect(bindingRow?.allowed_hosts_json).toBe(JSON.stringify(["graphql.vendor.test"]));
|
|
1053
|
+
createdBindingIds.push(bindingRow!.id);
|
|
1054
|
+
});
|
|
1055
|
+
|
|
308
1056
|
test("ctx.api runtime emits plain fetch with credential placeholders", async () => {
|
|
309
1057
|
let observed: { url: string; authorization: string | null } | null = null;
|
|
310
1058
|
const server = Bun.serve({
|
|
@@ -323,7 +1071,7 @@ describe("script connections", () => {
|
|
|
323
1071
|
headerTemplate: "Authorization: Bearer [REDACTED:RUNTIME_VENDOR_KEY]",
|
|
324
1072
|
});
|
|
325
1073
|
createdBindingIds.push(binding.id);
|
|
326
|
-
const connection = upsertScriptConnection({
|
|
1074
|
+
const connection = await upsertScriptConnection({
|
|
327
1075
|
slug: "runtimeVendor",
|
|
328
1076
|
kind: "openapi",
|
|
329
1077
|
baseUrl: `http://127.0.0.1:${server.port}`,
|