@desplega.ai/agent-swarm 1.70.0 → 1.71.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/openapi.json +184 -1
- package/package.json +1 -1
- package/src/be/db-queries/oauth.ts +45 -15
- package/src/be/db-queries/tracker.ts +109 -0
- package/src/be/migrations/043_jira_source.sql +128 -0
- package/src/commands/runner.ts +7 -2
- package/src/http/core.ts +6 -21
- package/src/http/index.ts +9 -1
- package/src/http/route-def.ts +19 -0
- package/src/http/trackers/index.ts +13 -0
- package/src/http/trackers/jira.ts +331 -0
- package/src/jira/adf.ts +132 -0
- package/src/jira/app.ts +65 -0
- package/src/jira/client.ts +82 -0
- package/src/jira/index.ts +24 -0
- package/src/jira/metadata.ts +104 -0
- package/src/jira/oauth.ts +98 -0
- package/src/jira/outbound.ts +155 -0
- package/src/jira/sync.ts +534 -0
- package/src/jira/templates.ts +84 -0
- package/src/jira/types.ts +35 -0
- package/src/jira/webhook-lifecycle.ts +363 -0
- package/src/jira/webhook.ts +159 -0
- package/src/oauth/wrapper.ts +11 -1
- package/src/tasks/context-key.ts +29 -1
- package/src/telemetry.ts +38 -3
- package/src/tests/context-key.test.ts +19 -0
- package/src/tests/jira-adf.test.ts +239 -0
- package/src/tests/jira-metadata.test.ts +147 -0
- package/src/tests/jira-oauth.test.ts +167 -0
- package/src/tests/jira-outbound-sync.test.ts +334 -0
- package/src/tests/jira-sync.test.ts +327 -0
- package/src/tests/jira-webhook-lifecycle.test.ts +234 -0
- package/src/tests/jira-webhook.test.ts +274 -0
- package/src/tests/telemetry-init.test.ts +108 -0
- package/src/tools/tracker/tracker-link-task.ts +1 -1
- package/src/tools/tracker/tracker-map-agent.ts +1 -1
- package/src/tools/tracker/tracker-status.ts +1 -1
- package/src/tools/tracker/tracker-sync-status.ts +1 -1
- package/src/tracker/types.ts +1 -1
- package/src/types.ts +1 -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.
|
|
5
|
+
"version": "1.71.0",
|
|
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": [
|
|
@@ -6209,6 +6209,189 @@
|
|
|
6209
6209
|
}
|
|
6210
6210
|
}
|
|
6211
6211
|
},
|
|
6212
|
+
"/api/trackers/jira/authorize": {
|
|
6213
|
+
"get": {
|
|
6214
|
+
"summary": "Redirect to Atlassian OAuth consent screen",
|
|
6215
|
+
"tags": [
|
|
6216
|
+
"Trackers"
|
|
6217
|
+
],
|
|
6218
|
+
"responses": {
|
|
6219
|
+
"302": {
|
|
6220
|
+
"description": "Redirect to Atlassian OAuth"
|
|
6221
|
+
},
|
|
6222
|
+
"500": {
|
|
6223
|
+
"description": "Failed to generate authorization URL"
|
|
6224
|
+
},
|
|
6225
|
+
"503": {
|
|
6226
|
+
"description": "Jira integration not configured"
|
|
6227
|
+
}
|
|
6228
|
+
}
|
|
6229
|
+
}
|
|
6230
|
+
},
|
|
6231
|
+
"/api/trackers/jira/callback": {
|
|
6232
|
+
"get": {
|
|
6233
|
+
"summary": "Handle Jira OAuth callback (resolves cloudId via accessible-resources)",
|
|
6234
|
+
"tags": [
|
|
6235
|
+
"Trackers"
|
|
6236
|
+
],
|
|
6237
|
+
"parameters": [
|
|
6238
|
+
{
|
|
6239
|
+
"schema": {
|
|
6240
|
+
"type": "string"
|
|
6241
|
+
},
|
|
6242
|
+
"required": true,
|
|
6243
|
+
"name": "code",
|
|
6244
|
+
"in": "query"
|
|
6245
|
+
},
|
|
6246
|
+
{
|
|
6247
|
+
"schema": {
|
|
6248
|
+
"type": "string"
|
|
6249
|
+
},
|
|
6250
|
+
"required": true,
|
|
6251
|
+
"name": "state",
|
|
6252
|
+
"in": "query"
|
|
6253
|
+
}
|
|
6254
|
+
],
|
|
6255
|
+
"responses": {
|
|
6256
|
+
"200": {
|
|
6257
|
+
"description": "OAuth complete"
|
|
6258
|
+
},
|
|
6259
|
+
"400": {
|
|
6260
|
+
"description": "Invalid state or code"
|
|
6261
|
+
},
|
|
6262
|
+
"500": {
|
|
6263
|
+
"description": "Token exchange or accessible-resources fetch failed"
|
|
6264
|
+
}
|
|
6265
|
+
}
|
|
6266
|
+
}
|
|
6267
|
+
},
|
|
6268
|
+
"/api/trackers/jira/status": {
|
|
6269
|
+
"get": {
|
|
6270
|
+
"summary": "Jira connection status, cloudId/siteUrl, token expiry, expected webhook URL, scope/token-config flags",
|
|
6271
|
+
"tags": [
|
|
6272
|
+
"Trackers"
|
|
6273
|
+
],
|
|
6274
|
+
"security": [
|
|
6275
|
+
{
|
|
6276
|
+
"bearerAuth": []
|
|
6277
|
+
}
|
|
6278
|
+
],
|
|
6279
|
+
"responses": {
|
|
6280
|
+
"200": {
|
|
6281
|
+
"description": "Connection status"
|
|
6282
|
+
},
|
|
6283
|
+
"503": {
|
|
6284
|
+
"description": "Jira integration not configured"
|
|
6285
|
+
}
|
|
6286
|
+
}
|
|
6287
|
+
}
|
|
6288
|
+
},
|
|
6289
|
+
"/api/trackers/jira/webhook/{token}": {
|
|
6290
|
+
"post": {
|
|
6291
|
+
"summary": "Receive Jira webhook events (URL-token authenticated). Phase 2 stub — Phase 3 fills in dispatch.",
|
|
6292
|
+
"tags": [
|
|
6293
|
+
"Trackers"
|
|
6294
|
+
],
|
|
6295
|
+
"parameters": [
|
|
6296
|
+
{
|
|
6297
|
+
"schema": {
|
|
6298
|
+
"type": "string"
|
|
6299
|
+
},
|
|
6300
|
+
"required": true,
|
|
6301
|
+
"name": "token",
|
|
6302
|
+
"in": "path"
|
|
6303
|
+
}
|
|
6304
|
+
],
|
|
6305
|
+
"responses": {
|
|
6306
|
+
"200": {
|
|
6307
|
+
"description": "Event accepted"
|
|
6308
|
+
},
|
|
6309
|
+
"401": {
|
|
6310
|
+
"description": "Invalid URL token"
|
|
6311
|
+
},
|
|
6312
|
+
"503": {
|
|
6313
|
+
"description": "Jira webhook handler not configured"
|
|
6314
|
+
}
|
|
6315
|
+
}
|
|
6316
|
+
}
|
|
6317
|
+
},
|
|
6318
|
+
"/api/trackers/jira/webhook-register": {
|
|
6319
|
+
"post": {
|
|
6320
|
+
"summary": "Register a Jira dynamic webhook (admin only)",
|
|
6321
|
+
"tags": [
|
|
6322
|
+
"Trackers"
|
|
6323
|
+
],
|
|
6324
|
+
"security": [
|
|
6325
|
+
{
|
|
6326
|
+
"bearerAuth": []
|
|
6327
|
+
}
|
|
6328
|
+
],
|
|
6329
|
+
"requestBody": {
|
|
6330
|
+
"content": {
|
|
6331
|
+
"application/json": {
|
|
6332
|
+
"schema": {
|
|
6333
|
+
"type": "object",
|
|
6334
|
+
"properties": {
|
|
6335
|
+
"jqlFilter": {
|
|
6336
|
+
"type": "string",
|
|
6337
|
+
"minLength": 1
|
|
6338
|
+
}
|
|
6339
|
+
},
|
|
6340
|
+
"required": [
|
|
6341
|
+
"jqlFilter"
|
|
6342
|
+
]
|
|
6343
|
+
}
|
|
6344
|
+
}
|
|
6345
|
+
}
|
|
6346
|
+
},
|
|
6347
|
+
"responses": {
|
|
6348
|
+
"200": {
|
|
6349
|
+
"description": "Webhook registered"
|
|
6350
|
+
},
|
|
6351
|
+
"400": {
|
|
6352
|
+
"description": "Invalid jqlFilter"
|
|
6353
|
+
},
|
|
6354
|
+
"503": {
|
|
6355
|
+
"description": "Jira not connected or JIRA_WEBHOOK_TOKEN missing"
|
|
6356
|
+
}
|
|
6357
|
+
}
|
|
6358
|
+
}
|
|
6359
|
+
},
|
|
6360
|
+
"/api/trackers/jira/webhook/{id}": {
|
|
6361
|
+
"delete": {
|
|
6362
|
+
"summary": "Delete a Jira dynamic webhook (admin only)",
|
|
6363
|
+
"tags": [
|
|
6364
|
+
"Trackers"
|
|
6365
|
+
],
|
|
6366
|
+
"security": [
|
|
6367
|
+
{
|
|
6368
|
+
"bearerAuth": []
|
|
6369
|
+
}
|
|
6370
|
+
],
|
|
6371
|
+
"parameters": [
|
|
6372
|
+
{
|
|
6373
|
+
"schema": {
|
|
6374
|
+
"type": "integer",
|
|
6375
|
+
"exclusiveMinimum": 0
|
|
6376
|
+
},
|
|
6377
|
+
"required": true,
|
|
6378
|
+
"name": "id",
|
|
6379
|
+
"in": "path"
|
|
6380
|
+
}
|
|
6381
|
+
],
|
|
6382
|
+
"responses": {
|
|
6383
|
+
"200": {
|
|
6384
|
+
"description": "Webhook deleted"
|
|
6385
|
+
},
|
|
6386
|
+
"400": {
|
|
6387
|
+
"description": "Invalid webhook id"
|
|
6388
|
+
},
|
|
6389
|
+
"503": {
|
|
6390
|
+
"description": "Jira not connected"
|
|
6391
|
+
}
|
|
6392
|
+
}
|
|
6393
|
+
}
|
|
6394
|
+
},
|
|
6212
6395
|
"/api/trackers/linear/authorize": {
|
|
6213
6396
|
"get": {
|
|
6214
6397
|
"summary": "Redirect to Linear OAuth consent screen",
|
package/package.json
CHANGED
|
@@ -39,9 +39,14 @@ export function upsertOAuthApp(
|
|
|
39
39
|
metadata?: string;
|
|
40
40
|
},
|
|
41
41
|
): void {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
// metadata is treated as a runtime-owned column (cloudId, webhookIds, etc.
|
|
43
|
+
// are written by OAuth callback + webhook-register flows). On INSERT we
|
|
44
|
+
// seed it with whatever the caller passed (or "{}"); on UPDATE we ONLY
|
|
45
|
+
// overwrite when the caller explicitly provided one — otherwise the
|
|
46
|
+
// existing value is preserved across server restarts.
|
|
47
|
+
const metadataProvided = data.metadata !== undefined;
|
|
48
|
+
const sql = metadataProvided
|
|
49
|
+
? `INSERT INTO oauth_apps (provider, clientId, clientSecret, authorizeUrl, tokenUrl, redirectUri, scopes, metadata)
|
|
45
50
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
46
51
|
ON CONFLICT(provider) DO UPDATE SET
|
|
47
52
|
clientId = excluded.clientId,
|
|
@@ -51,18 +56,43 @@ export function upsertOAuthApp(
|
|
|
51
56
|
redirectUri = excluded.redirectUri,
|
|
52
57
|
scopes = excluded.scopes,
|
|
53
58
|
metadata = excluded.metadata,
|
|
54
|
-
updatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
59
|
+
updatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')`
|
|
60
|
+
: `INSERT INTO oauth_apps (provider, clientId, clientSecret, authorizeUrl, tokenUrl, redirectUri, scopes, metadata)
|
|
61
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, '{}')
|
|
62
|
+
ON CONFLICT(provider) DO UPDATE SET
|
|
63
|
+
clientId = excluded.clientId,
|
|
64
|
+
clientSecret = excluded.clientSecret,
|
|
65
|
+
authorizeUrl = excluded.authorizeUrl,
|
|
66
|
+
tokenUrl = excluded.tokenUrl,
|
|
67
|
+
redirectUri = excluded.redirectUri,
|
|
68
|
+
scopes = excluded.scopes,
|
|
69
|
+
updatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')`;
|
|
70
|
+
if (metadataProvided) {
|
|
71
|
+
getDb()
|
|
72
|
+
.query(sql)
|
|
73
|
+
.run(
|
|
74
|
+
provider,
|
|
75
|
+
data.clientId,
|
|
76
|
+
data.clientSecret,
|
|
77
|
+
data.authorizeUrl,
|
|
78
|
+
data.tokenUrl,
|
|
79
|
+
data.redirectUri,
|
|
80
|
+
data.scopes,
|
|
81
|
+
data.metadata as string,
|
|
82
|
+
);
|
|
83
|
+
} else {
|
|
84
|
+
getDb()
|
|
85
|
+
.query(sql)
|
|
86
|
+
.run(
|
|
87
|
+
provider,
|
|
88
|
+
data.clientId,
|
|
89
|
+
data.clientSecret,
|
|
90
|
+
data.authorizeUrl,
|
|
91
|
+
data.tokenUrl,
|
|
92
|
+
data.redirectUri,
|
|
93
|
+
data.scopes,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
66
96
|
}
|
|
67
97
|
|
|
68
98
|
// ── OAuth Tokens ──
|
|
@@ -41,6 +41,75 @@ export function getTrackerSyncByExternalId(
|
|
|
41
41
|
return row ? normalizeTrackerSync(row) : null;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Idempotent UNIQUE-gated insert into `tracker_sync`. Returns
|
|
46
|
+
* `{ inserted: true, sync }` when a fresh row was created, or
|
|
47
|
+
* `{ inserted: false, sync }` when the `(provider, entityType, externalId)`
|
|
48
|
+
* tuple already had a row. Used by inbound webhook handlers (currently Jira)
|
|
49
|
+
* to gate task creation atomically: insert sync row first, only call
|
|
50
|
+
* `createTaskExtended` if `inserted === true`.
|
|
51
|
+
*
|
|
52
|
+
* Note: this is a "claim" insert — `swarmId` is initially the sentinel value
|
|
53
|
+
* passed in (callers typically pass a placeholder like `""` or a known UUID),
|
|
54
|
+
* then update it with `updateTrackerSyncSwarmId` once the task is created.
|
|
55
|
+
*/
|
|
56
|
+
export function createTrackerSyncIfAbsent(data: {
|
|
57
|
+
provider: string;
|
|
58
|
+
entityType: "task";
|
|
59
|
+
providerEntityType?: string | null;
|
|
60
|
+
swarmId: string;
|
|
61
|
+
externalId: string;
|
|
62
|
+
externalIdentifier?: string | null;
|
|
63
|
+
externalUrl?: string | null;
|
|
64
|
+
lastSyncOrigin?: "swarm" | "external" | null;
|
|
65
|
+
lastDeliveryId?: string | null;
|
|
66
|
+
syncDirection?: "inbound" | "outbound" | "bidirectional";
|
|
67
|
+
}): { inserted: boolean; sync: TrackerSync } {
|
|
68
|
+
const insertResult = getDb()
|
|
69
|
+
.query(
|
|
70
|
+
`INSERT INTO tracker_sync (provider, entityType, providerEntityType, swarmId, externalId, externalIdentifier, externalUrl, lastSyncOrigin, lastDeliveryId, syncDirection)
|
|
71
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
72
|
+
ON CONFLICT (provider, entityType, externalId) DO NOTHING
|
|
73
|
+
RETURNING *`,
|
|
74
|
+
)
|
|
75
|
+
.get(
|
|
76
|
+
data.provider,
|
|
77
|
+
data.entityType,
|
|
78
|
+
data.providerEntityType ?? null,
|
|
79
|
+
data.swarmId,
|
|
80
|
+
data.externalId,
|
|
81
|
+
data.externalIdentifier ?? null,
|
|
82
|
+
data.externalUrl ?? null,
|
|
83
|
+
data.lastSyncOrigin ?? null,
|
|
84
|
+
data.lastDeliveryId ?? null,
|
|
85
|
+
data.syncDirection ?? "inbound",
|
|
86
|
+
) as TrackerSync | null;
|
|
87
|
+
|
|
88
|
+
if (insertResult) {
|
|
89
|
+
return { inserted: true, sync: normalizeTrackerSync(insertResult) };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Row already existed — fetch it for the caller.
|
|
93
|
+
const existing = getTrackerSyncByExternalId(data.provider, data.entityType, data.externalId);
|
|
94
|
+
if (!existing) {
|
|
95
|
+
// Should be unreachable: ON CONFLICT means a row exists, but this guard
|
|
96
|
+
// satisfies the type system and surfaces unexpected races loudly.
|
|
97
|
+
throw new Error(
|
|
98
|
+
`[tracker] createTrackerSyncIfAbsent: ON CONFLICT fired but no existing row found for (${data.provider}, ${data.entityType}, ${data.externalId})`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
return { inserted: false, sync: existing };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Update the `swarmId` on an existing `tracker_sync` row. Used after the
|
|
106
|
+
* idempotent `createTrackerSyncIfAbsent` returned `{ inserted: true }` and
|
|
107
|
+
* we've now created the swarm task that should own this row.
|
|
108
|
+
*/
|
|
109
|
+
export function updateTrackerSyncSwarmId(id: string, swarmId: string): void {
|
|
110
|
+
getDb().query("UPDATE tracker_sync SET swarmId = ? WHERE id = ?").run(swarmId, id);
|
|
111
|
+
}
|
|
112
|
+
|
|
44
113
|
export function createTrackerSync(data: {
|
|
45
114
|
provider: string;
|
|
46
115
|
entityType: "task";
|
|
@@ -128,6 +197,46 @@ export function deleteTrackerSync(id: string): void {
|
|
|
128
197
|
getDb().query("DELETE FROM tracker_sync WHERE id = ?").run(id);
|
|
129
198
|
}
|
|
130
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Check whether a `tracker_sync` row exists for `provider` with the given
|
|
202
|
+
* `lastDeliveryId`. Used by inbound webhook handlers (Jira) to dedupe
|
|
203
|
+
* deliveries via DB-persisted state instead of a process-local Map.
|
|
204
|
+
*
|
|
205
|
+
* Returns `false` when `deliveryId` is falsy/empty so callers don't have to
|
|
206
|
+
* branch.
|
|
207
|
+
*/
|
|
208
|
+
export function hasTrackerDelivery(
|
|
209
|
+
provider: string,
|
|
210
|
+
deliveryId: string | null | undefined,
|
|
211
|
+
): boolean {
|
|
212
|
+
if (!deliveryId) return false;
|
|
213
|
+
const row = getDb()
|
|
214
|
+
.query("SELECT 1 AS hit FROM tracker_sync WHERE provider = ? AND lastDeliveryId = ? LIMIT 1")
|
|
215
|
+
.get(provider, deliveryId) as { hit: number } | null;
|
|
216
|
+
return !!row;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Mark a delivery as processed by writing `deliveryId` into the relevant
|
|
221
|
+
* `tracker_sync` row identified by `(provider, entityType, externalId)`.
|
|
222
|
+
*
|
|
223
|
+
* No-op when the row doesn't exist yet (the very first inbound event creates
|
|
224
|
+
* the row via `createTrackerSyncIfAbsent` — recording delivery happens after
|
|
225
|
+
* that). Caller is responsible for ordering.
|
|
226
|
+
*/
|
|
227
|
+
export function markTrackerDelivery(
|
|
228
|
+
provider: string,
|
|
229
|
+
entityType: "task",
|
|
230
|
+
externalId: string,
|
|
231
|
+
deliveryId: string,
|
|
232
|
+
): void {
|
|
233
|
+
getDb()
|
|
234
|
+
.query(
|
|
235
|
+
"UPDATE tracker_sync SET lastDeliveryId = ? WHERE provider = ? AND entityType = ? AND externalId = ?",
|
|
236
|
+
)
|
|
237
|
+
.run(deliveryId, provider, entityType, externalId);
|
|
238
|
+
}
|
|
239
|
+
|
|
131
240
|
export function getAllTrackerSyncs(provider?: string, entityType?: "task"): TrackerSync[] {
|
|
132
241
|
const conditions: string[] = [];
|
|
133
242
|
const values: string[] = [];
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
-- Add 'jira' to the agent_tasks.source CHECK constraint.
|
|
2
|
+
-- SQLite cannot ALTER an existing CHECK; we follow the table-rebuild pattern
|
|
3
|
+
-- (per migration 026). Column list mirrors the live post-042 schema verbatim
|
|
4
|
+
-- — including all post-026 additions (credentialKeySuffix/Type, requestedByUserId,
|
|
5
|
+
-- vcsInstallationId, vcsNodeId, slackReplySent, swarmVersion, contextKey).
|
|
6
|
+
-- INSERT uses an explicit column list (no `SELECT *`) to be robust against
|
|
7
|
+
-- column-order drift between SQLite versions.
|
|
8
|
+
PRAGMA foreign_keys=off;
|
|
9
|
+
|
|
10
|
+
CREATE TABLE agent_tasks_new (
|
|
11
|
+
id TEXT PRIMARY KEY,
|
|
12
|
+
agentId TEXT,
|
|
13
|
+
creatorAgentId TEXT,
|
|
14
|
+
task TEXT NOT NULL,
|
|
15
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
16
|
+
source TEXT NOT NULL DEFAULT 'mcp' CHECK(source IN ('mcp', 'slack', 'api', 'github', 'gitlab', 'agentmail', 'system', 'schedule', 'workflow', 'linear', 'jira')),
|
|
17
|
+
taskType TEXT,
|
|
18
|
+
tags TEXT DEFAULT '[]',
|
|
19
|
+
priority INTEGER DEFAULT 50,
|
|
20
|
+
dependsOn TEXT DEFAULT '[]',
|
|
21
|
+
offeredTo TEXT,
|
|
22
|
+
offeredAt TEXT,
|
|
23
|
+
acceptedAt TEXT,
|
|
24
|
+
rejectionReason TEXT,
|
|
25
|
+
slackChannelId TEXT,
|
|
26
|
+
slackThreadTs TEXT,
|
|
27
|
+
slackUserId TEXT,
|
|
28
|
+
mentionMessageId TEXT,
|
|
29
|
+
mentionChannelId TEXT,
|
|
30
|
+
vcsProvider TEXT,
|
|
31
|
+
vcsRepo TEXT,
|
|
32
|
+
vcsEventType TEXT,
|
|
33
|
+
vcsNumber INTEGER,
|
|
34
|
+
vcsCommentId INTEGER,
|
|
35
|
+
vcsAuthor TEXT,
|
|
36
|
+
vcsUrl TEXT,
|
|
37
|
+
parentTaskId TEXT,
|
|
38
|
+
claudeSessionId TEXT,
|
|
39
|
+
agentmailInboxId TEXT,
|
|
40
|
+
agentmailMessageId TEXT,
|
|
41
|
+
agentmailThreadId TEXT,
|
|
42
|
+
model TEXT,
|
|
43
|
+
scheduleId TEXT,
|
|
44
|
+
workflowRunId TEXT REFERENCES workflow_runs(id),
|
|
45
|
+
workflowRunStepId TEXT REFERENCES workflow_run_steps(id),
|
|
46
|
+
createdAt TEXT NOT NULL,
|
|
47
|
+
lastUpdatedAt TEXT NOT NULL,
|
|
48
|
+
finishedAt TEXT,
|
|
49
|
+
failureReason TEXT,
|
|
50
|
+
output TEXT,
|
|
51
|
+
progress TEXT,
|
|
52
|
+
notifiedAt TEXT,
|
|
53
|
+
dir TEXT,
|
|
54
|
+
outputSchema TEXT,
|
|
55
|
+
compactionCount INTEGER DEFAULT 0,
|
|
56
|
+
peakContextPercent REAL,
|
|
57
|
+
totalContextTokensUsed INTEGER,
|
|
58
|
+
contextWindowSize INTEGER,
|
|
59
|
+
was_paused INTEGER NOT NULL DEFAULT 0,
|
|
60
|
+
credentialKeySuffix TEXT,
|
|
61
|
+
credentialKeyType TEXT,
|
|
62
|
+
requestedByUserId TEXT REFERENCES users(id),
|
|
63
|
+
vcsInstallationId INTEGER,
|
|
64
|
+
vcsNodeId TEXT,
|
|
65
|
+
slackReplySent INTEGER DEFAULT 0,
|
|
66
|
+
swarmVersion TEXT,
|
|
67
|
+
contextKey TEXT
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
INSERT INTO agent_tasks_new (
|
|
71
|
+
id, agentId, creatorAgentId, task, status, source, taskType, tags,
|
|
72
|
+
priority, dependsOn, offeredTo, offeredAt, acceptedAt, rejectionReason,
|
|
73
|
+
slackChannelId, slackThreadTs, slackUserId,
|
|
74
|
+
mentionMessageId, mentionChannelId,
|
|
75
|
+
vcsProvider, vcsRepo, vcsEventType, vcsNumber, vcsCommentId, vcsAuthor, vcsUrl,
|
|
76
|
+
parentTaskId, claudeSessionId,
|
|
77
|
+
agentmailInboxId, agentmailMessageId, agentmailThreadId,
|
|
78
|
+
model, scheduleId, workflowRunId, workflowRunStepId,
|
|
79
|
+
createdAt, lastUpdatedAt, finishedAt, failureReason, output, progress, notifiedAt,
|
|
80
|
+
dir, outputSchema, compactionCount, peakContextPercent,
|
|
81
|
+
totalContextTokensUsed, contextWindowSize, was_paused,
|
|
82
|
+
credentialKeySuffix, credentialKeyType, requestedByUserId,
|
|
83
|
+
vcsInstallationId, vcsNodeId, slackReplySent, swarmVersion, contextKey
|
|
84
|
+
)
|
|
85
|
+
SELECT
|
|
86
|
+
id, agentId, creatorAgentId, task, status, source, taskType, tags,
|
|
87
|
+
priority, dependsOn, offeredTo, offeredAt, acceptedAt, rejectionReason,
|
|
88
|
+
slackChannelId, slackThreadTs, slackUserId,
|
|
89
|
+
mentionMessageId, mentionChannelId,
|
|
90
|
+
vcsProvider, vcsRepo, vcsEventType, vcsNumber, vcsCommentId, vcsAuthor, vcsUrl,
|
|
91
|
+
parentTaskId, claudeSessionId,
|
|
92
|
+
agentmailInboxId, agentmailMessageId, agentmailThreadId,
|
|
93
|
+
model, scheduleId, workflowRunId, workflowRunStepId,
|
|
94
|
+
createdAt, lastUpdatedAt, finishedAt, failureReason, output, progress, notifiedAt,
|
|
95
|
+
dir, outputSchema, compactionCount, peakContextPercent,
|
|
96
|
+
totalContextTokensUsed, contextWindowSize, was_paused,
|
|
97
|
+
credentialKeySuffix, credentialKeyType, requestedByUserId,
|
|
98
|
+
vcsInstallationId, vcsNodeId, slackReplySent, swarmVersion, contextKey
|
|
99
|
+
FROM agent_tasks;
|
|
100
|
+
|
|
101
|
+
DROP TABLE agent_tasks;
|
|
102
|
+
ALTER TABLE agent_tasks_new RENAME TO agent_tasks;
|
|
103
|
+
|
|
104
|
+
-- Recreate every index that existed on agent_tasks (per `grep -rn "ON agent_tasks(" src/be/migrations/`):
|
|
105
|
+
-- 001/004/006/009/026: agentId, status, offeredTo, taskType, agentmailThreadId, scheduleId, workflowRunId
|
|
106
|
+
-- 031: requestedByUserId (partial)
|
|
107
|
+
-- 034: parentTaskId
|
|
108
|
+
-- 037: swarmVersion
|
|
109
|
+
-- 040: composite (slackChannelId, slackThreadTs, status)
|
|
110
|
+
-- 042: contextKey + (contextKey, status) composite
|
|
111
|
+
CREATE INDEX IF NOT EXISTS idx_agent_tasks_agentId ON agent_tasks(agentId);
|
|
112
|
+
CREATE INDEX IF NOT EXISTS idx_agent_tasks_status ON agent_tasks(status);
|
|
113
|
+
CREATE INDEX IF NOT EXISTS idx_agent_tasks_offeredTo ON agent_tasks(offeredTo);
|
|
114
|
+
CREATE INDEX IF NOT EXISTS idx_agent_tasks_taskType ON agent_tasks(taskType);
|
|
115
|
+
CREATE INDEX IF NOT EXISTS idx_agent_tasks_agentmailThreadId ON agent_tasks(agentmailThreadId);
|
|
116
|
+
CREATE INDEX IF NOT EXISTS idx_agent_tasks_schedule_id ON agent_tasks(scheduleId);
|
|
117
|
+
CREATE INDEX IF NOT EXISTS idx_agent_tasks_workflow_run ON agent_tasks(workflowRunId);
|
|
118
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_requested_by ON agent_tasks(requestedByUserId) WHERE requestedByUserId IS NOT NULL;
|
|
119
|
+
CREATE INDEX IF NOT EXISTS idx_agent_tasks_parentTaskId ON agent_tasks(parentTaskId);
|
|
120
|
+
CREATE INDEX IF NOT EXISTS idx_agent_tasks_swarmVersion ON agent_tasks(swarmVersion);
|
|
121
|
+
CREATE INDEX IF NOT EXISTS idx_agent_tasks_slack_thread
|
|
122
|
+
ON agent_tasks(slackChannelId, slackThreadTs, status);
|
|
123
|
+
CREATE INDEX IF NOT EXISTS idx_agent_tasks_context_key
|
|
124
|
+
ON agent_tasks(contextKey);
|
|
125
|
+
CREATE INDEX IF NOT EXISTS idx_agent_tasks_context_key_status
|
|
126
|
+
ON agent_tasks(contextKey, status);
|
|
127
|
+
|
|
128
|
+
PRAGMA foreign_keys=on;
|
package/src/commands/runner.ts
CHANGED
|
@@ -2207,8 +2207,13 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
2207
2207
|
configureHttpResolver(apiUrl, process.env.API_KEY);
|
|
2208
2208
|
}
|
|
2209
2209
|
|
|
2210
|
-
// Initialize anonymized telemetry (opt-out via ANONYMIZED_TELEMETRY=false)
|
|
2211
|
-
// Workers use HTTP-based config access (cannot import DB directly)
|
|
2210
|
+
// Initialize anonymized telemetry (opt-out via ANONYMIZED_TELEMETRY=false).
|
|
2211
|
+
// Workers use HTTP-based config access (cannot import DB directly).
|
|
2212
|
+
// IMPORTANT: workers must NOT pass `generateIfMissing` — the api-server is
|
|
2213
|
+
// the sole authority for `telemetry_installation_id`. If the API hasn't
|
|
2214
|
+
// persisted one yet (network blip, fresh boot, API down), the worker simply
|
|
2215
|
+
// skips telemetry instead of minting a fresh `install_<hex>` ID per
|
|
2216
|
+
// restart, which floods prod metrics with phantom installs.
|
|
2212
2217
|
{
|
|
2213
2218
|
const telemetryApiKey = process.env.API_KEY;
|
|
2214
2219
|
await initTelemetry(
|
package/src/http/core.ts
CHANGED
|
@@ -11,32 +11,14 @@ import {
|
|
|
11
11
|
updateAgentStatus,
|
|
12
12
|
} from "../be/db";
|
|
13
13
|
import { initGitHub, resetGitHub } from "../github";
|
|
14
|
+
import { initJira, resetJira } from "../jira";
|
|
14
15
|
import { initLinear, resetLinear } from "../linear";
|
|
15
16
|
import { startSlackApp, stopSlackApp } from "../slack";
|
|
16
17
|
import type { AgentStatus } from "../types";
|
|
17
18
|
import { refreshSecretScrubberCache } from "../utils/secret-scrubber";
|
|
18
19
|
import { generateOpenApiSpec, SCALAR_HTML } from "./openapi";
|
|
19
|
-
import {
|
|
20
|
-
import { agentWithCapacity, getPathSegments,
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Check whether a request targets a route declared (via the `route()` factory)
|
|
24
|
-
* with `auth: { apiKey: false }` — i.e. one that opts out of the API-key
|
|
25
|
-
* bearer check. Handler files must use the `route()` factory for this to take
|
|
26
|
-
* effect; unknown paths fail closed (auth required).
|
|
27
|
-
*/
|
|
28
|
-
function isPublicRoute(method: string | undefined, pathSegments: string[]): boolean {
|
|
29
|
-
for (const def of routeRegistry) {
|
|
30
|
-
if (def.auth?.apiKey === false) {
|
|
31
|
-
if (
|
|
32
|
-
matchRoute(method, pathSegments, def.method.toUpperCase(), def.pattern, def.exact ?? true)
|
|
33
|
-
) {
|
|
34
|
-
return true;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
20
|
+
import { isPublicRoute } from "./route-def";
|
|
21
|
+
import { agentWithCapacity, getPathSegments, parseQueryParams } from "./utils";
|
|
40
22
|
|
|
41
23
|
/**
|
|
42
24
|
* Load global swarm_config entries into process.env.
|
|
@@ -88,6 +70,9 @@ export async function reloadGlobalConfigsAndIntegrations(): Promise<ReloadConfig
|
|
|
88
70
|
resetLinear();
|
|
89
71
|
if (initLinear()) integrations.push("linear");
|
|
90
72
|
|
|
73
|
+
resetJira();
|
|
74
|
+
if (initJira()) integrations.push("jira");
|
|
75
|
+
|
|
91
76
|
await stopSlackApp();
|
|
92
77
|
await startSlackApp();
|
|
93
78
|
integrations.push("slack");
|
package/src/http/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { closeDb, getSwarmConfigs, upsertSwarmConfig } from "../be/db";
|
|
|
12
12
|
import { initGitHub } from "../github";
|
|
13
13
|
import { initGitLab } from "../gitlab";
|
|
14
14
|
import { stopHeartbeat } from "../heartbeat";
|
|
15
|
+
import { initJira } from "../jira";
|
|
15
16
|
import { initLinear } from "../linear";
|
|
16
17
|
import { startSlackApp, stopSlackApp } from "../slack";
|
|
17
18
|
import { initTelemetry, telemetry } from "../telemetry";
|
|
@@ -245,13 +246,17 @@ httpServer
|
|
|
245
246
|
);
|
|
246
247
|
}
|
|
247
248
|
|
|
248
|
-
// Initialize anonymized telemetry (opt-out via ANONYMIZED_TELEMETRY=false)
|
|
249
|
+
// Initialize anonymized telemetry (opt-out via ANONYMIZED_TELEMETRY=false).
|
|
250
|
+
// The api-server is the sole authority for the install identity — pass
|
|
251
|
+
// generateIfMissing so it mints a new install ID on first boot. Workers
|
|
252
|
+
// must NOT mint (see src/commands/runner.ts).
|
|
249
253
|
await initTelemetry(
|
|
250
254
|
"api-server",
|
|
251
255
|
(key) => getSwarmConfigs({ scope: "global", key })?.[0]?.value,
|
|
252
256
|
(key, value) => {
|
|
253
257
|
upsertSwarmConfig({ scope: "global", key, value });
|
|
254
258
|
},
|
|
259
|
+
{ generateIfMissing: true },
|
|
255
260
|
);
|
|
256
261
|
telemetry.server("started", { port });
|
|
257
262
|
|
|
@@ -270,6 +275,9 @@ httpServer
|
|
|
270
275
|
// Initialize Linear tracker integration (if configured)
|
|
271
276
|
initLinear();
|
|
272
277
|
|
|
278
|
+
// Initialize Jira tracker integration (if configured)
|
|
279
|
+
initJira();
|
|
280
|
+
|
|
273
281
|
// Initialize workflow engine (trigger subscriptions + resume listener)
|
|
274
282
|
initWorkflows();
|
|
275
283
|
|
package/src/http/route-def.ts
CHANGED
|
@@ -60,6 +60,25 @@ interface RouteHandle<TParams, TQuery, TBody> {
|
|
|
60
60
|
/** Global registry — populated at import time, read by OpenAPI generator */
|
|
61
61
|
export const routeRegistry: RouteDef[] = [];
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Check whether a request targets a route declared (via the `route()` factory)
|
|
65
|
+
* with `auth: { apiKey: false }` — i.e. one that opts out of the API-key
|
|
66
|
+
* bearer check. Handler files must use the `route()` factory for this to take
|
|
67
|
+
* effect; unknown paths fail closed (auth required).
|
|
68
|
+
*/
|
|
69
|
+
export function isPublicRoute(method: string | undefined, pathSegments: string[]): boolean {
|
|
70
|
+
for (const def of routeRegistry) {
|
|
71
|
+
if (def.auth?.apiKey === false) {
|
|
72
|
+
if (
|
|
73
|
+
matchRoute(method, pathSegments, def.method.toUpperCase(), def.pattern, def.exact ?? true)
|
|
74
|
+
) {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
63
82
|
// ─── Factory ─────────────────────────────────────────────────────────────────
|
|
64
83
|
|
|
65
84
|
export function route<
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import { handleJiraTracker } from "./jira";
|
|
2
3
|
import { handleLinearTracker } from "./linear";
|
|
3
4
|
|
|
4
5
|
export async function handleTrackers(
|
|
@@ -6,5 +7,17 @@ export async function handleTrackers(
|
|
|
6
7
|
res: ServerResponse,
|
|
7
8
|
pathSegments: string[],
|
|
8
9
|
): Promise<boolean> {
|
|
10
|
+
// Provider-specific dispatch based on the third path segment
|
|
11
|
+
// (e.g. "api", "trackers", "<provider>", ...).
|
|
12
|
+
if (pathSegments[0] === "api" && pathSegments[1] === "trackers") {
|
|
13
|
+
if (pathSegments[2] === "jira") {
|
|
14
|
+
return await handleJiraTracker(req, res, pathSegments);
|
|
15
|
+
}
|
|
16
|
+
if (pathSegments[2] === "linear") {
|
|
17
|
+
return await handleLinearTracker(req, res, pathSegments);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
// Fallback: try Linear (preserves existing behavior for any path that
|
|
21
|
+
// somehow falls through without an explicit provider segment).
|
|
9
22
|
return await handleLinearTracker(req, res, pathSegments);
|
|
10
23
|
}
|