@desplega.ai/agent-swarm 1.71.0 → 1.71.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/openapi.json +43 -1
- package/package.json +1 -1
- package/src/http/trackers/jira.ts +73 -9
- package/src/http/trackers/linear.ts +47 -4
- package/src/http/utils.ts +27 -0
- package/src/jira/app.ts +18 -0
- package/src/jira/metadata.ts +13 -0
- package/src/linear/app.ts +17 -0
- package/src/linear/oauth.ts +24 -0
package/openapi.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"openapi": "3.1.0",
|
|
3
3
|
"info": {
|
|
4
4
|
"title": "Agent Swarm API",
|
|
5
|
-
"version": "1.71.
|
|
5
|
+
"version": "1.71.1",
|
|
6
6
|
"description": "Multi-agent orchestration API for Claude Code, Codex, and Gemini CLI. Enables task distribution, agent communication, and service discovery.\n\nMCP tools are documented separately in [MCP.md](./MCP.md)."
|
|
7
7
|
},
|
|
8
8
|
"servers": [
|
|
@@ -6392,6 +6392,27 @@
|
|
|
6392
6392
|
}
|
|
6393
6393
|
}
|
|
6394
6394
|
},
|
|
6395
|
+
"/api/trackers/jira/disconnect": {
|
|
6396
|
+
"delete": {
|
|
6397
|
+
"summary": "Fully disconnect Jira: delete all webhooks, drop tokens, clear metadata",
|
|
6398
|
+
"tags": [
|
|
6399
|
+
"Trackers"
|
|
6400
|
+
],
|
|
6401
|
+
"security": [
|
|
6402
|
+
{
|
|
6403
|
+
"bearerAuth": []
|
|
6404
|
+
}
|
|
6405
|
+
],
|
|
6406
|
+
"responses": {
|
|
6407
|
+
"200": {
|
|
6408
|
+
"description": "Disconnected"
|
|
6409
|
+
},
|
|
6410
|
+
"503": {
|
|
6411
|
+
"description": "Jira not configured"
|
|
6412
|
+
}
|
|
6413
|
+
}
|
|
6414
|
+
}
|
|
6415
|
+
},
|
|
6395
6416
|
"/api/trackers/linear/authorize": {
|
|
6396
6417
|
"get": {
|
|
6397
6418
|
"summary": "Redirect to Linear OAuth consent screen",
|
|
@@ -6488,6 +6509,27 @@
|
|
|
6488
6509
|
}
|
|
6489
6510
|
}
|
|
6490
6511
|
},
|
|
6512
|
+
"/api/trackers/linear/disconnect": {
|
|
6513
|
+
"delete": {
|
|
6514
|
+
"summary": "Fully disconnect Linear: revoke OAuth grant + drop tokens",
|
|
6515
|
+
"tags": [
|
|
6516
|
+
"Trackers"
|
|
6517
|
+
],
|
|
6518
|
+
"security": [
|
|
6519
|
+
{
|
|
6520
|
+
"bearerAuth": []
|
|
6521
|
+
}
|
|
6522
|
+
],
|
|
6523
|
+
"responses": {
|
|
6524
|
+
"200": {
|
|
6525
|
+
"description": "Disconnected"
|
|
6526
|
+
},
|
|
6527
|
+
"503": {
|
|
6528
|
+
"description": "Linear not configured"
|
|
6529
|
+
}
|
|
6530
|
+
}
|
|
6531
|
+
}
|
|
6532
|
+
},
|
|
6491
6533
|
"/api/github/webhook": {
|
|
6492
6534
|
"post": {
|
|
6493
6535
|
"summary": "Handle GitHub webhook events",
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { getOAuthTokens } from "../../be/db-queries/oauth";
|
|
3
|
+
import { deleteOAuthTokens, getOAuthTokens } from "../../be/db-queries/oauth";
|
|
4
4
|
import { isJiraEnabled } from "../../jira/app";
|
|
5
|
-
import { getJiraMetadata } from "../../jira/metadata";
|
|
5
|
+
import { clearJiraMetadata, getJiraMetadata } from "../../jira/metadata";
|
|
6
6
|
import { getJiraAuthorizationUrl, handleJiraCallback } from "../../jira/oauth";
|
|
7
7
|
import { handleJiraWebhook } from "../../jira/webhook";
|
|
8
8
|
import { deleteJiraWebhook, registerJiraWebhook } from "../../jira/webhook-lifecycle";
|
|
9
9
|
import { route } from "../route-def";
|
|
10
|
-
import { parseQueryParams } from "../utils";
|
|
10
|
+
import { deriveApiBaseUrl, parseQueryParams } from "../utils";
|
|
11
11
|
|
|
12
12
|
const MANUAL_WEBHOOK_INSTRUCTIONS =
|
|
13
13
|
"See docs-site/.../guides/jira-integration.mdx for manual webhook registration steps.";
|
|
@@ -112,15 +112,31 @@ const jiraWebhookDelete = route({
|
|
|
112
112
|
},
|
|
113
113
|
});
|
|
114
114
|
|
|
115
|
+
// Admin: full disconnect — delete all registered Atlassian webhooks, drop
|
|
116
|
+
// stored OAuth tokens, and clear cloudId/siteUrl/webhookIds metadata. Atlassian
|
|
117
|
+
// 3LO has no public token revocation endpoint, so the OAuth grant itself must
|
|
118
|
+
// be revoked by the user via id.atlassian.com → Connected apps.
|
|
119
|
+
const jiraDisconnect = route({
|
|
120
|
+
method: "delete",
|
|
121
|
+
path: "/api/trackers/jira/disconnect",
|
|
122
|
+
pattern: ["api", "trackers", "jira", "disconnect"],
|
|
123
|
+
summary: "Fully disconnect Jira: delete all webhooks, drop tokens, clear metadata",
|
|
124
|
+
tags: ["Trackers"],
|
|
125
|
+
responses: {
|
|
126
|
+
200: { description: "Disconnected" },
|
|
127
|
+
503: { description: "Jira not configured" },
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
|
|
115
131
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
116
132
|
|
|
117
|
-
function
|
|
118
|
-
|
|
133
|
+
function getWebhookUrl(req: IncomingMessage): string {
|
|
134
|
+
const token = process.env.JIRA_WEBHOOK_TOKEN ?? "<unset>";
|
|
135
|
+
return `${deriveApiBaseUrl(req)}/api/trackers/jira/webhook/${token}`;
|
|
119
136
|
}
|
|
120
137
|
|
|
121
|
-
function
|
|
122
|
-
|
|
123
|
-
return `${getWebhookBaseUrl()}/api/trackers/jira/webhook/${token}`;
|
|
138
|
+
function getRedirectUri(req: IncomingMessage): string {
|
|
139
|
+
return `${deriveApiBaseUrl(req)}/api/trackers/jira/callback`;
|
|
124
140
|
}
|
|
125
141
|
|
|
126
142
|
// ─── Handler ─────────────────────────────────────────────────────────────────
|
|
@@ -218,7 +234,8 @@ export async function handleJiraTracker(
|
|
|
218
234
|
scope,
|
|
219
235
|
hasManageWebhookScope,
|
|
220
236
|
webhookTokenConfigured: Boolean(process.env.JIRA_WEBHOOK_TOKEN),
|
|
221
|
-
webhookUrl: getWebhookUrl(),
|
|
237
|
+
webhookUrl: getWebhookUrl(req),
|
|
238
|
+
redirectUri: getRedirectUri(req),
|
|
222
239
|
webhookIds: meta.webhookIds ?? [],
|
|
223
240
|
};
|
|
224
241
|
|
|
@@ -327,5 +344,52 @@ export async function handleJiraTracker(
|
|
|
327
344
|
return true;
|
|
328
345
|
}
|
|
329
346
|
|
|
347
|
+
// DELETE /api/trackers/jira/disconnect — full cleanup.
|
|
348
|
+
if (jiraDisconnect.match(req.method, pathSegments)) {
|
|
349
|
+
if (!isJiraEnabled()) {
|
|
350
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
351
|
+
res.end(JSON.stringify({ error: "Jira integration not configured" }));
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const meta = getJiraMetadata();
|
|
356
|
+
const ids = (meta.webhookIds ?? []).map((entry) => entry.id);
|
|
357
|
+
|
|
358
|
+
let webhooksDeleted = 0;
|
|
359
|
+
const webhookFailures: Array<{ id: number; error: string }> = [];
|
|
360
|
+
for (const id of ids) {
|
|
361
|
+
try {
|
|
362
|
+
await deleteJiraWebhook(id);
|
|
363
|
+
webhooksDeleted++;
|
|
364
|
+
} catch (err) {
|
|
365
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
366
|
+
console.warn(`[Jira] Disconnect: webhook delete failed (id=${id}): ${message}`);
|
|
367
|
+
webhookFailures.push({ id, error: message });
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
deleteOAuthTokens("jira");
|
|
372
|
+
clearJiraMetadata();
|
|
373
|
+
|
|
374
|
+
console.log(
|
|
375
|
+
`[Jira] Disconnected: ${webhooksDeleted}/${ids.length} webhooks deleted, tokens cleared, metadata reset`,
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
379
|
+
res.end(
|
|
380
|
+
JSON.stringify({
|
|
381
|
+
disconnected: true,
|
|
382
|
+
webhooksDeleted,
|
|
383
|
+
webhooksTotal: ids.length,
|
|
384
|
+
webhookFailures,
|
|
385
|
+
// Atlassian 3LO has no token revocation endpoint — surface this so
|
|
386
|
+
// the UI can prompt the user to revoke the grant manually if desired.
|
|
387
|
+
revokeNote:
|
|
388
|
+
"Atlassian OAuth grants must be revoked manually at https://id.atlassian.com/manage/connected-apps if you want to fully sever the consent.",
|
|
389
|
+
}),
|
|
390
|
+
);
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
393
|
+
|
|
330
394
|
return false;
|
|
331
395
|
}
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { getOAuthTokens } from "../../be/db-queries/oauth";
|
|
3
|
+
import { deleteOAuthTokens, getOAuthTokens } from "../../be/db-queries/oauth";
|
|
4
4
|
import { isLinearEnabled } from "../../linear/app";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
getLinearAuthorizationUrl,
|
|
7
|
+
handleLinearCallback,
|
|
8
|
+
revokeLinearToken,
|
|
9
|
+
} from "../../linear/oauth";
|
|
6
10
|
import { handleLinearWebhook } from "../../linear/webhook";
|
|
7
11
|
import { route } from "../route-def";
|
|
8
|
-
import { parseQueryParams } from "../utils";
|
|
12
|
+
import { deriveApiBaseUrl, parseQueryParams } from "../utils";
|
|
9
13
|
|
|
10
14
|
// ─── Route Definitions ───────────────────────────────────────────────────────
|
|
11
15
|
|
|
@@ -67,6 +71,21 @@ const linearWebhook = route({
|
|
|
67
71
|
},
|
|
68
72
|
});
|
|
69
73
|
|
|
74
|
+
// Admin: full disconnect — best-effort revoke the OAuth grant with Linear,
|
|
75
|
+
// then drop stored tokens. Linear webhooks are configured globally on the
|
|
76
|
+
// OAuth app (not per-tenant), so no per-tenant webhook delete is needed.
|
|
77
|
+
const linearDisconnect = route({
|
|
78
|
+
method: "delete",
|
|
79
|
+
path: "/api/trackers/linear/disconnect",
|
|
80
|
+
pattern: ["api", "trackers", "linear", "disconnect"],
|
|
81
|
+
summary: "Fully disconnect Linear: revoke OAuth grant + drop tokens",
|
|
82
|
+
tags: ["Trackers"],
|
|
83
|
+
responses: {
|
|
84
|
+
200: { description: "Disconnected" },
|
|
85
|
+
503: { description: "Linear not configured" },
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
70
89
|
// ─── Handler ─────────────────────────────────────────────────────────────────
|
|
71
90
|
|
|
72
91
|
export async function handleLinearTracker(
|
|
@@ -146,7 +165,7 @@ export async function handleLinearTracker(
|
|
|
146
165
|
}
|
|
147
166
|
|
|
148
167
|
const tokens = getOAuthTokens("linear");
|
|
149
|
-
const baseUrl =
|
|
168
|
+
const baseUrl = deriveApiBaseUrl(req);
|
|
150
169
|
|
|
151
170
|
const status = {
|
|
152
171
|
provider: "linear",
|
|
@@ -154,6 +173,7 @@ export async function handleLinearTracker(
|
|
|
154
173
|
tokenExpiry: tokens?.expiresAt ?? null,
|
|
155
174
|
scope: tokens?.scope ?? null,
|
|
156
175
|
webhookUrl: `${baseUrl}/api/trackers/linear/webhook`,
|
|
176
|
+
redirectUri: `${baseUrl}/api/trackers/linear/callback`,
|
|
157
177
|
};
|
|
158
178
|
|
|
159
179
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -183,5 +203,28 @@ export async function handleLinearTracker(
|
|
|
183
203
|
return true;
|
|
184
204
|
}
|
|
185
205
|
|
|
206
|
+
// DELETE /api/trackers/linear/disconnect — full cleanup.
|
|
207
|
+
if (linearDisconnect.match(req.method, pathSegments)) {
|
|
208
|
+
if (!isLinearEnabled()) {
|
|
209
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
210
|
+
res.end(JSON.stringify({ error: "Linear integration not configured" }));
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const tokens = getOAuthTokens("linear");
|
|
215
|
+
let revoked = false;
|
|
216
|
+
if (tokens?.accessToken) {
|
|
217
|
+
revoked = await revokeLinearToken(tokens.accessToken);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
deleteOAuthTokens("linear");
|
|
221
|
+
|
|
222
|
+
console.log(`[Linear] Disconnected: revoke=${revoked}, tokens cleared`);
|
|
223
|
+
|
|
224
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
225
|
+
res.end(JSON.stringify({ disconnected: true, revoked }));
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
|
|
186
229
|
return false;
|
|
187
230
|
}
|
package/src/http/utils.ts
CHANGED
|
@@ -57,6 +57,33 @@ export function jsonError(res: ServerResponse, error: string, status = 400) {
|
|
|
57
57
|
res.end(JSON.stringify({ error }));
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Derive the API base URL for outbound-facing values (webhook URLs, OAuth
|
|
62
|
+
* redirect URIs). Returns a URL with no trailing slash.
|
|
63
|
+
*
|
|
64
|
+
* Resolution order:
|
|
65
|
+
* 1. `MCP_BASE_URL` env (canonical)
|
|
66
|
+
* 2. Inbound request host — `X-Forwarded-Proto`/`X-Forwarded-Host` if behind
|
|
67
|
+
* a proxy/tunnel (ngrok), else `Host` header. Lets the URL stay correct
|
|
68
|
+
* when MCP_BASE_URL is unset and the API is reached via an arbitrary
|
|
69
|
+
* external hostname.
|
|
70
|
+
* 3. `http://localhost:<PORT>` fallback
|
|
71
|
+
*/
|
|
72
|
+
export function deriveApiBaseUrl(req: IncomingMessage): string {
|
|
73
|
+
const envBase = process.env.MCP_BASE_URL?.trim();
|
|
74
|
+
if (envBase) return envBase.replace(/\/+$/, "");
|
|
75
|
+
|
|
76
|
+
const fwdProtoRaw = req.headers["x-forwarded-proto"];
|
|
77
|
+
const fwdHostRaw = req.headers["x-forwarded-host"];
|
|
78
|
+
const fwdProto = Array.isArray(fwdProtoRaw) ? fwdProtoRaw[0] : fwdProtoRaw;
|
|
79
|
+
const fwdHost = Array.isArray(fwdHostRaw) ? fwdHostRaw[0] : fwdHostRaw;
|
|
80
|
+
const proto = fwdProto?.split(",")[0]?.trim() || "http";
|
|
81
|
+
const host = fwdHost?.split(",")[0]?.trim() || req.headers.host;
|
|
82
|
+
|
|
83
|
+
if (host) return `${proto}://${host}`;
|
|
84
|
+
return `http://localhost:${process.env.PORT || "3013"}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
60
87
|
/**
|
|
61
88
|
* Match a route pattern against HTTP method and path segments.
|
|
62
89
|
*
|
package/src/jira/app.ts
CHANGED
|
@@ -60,6 +60,24 @@ export function initJira(): boolean {
|
|
|
60
60
|
initJiraOutboundSync();
|
|
61
61
|
startJiraWebhookKeepalive();
|
|
62
62
|
|
|
63
|
+
warnIfMcpBaseUrlLooksLikeAppUrl();
|
|
64
|
+
|
|
63
65
|
console.log("[Jira] Integration initialized");
|
|
64
66
|
return true;
|
|
65
67
|
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Soft sanity check for `MCP_BASE_URL`. If it equals `APP_URL` (a common
|
|
71
|
+
* misconfig that points the webhook URL at the dashboard host), warn loudly
|
|
72
|
+
* so the operator can fix the env. We don't fail boot — Atlassian will just
|
|
73
|
+
* 404 webhook deliveries until corrected.
|
|
74
|
+
*/
|
|
75
|
+
function warnIfMcpBaseUrlLooksLikeAppUrl(): void {
|
|
76
|
+
const mcp = process.env.MCP_BASE_URL?.trim().replace(/\/+$/, "");
|
|
77
|
+
const app = process.env.APP_URL?.trim().replace(/\/+$/, "");
|
|
78
|
+
if (mcp && app && mcp === app) {
|
|
79
|
+
console.warn(
|
|
80
|
+
`[Jira] WARNING: MCP_BASE_URL (${mcp}) equals APP_URL — registered webhook URLs will hit the dashboard host, not the API. Atlassian will likely 404 webhook deliveries. Point MCP_BASE_URL at the API server.`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
package/src/jira/metadata.ts
CHANGED
|
@@ -102,3 +102,16 @@ export function updateJiraMetadata(partial: Partial<JiraOAuthAppMetadata>): void
|
|
|
102
102
|
|
|
103
103
|
txn();
|
|
104
104
|
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Reset the Jira `oauth_apps.metadata` blob to `{}`. Used by the disconnect
|
|
108
|
+
* flow to drop cloudId, siteUrl, and webhookIds in one shot. The row itself
|
|
109
|
+
* stays — `initJira()` requires the `oauth_apps` row to exist.
|
|
110
|
+
*/
|
|
111
|
+
export function clearJiraMetadata(): void {
|
|
112
|
+
getDb()
|
|
113
|
+
.query(
|
|
114
|
+
"UPDATE oauth_apps SET metadata = '{}', updatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE provider = 'jira'",
|
|
115
|
+
)
|
|
116
|
+
.run();
|
|
117
|
+
}
|
package/src/linear/app.ts
CHANGED
|
@@ -43,6 +43,23 @@ export function initLinear(): boolean {
|
|
|
43
43
|
|
|
44
44
|
initLinearOutboundSync();
|
|
45
45
|
|
|
46
|
+
warnIfMcpBaseUrlLooksLikeAppUrl();
|
|
47
|
+
|
|
46
48
|
console.log("[Linear] Integration initialized");
|
|
47
49
|
return true;
|
|
48
50
|
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Soft sanity check for `MCP_BASE_URL`. If it equals `APP_URL` (a common
|
|
54
|
+
* misconfig that surfaces a wrong-looking webhook URL in the dashboard),
|
|
55
|
+
* warn loudly so the operator can fix the env.
|
|
56
|
+
*/
|
|
57
|
+
function warnIfMcpBaseUrlLooksLikeAppUrl(): void {
|
|
58
|
+
const mcp = process.env.MCP_BASE_URL?.trim().replace(/\/+$/, "");
|
|
59
|
+
const app = process.env.APP_URL?.trim().replace(/\/+$/, "");
|
|
60
|
+
if (mcp && app && mcp === app) {
|
|
61
|
+
console.warn(
|
|
62
|
+
`[Linear] WARNING: MCP_BASE_URL (${mcp}) equals APP_URL — surfaced webhook URL points at the dashboard host, not the API. Configure Linear with this URL only if the dashboard host also serves /api/*.`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
package/src/linear/oauth.ts
CHANGED
|
@@ -33,3 +33,27 @@ export async function handleLinearCallback(
|
|
|
33
33
|
if (!config) throw new Error("Linear OAuth not configured");
|
|
34
34
|
return exchangeCode(config, code, state);
|
|
35
35
|
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Revoke an OAuth access token with Linear. Best-effort — caller should not
|
|
39
|
+
* abort the disconnect flow if this fails. Linear's revocation endpoint is
|
|
40
|
+
* `POST https://api.linear.app/oauth/revoke` with the access token in the
|
|
41
|
+
* Authorization header (per https://developers.linear.app/docs/oauth/authentication).
|
|
42
|
+
*
|
|
43
|
+
* Returns true on a 2xx response, false otherwise.
|
|
44
|
+
*/
|
|
45
|
+
export async function revokeLinearToken(accessToken: string): Promise<boolean> {
|
|
46
|
+
try {
|
|
47
|
+
const res = await fetch("https://api.linear.app/oauth/revoke", {
|
|
48
|
+
method: "POST",
|
|
49
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
50
|
+
});
|
|
51
|
+
return res.ok;
|
|
52
|
+
} catch (err) {
|
|
53
|
+
console.warn(
|
|
54
|
+
"[Linear] Token revocation failed (best-effort):",
|
|
55
|
+
err instanceof Error ? err.message : err,
|
|
56
|
+
);
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|