@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.
Files changed (237) hide show
  1. package/README.md +2 -2
  2. package/dist/{actions-q4n7cz6e.js → actions-17ya5wde.js} +8 -9
  3. package/dist/{app-es4nzc71.js → app-4f718yk0.js} +6 -6
  4. package/dist/{assistant-0x8ey0vs.js → assistant-ah7csnjz.js} +19 -18
  5. package/dist/{boot-reembed-69agkhv6.js → boot-reembed-4jpxhb25.js} +6 -6
  6. package/dist/{boot-reembed-p0ny05t8.js → boot-reembed-qata34xa.js} +5 -5
  7. package/dist/{boot-scrub-logs-yybrkmvr.js → boot-scrub-logs-tv48nn3z.js} +4 -4
  8. package/dist/{claude-adapter-mma9a1yr.js → claude-adapter-3csn5bks.js} +3 -3
  9. package/dist/{claude-managed-adapter-pv2nbqy4.js → claude-managed-adapter-kysmkz89.js} +2 -2
  10. package/dist/cli-0pygq9r1.js +429 -0
  11. package/dist/{cli-hsetkkd3.js → cli-12vb0xg6.js} +33 -9
  12. package/dist/{cli-ncfgsqbd.js → cli-15y7qhzp.js} +262 -33
  13. package/dist/{cli-7ga43spr.js → cli-1yjrbg1f.js} +1 -1
  14. package/dist/{cli-36ymmpmp.js → cli-3samkyap.js} +2 -2
  15. package/dist/cli-52fg7s7x.js +269 -0
  16. package/dist/{cli-hjhvekf4.js → cli-65ekje01.js} +11 -8
  17. package/dist/{cli-h3k622d0.js → cli-6aq8xhvm.js} +90 -3
  18. package/dist/{cli-f3qa069v.js → cli-7xje5amz.js} +1 -1
  19. package/dist/cli-86fdnerz.js +405 -0
  20. package/dist/{cli-9cy6nc5m.js → cli-8fjwym53.js} +1 -1
  21. package/dist/{cli-c3frqf65.js → cli-b23axgz4.js} +3 -3
  22. package/dist/{cli-a0t7vapf.js → cli-bc89my0z.js} +1 -1
  23. package/dist/{cli-bnzbn8j8.js → cli-beea4a3y.js} +73 -24
  24. package/dist/{cli-q21d49ac.js → cli-bw80ck94.js} +1 -1
  25. package/dist/{cli-pwc5e83c.js → cli-cchksf5q.js} +12 -7
  26. package/dist/{cli-30bbaveh.js → cli-cmw9rzy5.js} +2 -2
  27. package/dist/{cli-y6mrptcn.js → cli-cnwssmsr.js} +1 -1
  28. package/dist/{cli-r5g6ngtn.js → cli-dcca6j8m.js} +46 -5
  29. package/dist/{cli-tkmm5nf9.js → cli-e61fs2hp.js} +59 -2
  30. package/dist/{cli-6wscj9z0.js → cli-e7306swv.js} +14 -14
  31. package/dist/{cli-dcfyyh3t.js → cli-ftyf58xj.js} +2 -2
  32. package/dist/{cli-4xyj9jtq.js → cli-hrhye2rm.js} +1062 -478
  33. package/dist/cli-jkhwmmgg.js +136 -0
  34. package/dist/{cli-y4ycrnjc.js → cli-k3y4z3h5.js} +1 -1
  35. package/dist/{cli-6xd0bvf8.js → cli-ma04faw5.js} +1 -1
  36. package/dist/{cli-2fc97xm5.js → cli-mnbgv3w7.js} +4 -4
  37. package/dist/{cli-trmapn9k.js → cli-pet3aev6.js} +95 -10
  38. package/dist/{cli-z46pnksz.js → cli-q1rx7g0d.js} +175 -28
  39. package/dist/{cli-qwgtacd6.js → cli-r9f6ngrf.js} +1244 -336
  40. package/dist/{cli-gtkqc144.js → cli-shvaxw27.js} +13 -7
  41. package/dist/{cli-ywez6yps.js → cli-sq7f72d6.js} +1 -1
  42. package/dist/cli-t0vqevnm.js +393 -0
  43. package/dist/{cli-tbw13sx8.js → cli-tece6zgm.js} +1 -1
  44. package/dist/{cli-bgvskydy.js → cli-tffvrh6j.js} +1 -1
  45. package/dist/cli-wh91zby6.js +187222 -0
  46. package/dist/{cli-anrj584m.js → cli-xz9aq0rf.js} +1 -1
  47. package/dist/{cli-mq580e06.js → cli-zsrdty74.js} +18 -18
  48. package/dist/cli.js +32 -15
  49. package/dist/{codex-adapter-5cmy858w.js → codex-adapter-de3w0nch.js} +10 -10
  50. package/dist/{codex-session-runner-46q8mrba.js → codex-session-runner-bnw8cysk.js} +10 -10
  51. package/dist/{commands-z5dbwta3.js → commands-hch4e2k1.js} +4 -4
  52. package/dist/{db-bbgahh4z.js → db-p3h2j06x.js} +14 -4
  53. package/dist/{handlers-6b44317z.js → handlers-9er4zx9q.js} +17 -31
  54. package/dist/{hook-sbp5fmps.js → hook-4axs756z.js} +7 -7
  55. package/dist/{http-2xz43gz3.js → http-809fvtz2.js} +1934 -886
  56. package/dist/{index-wz1ea15p.js → index-2dgfj9av.js} +3 -3
  57. package/dist/{index-nhat35em.js → index-47em3d2n.js} +14 -11
  58. package/dist/{index-5ghxn8s6.js → index-9fqaktjr.js} +12 -9
  59. package/dist/{index-0gzmqj4k.js → index-9ht8gr56.js} +13 -10
  60. package/dist/{index-vfedgdw6.js → index-rm85cfdx.js} +368 -40
  61. package/dist/{index-ykvs43z4.js → index-t2snazez.js} +4 -4
  62. package/dist/{keepalive-62gfj91d.js → keepalive-3cps6e3e.js} +7 -6
  63. package/dist/{lead-ag0akn0v.js → lead-fkk95wgq.js} +36 -32
  64. package/dist/{maintenance-xxrrzk5x.js → maintenance-fweqh0yj.js} +6 -6
  65. package/dist/{mistral-conversations-26yrnbf1.js → mistral-conversations-7gqnz2wt.js} +7 -7
  66. package/dist/oauth-refresh-sweep-sycqtbph.js +114 -0
  67. package/dist/{onboard-64zyfcbs.js → onboard-cy0rkgvx.js} +2 -2
  68. package/dist/{opencode-adapter-mqsxr170.js → opencode-adapter-p6bxnbm4.js} +2 -2
  69. package/dist/{otel-zjd9zaxs.js → otel-g299k41b.js} +2 -2
  70. package/dist/{otel-impl-b86qseaa.js → otel-impl-wbns8hf7.js} +1 -1
  71. package/dist/{pi-mono-adapter-kxjxpfgy.js → pi-mono-adapter-05sck1ds.js} +17 -109
  72. package/dist/{pricing-refresh-cgtkffpa.js → pricing-refresh-0hvazmkf.js} +6 -6
  73. package/dist/rbac-roles-kfesjy9y.js +28 -0
  74. package/dist/rbac-roles-mwcwhbxc.js +283 -0
  75. package/dist/{seed-pricing-1r1y1xkq.js → seed-pricing-h2kdq4cq.js} +5 -5
  76. package/dist/{setup-mkcgkjf7.js → setup-8506q97w.js} +2 -2
  77. package/dist/{worker-351sze63.js → worker-4r5c6zyb.js} +36 -32
  78. package/openapi.json +1327 -20
  79. package/package.json +3 -3
  80. package/src/agentmail/handlers.ts +16 -6
  81. package/src/agentmail/templates.ts +25 -5
  82. package/src/be/audit-user.ts +28 -4
  83. package/src/be/db-queries/oauth.ts +42 -0
  84. package/src/be/db.ts +314 -28
  85. package/src/be/identity.ts +79 -0
  86. package/src/be/mcp-proxy.ts +132 -0
  87. package/src/be/migrations/109_rbac_roles.sql +79 -0
  88. package/src/be/migrations/111_oauth_credential_bindings.sql +6 -0
  89. package/src/be/migrations/112_script_connections_graphql.sql +96 -0
  90. package/src/be/migrations/113_task_routing_affinity.sql +9 -0
  91. package/src/be/migrations/114_backfill_gpt_5_6_pricing.sql +15 -0
  92. package/src/be/modelsdev-cache.json +135 -0
  93. package/src/be/oauth-credential-bindings.ts +35 -0
  94. package/src/be/oauth-refresh-sweep.ts +159 -0
  95. package/src/be/rbac-audit.ts +52 -13
  96. package/src/be/rbac-roles.ts +424 -0
  97. package/src/be/script-connections.ts +459 -40
  98. package/src/be/script-credential-broker.ts +29 -3
  99. package/src/be/scripts/typecheck.ts +51 -7
  100. package/src/be/seed-scripts/catalog/catalog-report.inline.ts +325 -24
  101. package/src/be/seed-scripts/catalog/catalog-report.ts +325 -24
  102. package/src/be/users.ts +24 -0
  103. package/src/cli.tsx +17 -0
  104. package/src/github/handlers.ts +10 -9
  105. package/src/github/templates.ts +45 -9
  106. package/src/gitlab/handlers.ts +20 -8
  107. package/src/gitlab/templates.ts +18 -6
  108. package/src/heartbeat/heartbeat.ts +143 -21
  109. package/src/http/all-routes.ts +3 -0
  110. package/src/http/approval-requests.ts +12 -0
  111. package/src/http/config.ts +44 -10
  112. package/src/http/core.ts +30 -3
  113. package/src/http/favorites.ts +1 -5
  114. package/src/http/index.ts +25 -1
  115. package/src/http/mcp-oauth.ts +2 -0
  116. package/src/http/mcp-servers.ts +44 -3
  117. package/src/http/oauth-generic.ts +105 -0
  118. package/src/http/poll.ts +20 -3
  119. package/src/http/script-connection-proxy.ts +116 -0
  120. package/src/http/script-connections.ts +1657 -0
  121. package/src/http/scripts.ts +27 -7
  122. package/src/http/skills.ts +11 -0
  123. package/src/http/tasks.ts +27 -13
  124. package/src/http/x.ts +6 -2
  125. package/src/integrations/kapso/inbound.ts +23 -6
  126. package/src/jira/sync.ts +111 -17
  127. package/src/jira/templates.ts +10 -2
  128. package/src/mcp-client/http-client.ts +194 -0
  129. package/src/oauth/app-validation.ts +16 -0
  130. package/src/oauth/ensure-token.ts +69 -14
  131. package/src/oauth/mcp-wrapper.ts +15 -0
  132. package/src/oauth/wrapper.ts +48 -20
  133. package/src/prompts/session-templates.ts +3 -3
  134. package/src/providers/codex-models.ts +100 -30
  135. package/src/providers/modelsdev-reasoning.json +51 -0
  136. package/src/providers/pi-mono-mcp-client.ts +2 -133
  137. package/src/providers/reasoning-effort.ts +11 -5
  138. package/src/rbac/admission.ts +68 -0
  139. package/src/rbac/index.ts +2 -0
  140. package/src/rbac/legacy-policy.ts +12 -0
  141. package/src/rbac/permissions.ts +48 -0
  142. package/src/scheduler/scheduler.ts +9 -1
  143. package/src/scripts-runtime/api-client.ts +151 -19
  144. package/src/scripts-runtime/api-types.ts +38 -6
  145. package/src/scripts-runtime/credential-broker/default-bindings.ts +1 -0
  146. package/src/scripts-runtime/credential-broker/types.ts +8 -0
  147. package/src/scripts-runtime/ctx.ts +11 -1
  148. package/src/scripts-runtime/eval-harness.ts +5 -1
  149. package/src/scripts-runtime/executors/types.ts +2 -1
  150. package/src/scripts-runtime/loader.ts +3 -1
  151. package/src/scripts-runtime/mcp-client.ts +87 -0
  152. package/src/scripts-runtime/sdk-allowlist.ts +1 -0
  153. package/src/scripts-runtime/types/stdlib.d.ts +6 -0
  154. package/src/scripts-runtime/types/swarm-sdk.d.ts +19 -0
  155. package/src/server-user.ts +114 -59
  156. package/src/server.ts +12 -0
  157. package/src/slack/assistant.ts +14 -6
  158. package/src/slack/enrich.ts +17 -0
  159. package/src/slack/handlers.ts +8 -22
  160. package/src/slack/responses.ts +91 -1
  161. package/src/slack/thread-buffer.ts +6 -3
  162. package/src/slack/watcher.ts +9 -6
  163. package/src/tasks/worker-follow-up.ts +100 -3
  164. package/src/tests/agentmail-handlers.test.ts +32 -3
  165. package/src/tests/approval-requests.test.ts +61 -0
  166. package/src/tests/audit-user.test.ts +47 -1
  167. package/src/tests/codex-adapter.test.ts +46 -23
  168. package/src/tests/credential-broker.test.ts +5 -2
  169. package/src/tests/delete-page-tool.test.ts +197 -0
  170. package/src/tests/github-handlers.test.ts +28 -2
  171. package/src/tests/gitlab-handlers.test.ts +125 -0
  172. package/src/tests/heartbeat.test.ts +48 -3
  173. package/src/tests/identity.test.ts +117 -0
  174. package/src/tests/jira-sync.test.ts +99 -1
  175. package/src/tests/kapso-inbound.test.ts +7 -0
  176. package/src/tests/mcp-tools-user.test.ts +60 -6
  177. package/src/tests/model-control.test.ts +8 -2
  178. package/src/tests/oauth-credential-bindings.test.ts +490 -0
  179. package/src/tests/oauth-refresh-sweep.test.ts +160 -0
  180. package/src/tests/pool-affinity.test.ts +401 -0
  181. package/src/tests/prompt-template-remaining.test.ts +2 -2
  182. package/src/tests/rbac-admission-e2e.test.ts +386 -0
  183. package/src/tests/rbac-admission.test.ts +492 -0
  184. package/src/tests/rbac-e2e-helpers.ts +3 -0
  185. package/src/tests/rbac-engine.test.ts +50 -0
  186. package/src/tests/rbac-mcp-admission-e2e.test.ts +362 -0
  187. package/src/tests/rbac-roles.test.ts +328 -0
  188. package/src/tests/reasoning-effort.test.ts +15 -2
  189. package/src/tests/schedule-target-type.test.ts +50 -0
  190. package/src/tests/scheduled-tasks.test.ts +40 -0
  191. package/src/tests/script-apis-mcp.test.ts +37 -13
  192. package/src/tests/script-connections-http.test.ts +1158 -0
  193. package/src/tests/script-connections-mcp.test.ts +894 -0
  194. package/src/tests/script-connections.test.ts +754 -6
  195. package/src/tests/scripts-external-api.test.ts +49 -1
  196. package/src/tests/scripts-runtime-secret-egress.test.ts +1 -0
  197. package/src/tests/scripts-runtime.test.ts +291 -0
  198. package/src/tests/seed-scripts.test.ts +36 -0
  199. package/src/tests/slack-identity-resolution.test.ts +37 -3
  200. package/src/tests/slack-inline-output.test.ts +65 -0
  201. package/src/tests/slack-thread-buffer.test.ts +28 -0
  202. package/src/tests/tool-annotations.test.ts +1 -0
  203. package/src/tests/user-token-rest-auth.test.ts +140 -1
  204. package/src/tests/workflow-swarm-script.test.ts +44 -0
  205. package/src/tests/workflow-triggers-v2.test.ts +300 -1
  206. package/src/tools/credential-bindings/tool.ts +277 -18
  207. package/src/tools/delete-page.ts +145 -0
  208. package/src/tools/mcp-servers/mcp-server-delete.ts +7 -4
  209. package/src/tools/request-human-input.ts +2 -0
  210. package/src/tools/resolve-user.ts +77 -27
  211. package/src/tools/script-apis.ts +62 -3
  212. package/src/tools/script-connections/tool.ts +275 -31
  213. package/src/tools/send-task.ts +15 -1
  214. package/src/tools/task-action.ts +34 -2
  215. package/src/tools/templates.ts +53 -1
  216. package/src/tools/tool-config.ts +2 -1
  217. package/src/tools/utils.ts +2 -0
  218. package/src/tools/workflows/create-workflow.ts +8 -1
  219. package/src/tools/workflows/update-workflow.ts +11 -2
  220. package/src/types.ts +90 -11
  221. package/src/utils/scoped-resource.ts +25 -0
  222. package/src/workflows/executors/human-in-the-loop.ts +1 -0
  223. package/src/workflows/executors/swarm-script.ts +7 -1
  224. package/src/workflows/triggers.ts +146 -18
  225. package/templates/skills/swarm-scripts/content.md +33 -8
  226. package/dist/cli-bgef578c.js +0 -771
  227. package/dist/cli-c9mtm09b.js +0 -43
  228. package/dist/cli-ceewqajw.js +0 -1698
  229. package/dist/{anthropic-messages-8nc6r2dw.js → anthropic-messages-kq3pjhbp.js} +6 -6
  230. package/dist/{azure-openai-responses-6yfzw3qw.js → azure-openai-responses-gm1ejtmn.js} +7 -7
  231. package/dist/{cli-vbraamxr.js → cli-6b4wwmm8.js} +9 -9
  232. package/dist/{cli-2r7s8jhp.js → cli-sznberje.js} +3 -3
  233. package/dist/{google-generative-ai-vfq6ymz9.js → google-generative-ai-hyrra2jm.js} +3 -3
  234. package/dist/{google-vertex-mj5y5rfj.js → google-vertex-8bs2pzgn.js} +3 -3
  235. package/dist/{openai-codex-responses-8h6kvfv8.js → openai-codex-responses-3nkvje79.js} +4 -4
  236. package/dist/{openai-completions-x6kjnqjd.js → openai-completions-0asftkwg.js} +11 -11
  237. package/dist/{openai-responses-7jwyz3ds.js → openai-responses-pmatek45.js} +10 -10
@@ -3,7 +3,7 @@ import { unlink } from "node:fs/promises";
3
3
  import type { IncomingMessage, ServerResponse } from "node:http";
4
4
  import { Readable } from "node:stream";
5
5
  import { __resetEncryptionKeyForTests } from "../be/crypto";
6
- import { closeDb, createAgent, getDb, initDb } from "../be/db";
6
+ import { closeDb, createAgent, createMcpServer, getDb, initDb } from "../be/db";
7
7
  import {
8
8
  createScriptApi,
9
9
  getScriptApiById,
@@ -287,6 +287,54 @@ describe("public execution route", () => {
287
287
  expect(typeof body.durationMs).toBe("number");
288
288
  });
289
289
 
290
+ test("external endpoint runs receive ctx.mcp connections (parity with /api/scripts/run)", async () => {
291
+ const mcpServer = createMcpServer({
292
+ name: `x-mcp-${crypto.randomUUID()}`,
293
+ transport: "http",
294
+ scope: "global",
295
+ url: "http://mcp.invalid.test/mcp",
296
+ });
297
+ const runtimeDescriptor = {
298
+ slug: "xmcp",
299
+ kind: "mcp",
300
+ connectionId: crypto.randomUUID(),
301
+ tools: [{ name: "ping", inputSchema: {} }],
302
+ };
303
+ getDb()
304
+ .prepare(
305
+ `INSERT INTO script_connections
306
+ (id, slug, kind, scope, allowed_hosts_json, mcp_server_id, generated_runtime_json)
307
+ VALUES (?, ?, 'mcp', 'global', '[]', ?, ?)`,
308
+ )
309
+ .run(runtimeDescriptor.connectionId, "xmcp", mcpServer.id, JSON.stringify(runtimeDescriptor));
310
+
311
+ const script = insertScript({
312
+ name: `mcp-keys-${crypto.randomUUID().slice(0, 8)}`,
313
+ scope: "agent",
314
+ scopeId: workerId,
315
+ source:
316
+ "export default async function run(args, ctx) { return { slugs: Object.keys(ctx.mcp ?? {}) }; }",
317
+ description: "Lists ctx.mcp slugs",
318
+ intent: "test fixture",
319
+ signatureJson: "{}",
320
+ });
321
+ const endpoint = createScriptApi({
322
+ scriptId: script.id,
323
+ agentId: workerId,
324
+ authMode: "none",
325
+ });
326
+
327
+ const res = await dispatch(`/api/x/script/${endpoint.id}`, {
328
+ method: "POST",
329
+ body: JSON.stringify({}),
330
+ auth: null,
331
+ });
332
+ const body = (await res.json()) as { ok: boolean; result: { slugs: string[] } };
333
+ expect(res.status).toBe(200);
334
+ expect(body.ok).toBe(true);
335
+ expect(body.result.slugs).toContain("xmcp");
336
+ });
337
+
290
338
  test("oversized body → 413 before execution, even for authMode 'none'", async () => {
291
339
  const script = insertDoubler();
292
340
  const endpoint = createScriptApi({
@@ -61,6 +61,7 @@ describe("egress-substitution", () => {
61
61
  scope: "global",
62
62
  scopeId: null,
63
63
  active: true,
64
+ authKind: "config",
64
65
  value: "ghp_test1234567890abcdef",
65
66
  },
66
67
  ]);
@@ -1,8 +1,10 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { createApiRegistryClient } from "../scripts-runtime/api-client";
2
3
  import { runScript } from "../scripts-runtime/loader";
3
4
  import { refreshSecretScrubberCache } from "../utils/secret-scrubber";
4
5
 
5
6
  const savedEnv = { ...process.env };
7
+ const originalFetch = globalThis.fetch;
6
8
  const resources = { memoryMb: 2048, cpuTimeSec: 20, maxStdoutBytes: 1_048_576 };
7
9
 
8
10
  beforeEach(() => {
@@ -13,6 +15,7 @@ beforeEach(() => {
13
15
  });
14
16
 
15
17
  afterEach(() => {
18
+ globalThis.fetch = originalFetch;
16
19
  for (const key of Object.keys(process.env)) {
17
20
  if (!(key in savedEnv)) delete process.env[key];
18
21
  }
@@ -375,4 +378,292 @@ describe("runScript", () => {
375
378
  expect(output.result).toEqual({ doubled: 84 });
376
379
  expect(output.exitCode).toBe(0);
377
380
  });
381
+
382
+ test("ctx.api GraphQL client posts JSON body with credential placeholders", async () => {
383
+ let observed: {
384
+ url: string;
385
+ method: string;
386
+ contentType: string | null;
387
+ authorization: string | null;
388
+ body: unknown;
389
+ } | null = null;
390
+ globalThis.fetch = (async (input, init) => {
391
+ observed = {
392
+ url: String(input),
393
+ method: init?.method ?? "GET",
394
+ contentType: new Headers(init?.headers).get("content-type"),
395
+ authorization: new Headers(init?.headers).get("authorization"),
396
+ body: JSON.parse(String(init?.body)),
397
+ };
398
+ return Response.json({ data: { country: { name: "Ukraine", capital: "Kyiv" } } });
399
+ }) as typeof fetch;
400
+
401
+ const client = createApiRegistryClient([
402
+ {
403
+ slug: "countries",
404
+ kind: "graphql",
405
+ baseUrl: "https://countries.vendor.test/graphql",
406
+ credential: {
407
+ configKey: "COUNTRIES_KEY",
408
+ headerTemplate: "Authorization: Bearer [REDACTED:COUNTRIES_KEY]",
409
+ },
410
+ },
411
+ ]);
412
+
413
+ const result = await client.countries.graphql(
414
+ "query Country($code: ID!) { country(code: $code) { name capital } }",
415
+ {
416
+ code: "UA",
417
+ },
418
+ );
419
+
420
+ expect(result).toEqual({ country: { name: "Ukraine", capital: "Kyiv" } });
421
+ expect(observed).toEqual({
422
+ url: "https://countries.vendor.test/graphql",
423
+ method: "POST",
424
+ contentType: "application/json",
425
+ authorization: "Bearer [REDACTED:COUNTRIES_KEY]",
426
+ body: {
427
+ query: "query Country($code: ID!) { country(code: $code) { name capital } }",
428
+ variables: { code: "UA" },
429
+ },
430
+ });
431
+ });
432
+
433
+ test("ctx.api OpenAPI client preserves the baseUrl path prefix for absolute spec paths", async () => {
434
+ let observedUrl = "";
435
+ globalThis.fetch = (async (input) => {
436
+ observedUrl = String(input);
437
+ return Response.json({ ok: true });
438
+ }) as typeof fetch;
439
+
440
+ const client = createApiRegistryClient([
441
+ {
442
+ slug: "petstore",
443
+ kind: "openapi",
444
+ baseUrl: "https://petstore.vendor.test/api/v3",
445
+ credential: null,
446
+ operations: [
447
+ {
448
+ name: "getInventory",
449
+ method: "GET",
450
+ path: "/store/inventory",
451
+ parameters: [],
452
+ hasBody: false,
453
+ successStatus: "200",
454
+ requestType: "PetstoreGetInventoryArgs",
455
+ responseType: "PetstoreGetInventoryResponse",
456
+ },
457
+ ],
458
+ },
459
+ ]);
460
+
461
+ await client.petstore.getInventory({});
462
+ expect(observedUrl).toBe("https://petstore.vendor.test/api/v3/store/inventory");
463
+ });
464
+
465
+ test("ctx.api OpenAPI client default path still parses 2xx bodies", async () => {
466
+ const client = createApiRegistryClient(
467
+ [
468
+ {
469
+ slug: "repos",
470
+ kind: "openapi",
471
+ baseUrl: "https://api.vendor.test",
472
+ credential: null,
473
+ operations: [
474
+ {
475
+ name: "getRepo",
476
+ method: "GET",
477
+ path: "/repos/{owner}/{repo}",
478
+ parameters: [
479
+ { name: "owner", in: "path", required: true, schema: {} },
480
+ { name: "repo", in: "path", required: true, schema: {} },
481
+ ],
482
+ hasBody: false,
483
+ successStatus: "200",
484
+ responseSchema: {},
485
+ requestType: "ReposGetRepoArgs",
486
+ responseType: "ReposGetRepoResponse",
487
+ },
488
+ ],
489
+ },
490
+ ],
491
+ {
492
+ fetch: (async () =>
493
+ Response.json({ full_name: "desplega-ai/agent-swarm" })) as typeof fetch,
494
+ },
495
+ );
496
+
497
+ await expect(
498
+ client.repos.getRepo({ path: { owner: "desplega-ai", repo: "agent-swarm" } }),
499
+ ).resolves.toEqual({ full_name: "desplega-ai/agent-swarm" });
500
+ });
501
+
502
+ test("ctx.api OpenAPI client enriches default non-2xx errors", async () => {
503
+ const client = createApiRegistryClient(
504
+ [
505
+ {
506
+ slug: "repos",
507
+ kind: "openapi",
508
+ baseUrl: "https://api.vendor.test",
509
+ credential: null,
510
+ operations: [
511
+ {
512
+ name: "getRepo",
513
+ method: "GET",
514
+ path: "/repos/{owner}/{repo}",
515
+ parameters: [
516
+ { name: "owner", in: "path", required: true, schema: {} },
517
+ { name: "repo", in: "path", required: true, schema: {} },
518
+ ],
519
+ hasBody: false,
520
+ successStatus: "200",
521
+ responseSchema: {},
522
+ requestType: "ReposGetRepoArgs",
523
+ responseType: "ReposGetRepoResponse",
524
+ },
525
+ ],
526
+ },
527
+ ],
528
+ {
529
+ fetch: (async () =>
530
+ Response.json(
531
+ { error: "missing" },
532
+ { status: 404, statusText: "Not Found" },
533
+ )) as typeof fetch,
534
+ },
535
+ );
536
+
537
+ try {
538
+ await client.repos.getRepo({ path: { owner: "desplega-ai", repo: "missing" } });
539
+ throw new Error("expected request to throw");
540
+ } catch (error) {
541
+ const err = error as Error & {
542
+ status?: number;
543
+ statusText?: string;
544
+ body?: unknown;
545
+ response?: Response;
546
+ };
547
+ expect(err.message).toBe("ctx.api.repos.getRepo failed with 404");
548
+ expect(err.status).toBe(404);
549
+ expect(err.statusText).toBe("Not Found");
550
+ expect(err.body).toEqual({ error: "missing" });
551
+ expect(err.response?.status).toBe(404);
552
+ expect(await err.response?.json()).toEqual({ error: "missing" });
553
+ }
554
+ });
555
+
556
+ test("ctx.api OpenAPI client raw mode returns binary 2xx responses without parsing", async () => {
557
+ let observedAuthorization: string | null = null;
558
+ const client = createApiRegistryClient(
559
+ [
560
+ {
561
+ slug: "studioApi",
562
+ kind: "openapi",
563
+ baseUrl: "https://studio.vendor.test",
564
+ credential: {
565
+ configKey: "STUDIO_API_KEY",
566
+ headerTemplate: "Authorization: Bearer [REDACTED:STUDIO_API_KEY]",
567
+ },
568
+ operations: [
569
+ {
570
+ name: "downloadRenderFile",
571
+ method: "GET",
572
+ path: "/renders/{id}/file",
573
+ parameters: [{ name: "id", in: "path", required: true, schema: {} }],
574
+ hasBody: false,
575
+ successStatus: "200",
576
+ responseSchema: {},
577
+ requestType: "StudioApiDownloadRenderFileArgs",
578
+ responseType: "StudioApiDownloadRenderFileResponse",
579
+ },
580
+ ],
581
+ },
582
+ ],
583
+ {
584
+ fetch: (async (_input, init) => {
585
+ observedAuthorization = new Headers(init?.headers).get("authorization");
586
+ return new Response(new Uint8Array([0, 1, 255]), {
587
+ status: 200,
588
+ headers: { "content-type": "application/octet-stream", "x-render-id": "render-123" },
589
+ });
590
+ }) as typeof fetch,
591
+ },
592
+ );
593
+
594
+ const result = (await client.studioApi.downloadRenderFile(
595
+ { path: { id: "render-123" } },
596
+ { raw: true },
597
+ )) as { ok: boolean; status: number; headers: Record<string, string>; response: Response };
598
+
599
+ expect(result.ok).toBe(true);
600
+ expect(result.status).toBe(200);
601
+ expect(result.headers).toBeTypeOf("object");
602
+ expect(Array.from(new Uint8Array(await result.response.arrayBuffer()))).toEqual([0, 1, 255]);
603
+ expect(observedAuthorization).toBe("Bearer [REDACTED:STUDIO_API_KEY]");
604
+ });
605
+
606
+ test("ctx.api OpenAPI client raw mode returns non-2xx responses without throwing", async () => {
607
+ const client = createApiRegistryClient(
608
+ [
609
+ {
610
+ slug: "repos",
611
+ kind: "openapi",
612
+ baseUrl: "https://api.vendor.test",
613
+ credential: null,
614
+ operations: [
615
+ {
616
+ name: "getRepo",
617
+ method: "GET",
618
+ path: "/repos/{owner}/{repo}",
619
+ parameters: [
620
+ { name: "owner", in: "path", required: true, schema: {} },
621
+ { name: "repo", in: "path", required: true, schema: {} },
622
+ ],
623
+ hasBody: false,
624
+ successStatus: "200",
625
+ responseSchema: {},
626
+ requestType: "ReposGetRepoArgs",
627
+ responseType: "ReposGetRepoResponse",
628
+ },
629
+ ],
630
+ },
631
+ ],
632
+ {
633
+ fetch: (async () =>
634
+ Response.json(
635
+ { error: "rate limited" },
636
+ { status: 429, statusText: "Too Many Requests" },
637
+ )) as typeof fetch,
638
+ },
639
+ );
640
+
641
+ const result = (await client.repos.getRepo(
642
+ { path: { owner: "desplega-ai", repo: "agent-swarm" } },
643
+ { raw: true },
644
+ )) as { ok: boolean; status: number; statusText: string; response: Response };
645
+
646
+ expect(result.ok).toBe(false);
647
+ expect(result.status).toBe(429);
648
+ expect(result.statusText).toBe("Too Many Requests");
649
+ expect(await result.response.json()).toEqual({ error: "rate limited" });
650
+ });
651
+
652
+ test("ctx.api GraphQL client throws on errors-only responses", async () => {
653
+ globalThis.fetch = (async () =>
654
+ Response.json({ errors: [{ message: "Cannot query field nope" }] })) as typeof fetch;
655
+
656
+ const client = createApiRegistryClient([
657
+ {
658
+ slug: "countries",
659
+ kind: "graphql",
660
+ baseUrl: "https://countries.vendor.test/graphql",
661
+ credential: null,
662
+ },
663
+ ]);
664
+
665
+ await expect(client.countries.graphql("query { nope }")).rejects.toThrow(
666
+ /Cannot query field nope/,
667
+ );
668
+ });
378
669
  });
@@ -9,6 +9,7 @@ import { typecheckScript } from "../be/scripts/typecheck";
9
9
  import { runSeeder } from "../be/seed";
10
10
  import { SEED_SCRIPTS, scriptsSeeder } from "../be/seed-scripts";
11
11
  import bootTriage from "../be/seed-scripts/catalog/boot-triage";
12
+ import { renderCatalogReportPage } from "../be/seed-scripts/catalog/catalog-report";
12
13
  import compoundInsights from "../be/seed-scripts/catalog/compound-insights";
13
14
  import opsCatalogAudit, {
14
15
  renderPage as renderOpsCatalogAuditPage,
@@ -710,6 +711,41 @@ describe("seed-scripts catalog", () => {
710
711
  expect(html).not.toContain("<ul>");
711
712
  });
712
713
 
714
+ test("catalog report renders complex checks as main-column data panels", () => {
715
+ const html = renderCatalogReportPage({
716
+ title: "Compound Insights Audit",
717
+ slug: "compound-insights",
718
+ description: "Daily ops snapshot.",
719
+ generatedAt: "2026-07-09T12:00:00.000Z",
720
+ lede: "Swarm-wide 3-day snapshot.",
721
+ metrics: [["Tasks", 12]],
722
+ sections: [
723
+ {
724
+ key: "script-usage",
725
+ goal: "Track script execution without dumping JSON into sidebars.",
726
+ findingCount: 0,
727
+ checks: {
728
+ total: 10,
729
+ perScript: [
730
+ { scriptName: "compound-insights", runs: 3, successRate: 100 },
731
+ { scriptName: "smart-recall", runs: 2, successRate: 100 },
732
+ ],
733
+ },
734
+ findings: [],
735
+ },
736
+ ],
737
+ appendix: { ok: true },
738
+ });
739
+
740
+ expect(html).toContain('class="report-nav"');
741
+ expect(html).toContain('class="data-panel"');
742
+ expect(html).toContain("<h3>Per Script</h3>");
743
+ expect(html).toContain("<th>Script Name</th>");
744
+ expect(html).toContain("<td>compound-insights</td>");
745
+ expect(html).toContain("<span>Total</span>");
746
+ expect(html).not.toContain("<strong>[{&quot;scriptName&quot;");
747
+ });
748
+
713
749
  test("boot-triage returns one read-only post-restart snapshot", async () => {
714
750
  const queries: Array<{ sql: string; params?: unknown[] }> = [];
715
751
  const result: any = await bootTriage(
@@ -19,9 +19,16 @@
19
19
 
20
20
  import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
21
21
  import { unlinkSync } from "node:fs";
22
- import { closeDb, getDb, getKv, initDb } from "../be/db";
23
- import { findUserByExternalId, getUserIdentities } from "../be/users";
24
- import { enrichSlackUserEmail, resolveSlackUserId } from "../slack/enrich";
22
+ import { closeDb, createUser, getDb, getKv, initDb } from "../be/db";
23
+ import {
24
+ findUserByExternalId,
25
+ getUserIdentities,
26
+ type IdentityActor,
27
+ linkIdentity,
28
+ } from "../be/users";
29
+ import { enrichSlackUserEmail, resolveSlackUserId, rewriteSlackMentions } from "../slack/enrich";
30
+
31
+ const SYSTEM_ACTOR: IdentityActor = { kind: "system", id: "test" };
25
32
 
26
33
  const TEST_DB_PATH = "./test-slack-identity-resolution.sqlite";
27
34
 
@@ -346,3 +353,30 @@ describe("findUserByExternalId — sanity check after cascade", () => {
346
353
  expect(looked!.id).toBe(id);
347
354
  });
348
355
  });
356
+
357
+ describe("rewriteSlackMentions — pure DB, zero Slack API calls", () => {
358
+ test("resolved mention renders '<@id|Name>' — the canonical pair", () => {
359
+ const user = createUser({ name: "Manuel", email: "manuel-rw@example.com" });
360
+ linkIdentity(user.id, "slack", "U3000RESOLVED", SYSTEM_ACTOR);
361
+
362
+ const rewritten = rewriteSlackMentions("hey <@U3000RESOLVED> can you look at this");
363
+ expect(rewritten).toBe("hey <@U3000RESOLVED|Manuel> can you look at this");
364
+ });
365
+
366
+ test("unresolved mention renders '<@id> (unknown user)' — never a guessed name", () => {
367
+ const rewritten = rewriteSlackMentions("hey <@U4000UNKNOWN> can you look at this");
368
+ expect(rewritten).toBe("hey <@U4000UNKNOWN> (unknown user) can you look at this");
369
+ });
370
+
371
+ test("multiple mentions in one string are each rewritten independently", () => {
372
+ const user = createUser({ name: "Tainá", email: "taina-rw@example.com" });
373
+ linkIdentity(user.id, "slack", "U5000RESOLVED", SYSTEM_ACTOR);
374
+
375
+ const rewritten = rewriteSlackMentions("<@U5000RESOLVED> and <@U6000UNKNOWN> both here");
376
+ expect(rewritten).toBe("<@U5000RESOLVED|Tainá> and <@U6000UNKNOWN> (unknown user) both here");
377
+ });
378
+
379
+ test("text with no mentions passes through unchanged", () => {
380
+ expect(rewriteSlackMentions("no mentions here")).toBe("no mentions here");
381
+ });
382
+ });
@@ -0,0 +1,65 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ formatInlineCompletionOutputText,
4
+ MAX_INLINE_OUTPUT_MESSAGE_LENGTH,
5
+ shouldPostInlineCompletionOutput,
6
+ } from "../slack/responses";
7
+ import type { AgentTask, TaskAttachment } from "../types";
8
+
9
+ const TASK_ID = "abcdef12-3456-7890-abcd-ef1234567890";
10
+
11
+ function task(overrides: Partial<AgentTask> = {}): AgentTask {
12
+ return {
13
+ id: TASK_ID,
14
+ task: "Answer the question",
15
+ status: "completed",
16
+ output: "Here is the answer with enough substance to post inline.",
17
+ slackChannelId: "C123",
18
+ slackThreadTs: "1700000000.000001",
19
+ slackReplySent: false,
20
+ ...overrides,
21
+ } as AgentTask;
22
+ }
23
+
24
+ function attachment(overrides: Partial<TaskAttachment> = {}): TaskAttachment {
25
+ return {
26
+ id: crypto.randomUUID(),
27
+ taskId: TASK_ID,
28
+ agentId: null,
29
+ name: "report",
30
+ kind: "url",
31
+ url: "https://example.com/report",
32
+ isPrimary: false,
33
+ createdAt: new Date().toISOString(),
34
+ ...overrides,
35
+ } as TaskAttachment;
36
+ }
37
+
38
+ describe("Slack inline completion output", () => {
39
+ test("posts only completed Slack tasks with substantive output and no primary artifact", () => {
40
+ expect(shouldPostInlineCompletionOutput(task(), [])).toBe(true);
41
+ expect(shouldPostInlineCompletionOutput(task(), [attachment()])).toBe(true);
42
+ expect(shouldPostInlineCompletionOutput(task(), [attachment({ isPrimary: true })])).toBe(false);
43
+ expect(shouldPostInlineCompletionOutput(task({ slackReplySent: true }), [])).toBe(false);
44
+ expect(shouldPostInlineCompletionOutput(task({ output: "done" }), [])).toBe(false);
45
+ expect(shouldPostInlineCompletionOutput(task({ slackChannelId: undefined }), [])).toBe(false);
46
+ expect(shouldPostInlineCompletionOutput(task({ status: "failed" }), [])).toBe(false);
47
+ });
48
+
49
+ test("formats markdown output and keeps the Slack fallback text under 4000 chars", () => {
50
+ const text = formatInlineCompletionOutputText({
51
+ agentName: "Analyst",
52
+ taskId: TASK_ID,
53
+ output: `### Summary\n\n**Answer:** ${"This is a detailed prose finding. ".repeat(220)}`,
54
+ });
55
+
56
+ expect(text.length).toBeLessThanOrEqual(MAX_INLINE_OUTPUT_MESSAGE_LENGTH);
57
+ expect(text).toContain("✅ *Analyst* completed with output");
58
+ expect(text).toContain("*Summary*");
59
+ expect(text).toContain("*Answer:*");
60
+ expect(text).not.toContain("###");
61
+ expect(text).not.toContain("**Answer:**");
62
+ expect(text).toContain("…(full output in task");
63
+ expect(text).toContain("|`abcdef12`>");
64
+ });
65
+ });
@@ -4,9 +4,11 @@ import {
4
4
  closeDb,
5
5
  createAgent,
6
6
  createTaskExtended,
7
+ createUser,
7
8
  getLatestActiveTaskInThread,
8
9
  initDb,
9
10
  } from "../be/db";
11
+ import { type IdentityActor, linkIdentity } from "../be/users";
10
12
  import {
11
13
  bufferThreadMessage,
12
14
  getBufferMessageCount,
@@ -14,6 +16,8 @@ import {
14
16
  isThreadBuffered,
15
17
  } from "../slack/thread-buffer";
16
18
 
19
+ const SYSTEM_ACTOR: IdentityActor = { kind: "system", id: "test" };
20
+
17
21
  const TEST_DB_PATH = "./test-slack-thread-buffer.sqlite";
18
22
 
19
23
  beforeAll(() => {
@@ -125,6 +129,30 @@ describe("Slack thread buffer", () => {
125
129
  expect(task!.slackChannelId).toBe(channelId);
126
130
  expect(task!.slackThreadTs).toBe(threadTs);
127
131
  });
132
+
133
+ test("in-body <@U…> mentions are rewritten via the identity primitive — resolved and unknown", async () => {
134
+ const channelId = "C601";
135
+ const threadTs = "6001.0001";
136
+ const linked = createUser({ name: "Luis", email: "luis-buf@example.com" });
137
+ // Slack user ids are uppercase-alphanumeric only (matches the
138
+ // `/<@([A-Z0-9]+)>/g` mention regex) — no underscores.
139
+ linkIdentity(linked.id, "slack", "U1000LINKED", SYSTEM_ACTOR);
140
+
141
+ bufferThreadMessage(
142
+ channelId,
143
+ threadTs,
144
+ "cc <@U1000LINKED> and <@U2000UNKNOWN>",
145
+ "U1",
146
+ "6001.0010",
147
+ );
148
+
149
+ await instantFlush(`${channelId}:${threadTs}`);
150
+
151
+ const task = getLatestActiveTaskInThread(channelId, threadTs);
152
+ expect(task).not.toBeNull();
153
+ expect(task!.task).toContain("<@U1000LINKED|Luis>");
154
+ expect(task!.task).toContain("<@U2000UNKNOWN> (unknown user)");
155
+ });
128
156
  });
129
157
 
130
158
  describe("flush sets dependsOn to latest active task in thread", () => {
@@ -106,6 +106,7 @@ describe("Tool Annotations & Classification", () => {
106
106
  "delete-schedule",
107
107
  "delete-config",
108
108
  "delete-workflow",
109
+ "delete-page",
109
110
  "unregister-service",
110
111
  "slack-delete",
111
112
  ];