@desplega.ai/agent-swarm 1.129.0 → 1.131.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 (226) hide show
  1. package/README.md +2 -2
  2. package/dist/{actions-kfv2dawe.js → actions-w53mbns9.js} +11 -11
  3. package/dist/{anthropic-messages-3xgme8my.js → anthropic-messages-rjmd2e7n.js} +5 -5
  4. package/dist/{app-q8vjk6e8.js → app-m9gwqsqv.js} +8 -8
  5. package/dist/{artifact-hg4t1nx7.js → artifact-jysj3wrg.js} +2 -2
  6. package/dist/{assistant-g037r345.js → assistant-bp65m6f3.js} +13 -13
  7. package/dist/{azure-openai-responses-xa4sh9f6.js → azure-openai-responses-c0yjmbfd.js} +8 -5
  8. package/dist/{boot-reembed-18pzsrxa.js → boot-reembed-62p0at2k.js} +6 -6
  9. package/dist/{boot-reembed-wpks2cj8.js → boot-reembed-xmfp6wdh.js} +7 -7
  10. package/dist/{boot-scrub-logs-n7szd57r.js → boot-scrub-logs-rr8q2dms.js} +5 -5
  11. package/dist/{claude-adapter-qz4r0ajt.js → claude-adapter-2y47mrkd.js} +9 -9
  12. package/dist/{cli-mhjnbgb6.js → cli-0rcyq6d9.js} +450 -186
  13. package/dist/{cli-ttkmehy0.js → cli-2q72qz2s.js} +274 -25
  14. package/dist/{cli-gztgc3hz.js → cli-2wadx7p2.js} +2 -2
  15. package/dist/cli-3fj60etf.js +153 -0
  16. package/dist/{cli-7ecgm5v0.js → cli-3kf51j15.js} +1 -1
  17. package/dist/{cli-vgexpfsr.js → cli-57khth04.js} +6 -3
  18. package/dist/{cli-2mbe7vk0.js → cli-5dsbcjjw.js} +1 -1
  19. package/dist/{cli-etr62zy6.js → cli-5kx36evn.js} +2 -2
  20. package/dist/{cli-v2tr6amf.js → cli-5n6e5yfn.js} +30 -8
  21. package/dist/{cli-hx02s9zq.js → cli-5v4rw6yd.js} +2 -2
  22. package/dist/{cli-v37yr6mf.js → cli-5ze9rv97.js} +4 -4
  23. package/dist/{cli-ch7t5v7n.js → cli-6map995v.js} +9 -4
  24. package/dist/{cli-h0457a67.js → cli-70xqj0z3.js} +3 -3
  25. package/dist/{cli-kk4f9r17.js → cli-72q8m8gk.js} +8 -6
  26. package/dist/{cli-7padk69a.js → cli-78xwvkhj.js} +2 -2
  27. package/dist/{cli-dxtxacy7.js → cli-9dr5qzc0.js} +45 -8
  28. package/dist/{cli-17zxdgsq.js → cli-a40rnf71.js} +6 -6
  29. package/dist/{cli-t53vnnxz.js → cli-a4jzdpa3.js} +2514 -1055
  30. package/dist/{cli-w10fkrf1.js → cli-akdvwc9v.js} +354 -115
  31. package/dist/{cli-1qkk5er0.js → cli-bmze986d.js} +1 -1
  32. package/dist/{cli-ga12dc1d.js → cli-bqw0mt1n.js} +3 -3
  33. package/dist/{cli-0sf92efp.js → cli-cdeg08wx.js} +1 -1
  34. package/dist/{cli-a8ynhkyj.js → cli-cvgjwnh1.js} +17 -15
  35. package/dist/{cli-9fxg3v5m.js → cli-d2tfkcga.js} +4 -4
  36. package/dist/{cli-nhcd7x43.js → cli-d3x3x1c9.js} +2 -2
  37. package/dist/{cli-6nh2qxvh.js → cli-darcwax3.js} +1 -1
  38. package/dist/{cli-yjbgmysx.js → cli-e7ckcg4v.js} +12 -12
  39. package/dist/{cli-4n2gcjpd.js → cli-efxfnq6q.js} +4 -3
  40. package/dist/{cli-z9xqqhfk.js → cli-er3a4bb0.js} +1 -1
  41. package/dist/{cli-xg2eezyv.js → cli-f1am9szv.js} +1 -1
  42. package/dist/{cli-xy4cwsm9.js → cli-fywms69x.js} +1 -1
  43. package/dist/{cli-33jj76y8.js → cli-g6b8x1h8.js} +1 -1
  44. package/dist/{cli-epsvdckx.js → cli-gm9cbhbq.js} +20 -10
  45. package/dist/{cli-rkndnn90.js → cli-k9qkf1hw.js} +6 -4
  46. package/dist/{cli-zejvt395.js → cli-kh3ba2zf.js} +1 -1
  47. package/dist/{cli-gb25p1h2.js → cli-nm5gtp1x.js} +2 -2
  48. package/dist/{cli-akxnpx17.js → cli-pm6xbh84.js} +1829 -1629
  49. package/dist/{cli-2k0ztbfp.js → cli-psh3c4va.js} +1 -1
  50. package/dist/{cli-39vgrsep.js → cli-qv61hp37.js} +2 -2
  51. package/dist/{cli-hv2fndhm.js → cli-r6kqqbj0.js} +6 -4
  52. package/dist/{cli-1y1cm570.js → cli-rzbpmxdr.js} +1 -1
  53. package/dist/{cli-mmrk00vp.js → cli-t1xze5qh.js} +1 -1
  54. package/dist/{cli-g252a9ky.js → cli-t55yw3a1.js} +2 -2
  55. package/dist/{cli-7bz8dzqa.js → cli-t76vw5db.js} +1184 -1522
  56. package/dist/{cli-4n4mdv21.js → cli-xpxde7rm.js} +7 -3
  57. package/dist/{cli-fpaa5k2q.js → cli-yw8vghx2.js} +54 -18
  58. package/dist/cli-zsspvvm2.js +490 -0
  59. package/dist/cli.js +27 -25
  60. package/dist/{codex-adapter-7v6t7ff6.js → codex-adapter-d24q75wk.js} +7 -7
  61. package/dist/{codex-hook-amndawyb.js → codex-hook-hq54t9ft.js} +2 -2
  62. package/dist/{codex-session-runner-e7j4khe2.js → codex-session-runner-g50vcsex.js} +7 -7
  63. package/dist/{commands-ex0k5tbf.js → commands-c3nrc5hg.js} +5 -5
  64. package/dist/{db-df7jatfc.js → db-nt7fg3tw.js} +5 -5
  65. package/dist/{google-generative-ai-y42hnw9p.js → google-generative-ai-hysrd8qx.js} +7 -5
  66. package/dist/{google-vertex-4sg66gb6.js → google-vertex-p5arrga0.js} +7 -5
  67. package/dist/{handlers-1j2cfqt2.js → handlers-fkr6swat.js} +13 -13
  68. package/dist/{hook-nx9a0mj3.js → hook-77wpvnjc.js} +7 -7
  69. package/dist/{hook-w6yaf6sy.js → hook-g019yehy.js} +8 -8
  70. package/dist/{http-4tz5h8wg.js → http-jewj0syk.js} +2993 -716
  71. package/dist/{index-0bpkzb09.js → index-070v4eyb.js} +3 -3
  72. package/dist/{index-0s67qv7h.js → index-6gm5he8h.js} +11 -11
  73. package/dist/{index-35gtv6gj.js → index-7n2xpbd4.js} +310 -40
  74. package/dist/{index-ker3gccb.js → index-9znhar3x.js} +4 -4
  75. package/dist/{index-5n2c8yay.js → index-hase6zbe.js} +2 -2
  76. package/dist/{index-kcf3tm9h.js → index-hfvggd0z.js} +12 -12
  77. package/dist/{index-7c0jqna0.js → index-hp1kdbjj.js} +13 -13
  78. package/dist/{keepalive-3j4a9168.js → keepalive-j0fjvqfd.js} +7 -7
  79. package/dist/{lead-6kpevymb.js → lead-1n8x30xe.js} +33 -33
  80. package/dist/{maintenance-m1k82ean.js → maintenance-gvev11sa.js} +7 -7
  81. package/dist/{mistral-conversations-bjkdfb6z.js → mistral-conversations-81fwav0j.js} +6 -6
  82. package/dist/{oauth-refresh-sweep-881k8e4q.js → oauth-refresh-sweep-s9ecsvh0.js} +6 -6
  83. package/dist/{onboard-c0ykffjn.js → onboard-pvm5b38w.js} +2 -2
  84. package/dist/{openai-codex-responses-xetseqvt.js → openai-codex-responses-5g27pd6f.js} +43 -23
  85. package/dist/{openai-completions-5r2g5axa.js → openai-completions-3vkmfz2a.js} +55 -11
  86. package/dist/{openai-responses-ce67hd23.js → openai-responses-dg4t7j7a.js} +8 -5
  87. package/dist/{openrouter-images-pzfp22jb.js → openrouter-images-g3gm8931.js} +1 -1
  88. package/dist/{otel-impl-sqagjerv.js → otel-impl-p6jvgd9c.js} +6 -4
  89. package/dist/{otel-3mp81ppd.js → otel-nqa12wf3.js} +2 -2
  90. package/dist/{pi-mono-adapter-k584k16g.js → pi-mono-adapter-t091s27g.js} +17117 -7517
  91. package/dist/{pricing-refresh-y82whbf9.js → pricing-refresh-k49rcyed.js} +7 -7
  92. package/dist/{rbac-roles-nvkqmzyd.js → rbac-roles-30pspvy7.js} +7 -7
  93. package/dist/{rbac-roles-4ht8gegc.js → rbac-roles-cr80g96t.js} +6 -6
  94. package/dist/{render-v2-wvqdgvvp.js → render-v2-xm457ng1.js} +8 -8
  95. package/dist/{seed-pricing-fy3rxhde.js → seed-pricing-yhpv35fr.js} +6 -6
  96. package/dist/{setup-ak5cnc6f.js → setup-nytnmwwr.js} +2 -2
  97. package/dist/{worker-d8769q8a.js → worker-msytr2cw.js} +33 -33
  98. package/openapi.json +34039 -11036
  99. package/package.json +8 -6
  100. package/src/apps/definition.ts +368 -26
  101. package/src/apps/row-store.ts +66 -10
  102. package/src/apps/schema-migrate.ts +95 -1
  103. package/src/apps/store.ts +13 -4
  104. package/src/apps/sync-run-as.ts +18 -0
  105. package/src/apps/sync.ts +934 -0
  106. package/src/apps/version.ts +16 -2
  107. package/src/assets/key.ts +8 -1
  108. package/src/be/asset-key-audit.ts +3 -1
  109. package/src/be/db-queries/mcp-oauth.ts +297 -24
  110. package/src/be/db.ts +42 -1
  111. package/src/be/migrations/129_asset_keys_apps_scripts.sql +161 -0
  112. package/src/be/scripts/db.ts +5 -2
  113. package/src/be/scripts/typecheck.ts +1 -0
  114. package/src/be/seed-scripts/catalog/app-sync-run.ts +32 -0
  115. package/src/be/seed-scripts/catalog/github-issues-pull.ts +102 -0
  116. package/src/be/seed-scripts/index.ts +18 -0
  117. package/src/be/seed-skills/index.ts +7 -0
  118. package/src/http/active-sessions.ts +26 -18
  119. package/src/http/agents.ts +94 -27
  120. package/src/http/api-keys.ts +91 -17
  121. package/src/http/approval-requests.ts +87 -10
  122. package/src/http/apps.ts +361 -53
  123. package/src/http/assets.ts +165 -31
  124. package/src/http/budgets.ts +7 -5
  125. package/src/http/codex-oauth-keep-warm.ts +21 -3
  126. package/src/http/config.ts +46 -16
  127. package/src/http/context.ts +33 -6
  128. package/src/http/ecosystem.ts +20 -6
  129. package/src/http/events.ts +24 -9
  130. package/src/http/favorites.ts +20 -6
  131. package/src/http/fs.ts +60 -17
  132. package/src/http/heartbeat.ts +18 -4
  133. package/src/http/inbox-state.ts +12 -6
  134. package/src/http/integrations.ts +25 -5
  135. package/src/http/kv.ts +36 -15
  136. package/src/http/mcp-bridge.ts +5 -1
  137. package/src/http/mcp-oauth.ts +332 -17
  138. package/src/http/mcp-servers.ts +56 -18
  139. package/src/http/memory.ts +282 -29
  140. package/src/http/metrics.ts +80 -17
  141. package/src/http/oauth-callback.ts +10 -4
  142. package/src/http/oauth-generic.ts +5 -1
  143. package/src/http/oauth-locks.ts +6 -3
  144. package/src/http/openapi.ts +57 -9
  145. package/src/http/page-proxy.ts +25 -5
  146. package/src/http/pages-public.ts +37 -20
  147. package/src/http/pages.ts +93 -28
  148. package/src/http/poll.ts +89 -5
  149. package/src/http/pricing.ts +9 -6
  150. package/src/http/prompt-templates.ts +85 -21
  151. package/src/http/route-def.ts +67 -4
  152. package/src/http/schedules.ts +63 -14
  153. package/src/http/script-connection-proxy.ts +30 -4
  154. package/src/http/script-connections.ts +393 -43
  155. package/src/http/script-runs.ts +73 -19
  156. package/src/http/scripts.ts +257 -39
  157. package/src/http/session-data.ts +87 -14
  158. package/src/http/sessions.ts +80 -5
  159. package/src/http/skills.ts +115 -35
  160. package/src/http/stats.ts +94 -13
  161. package/src/http/task-templates.ts +6 -4
  162. package/src/http/tasks.ts +230 -61
  163. package/src/http/trackers/jira.ts +90 -30
  164. package/src/http/trackers/linear.ts +46 -12
  165. package/src/http/users.ts +162 -25
  166. package/src/http/utils.ts +12 -2
  167. package/src/http/webhooks.ts +30 -16
  168. package/src/http/workflow-events.ts +18 -5
  169. package/src/http/workflows.ts +128 -39
  170. package/src/http/x.ts +37 -6
  171. package/src/oauth/ensure-mcp-token.ts +16 -1
  172. package/src/oauth/mcp-wrapper.ts +281 -26
  173. package/src/providers/pi-mono-adapter.ts +1 -1
  174. package/src/scripts-runtime/credential-broker/README.md +1 -1
  175. package/src/scripts-runtime/credential-broker/fetch-patch.ts +12 -2
  176. package/src/scripts-runtime/sdk-allowlist.ts +1 -0
  177. package/src/scripts-runtime/stdlib/fetch.ts +6 -0
  178. package/src/scripts-runtime/types/stdlib.d.ts +1 -0
  179. package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
  180. package/src/server.ts +2 -0
  181. package/src/tests/apps-rbac.test.ts +110 -0
  182. package/src/tests/apps-spike2.test.ts +4 -1
  183. package/src/tests/apps-sync-engine.test.ts +2540 -0
  184. package/src/tests/apps-sync.test.ts +1627 -0
  185. package/src/tests/asset-key-api.test.ts +48 -2
  186. package/src/tests/asset-key-invariants.test.ts +92 -0
  187. package/src/tests/asset-key-migration.test.ts +174 -8
  188. package/src/tests/asset-key-normalization.test.ts +2 -0
  189. package/src/tests/credential-broker.test.ts +27 -2
  190. package/src/tests/mcp-oauth-dcr-reuse.test.ts +743 -0
  191. package/src/tests/mcp-oauth-ensure-token.test.ts +45 -0
  192. package/src/tests/mcp-oauth-manual-client.test.ts +135 -2
  193. package/src/tests/mcp-oauth-queries.test.ts +153 -0
  194. package/src/tests/mcp-oauth-wrapper.test.ts +432 -0
  195. package/src/tests/memory-http-recall-gating.test.ts +2 -0
  196. package/src/tests/openapi-response-contract.test.ts +138 -0
  197. package/src/tests/rbac-admission-e2e.test.ts +68 -0
  198. package/src/tests/script-connections.test.ts +3 -0
  199. package/src/tests/scripts-runtime-fetch.test.ts +121 -0
  200. package/src/tests/scripts-runtime-secret-egress.test.ts +4 -2
  201. package/src/tests/seed-scripts.test.ts +2 -2
  202. package/src/tests/swarm-tool-result-gate.test.ts +32 -0
  203. package/src/tests/task-working-dir.test.ts +8 -4
  204. package/src/tests/whoami-route.test.ts +127 -0
  205. package/src/tests/workflow-engine-v2.test.ts +80 -7
  206. package/src/tests/workflow-swarm-script.test.ts +104 -0
  207. package/src/tools/app-get.ts +10 -2
  208. package/src/tools/app-rollback.ts +1 -0
  209. package/src/tools/app-sync.ts +112 -0
  210. package/src/tools/script-common.ts +20 -0
  211. package/src/tools/tool-config.ts +2 -1
  212. package/src/tools/utils.ts +11 -0
  213. package/src/tools/workflows/create-workflow.ts +4 -1
  214. package/src/types.ts +1394 -1206
  215. package/src/utils/template.ts +8 -1
  216. package/src/utils/zod-openapi.ts +11 -0
  217. package/src/workflows/engine.ts +27 -4
  218. package/src/workflows/executors/swarm-script.ts +28 -0
  219. package/src/workflows/retry-poller.ts +19 -2
  220. package/templates/skills/apps/content.md +94 -5
  221. package/templates/skills/asset-namespaces/content.md +2 -2
  222. package/templates/skills/scheduled-task-resilience/SKILL.md +102 -0
  223. package/templates/skills/scheduled-task-resilience/config.json +14 -0
  224. package/templates/skills/scheduled-task-resilience/content.md +97 -0
  225. package/dist/cli-7wmybrt5.js +0 -115
  226. package/dist/cli-d91pvmq1.js +0 -14
@@ -17,14 +17,16 @@ import {
17
17
  type Metric,
18
18
  MetricDefinitionSchema,
19
19
  type MetricParam,
20
+ MetricSchema,
20
21
  type MetricSummary,
21
22
  type MetricVariable,
22
23
  MetricVersionSchema,
23
24
  type MetricWidget,
25
+ MetricWidgetSchema,
24
26
  } from "../types";
25
27
  import { assertSelectOnlyQuery, executeReadOnlyQuery } from "./db-query";
26
28
  import { route } from "./route-def";
27
- import { json, jsonError } from "./utils";
29
+ import { jsonError } from "./utils";
28
30
 
29
31
  const DEFAULT_METRIC_MAX_ROWS = 100;
30
32
  const HARD_METRIC_MAX_ROWS = 500;
@@ -192,6 +194,18 @@ const metricDefinitionBody = z.object({
192
194
  definition: MetricDefinitionSchema,
193
195
  });
194
196
 
197
+ // `{ id, version }` shape sent by both create (always version 1) and update
198
+ // (the next edit counter) — see `metricEditCounter` below.
199
+ const metricIdVersionSchema = z.object({
200
+ id: z.string(),
201
+ version: z.number().int().min(1),
202
+ });
203
+
204
+ // `MetricSummary` (see ../types) is `Omit<Metric, "definition">` — the "slim"
205
+ // list projection returned by `listMetricsByAgent`/`listAllMetrics` when
206
+ // `fields=slim`.
207
+ const metricSummarySchema = MetricSchema.omit({ definition: true });
208
+
195
209
  const createMetricRoute = route({
196
210
  method: "post",
197
211
  path: "/api/metrics/definitions",
@@ -200,7 +214,7 @@ const createMetricRoute = route({
200
214
  tags: ["Metrics"],
201
215
  body: metricDefinitionBody,
202
216
  responses: {
203
- 201: { description: "Metric created" },
217
+ 201: { description: "Metric created", schema: metricIdVersionSchema },
204
218
  400: { description: "Invalid metric definition" },
205
219
  409: { description: "Slug already exists for this agent" },
206
220
  },
@@ -219,7 +233,15 @@ const listMetricsRoute = route({
219
233
  fields: z.enum(["full", "slim"]).optional(),
220
234
  }),
221
235
  responses: {
222
- 200: { description: "Metric definitions" },
236
+ 200: {
237
+ description: "Metric definitions",
238
+ schema: z.object({
239
+ metrics: z.array(z.union([MetricSchema, metricSummarySchema])),
240
+ total: z.number(),
241
+ limit: z.number(),
242
+ offset: z.number(),
243
+ }),
244
+ },
223
245
  },
224
246
  });
225
247
 
@@ -231,7 +253,7 @@ const getMetricRoute = route({
231
253
  tags: ["Metrics"],
232
254
  params: z.object({ id: z.string() }),
233
255
  responses: {
234
- 200: { description: "Metric definition" },
256
+ 200: { description: "Metric definition", schema: MetricSchema },
235
257
  404: { description: "Metric not found" },
236
258
  },
237
259
  });
@@ -245,7 +267,7 @@ const updateMetricRoute = route({
245
267
  params: z.object({ id: z.string() }),
246
268
  body: metricDefinitionBody.partial(),
247
269
  responses: {
248
- 200: { description: "Metric updated" },
270
+ 200: { description: "Metric updated", schema: metricIdVersionSchema },
249
271
  404: { description: "Metric not found" },
250
272
  },
251
273
  });
@@ -263,6 +285,32 @@ const deleteMetricRoute = route({
263
285
  },
264
286
  });
265
287
 
288
+ // Wire shape of a single widget's query result inside a `run` response — see
289
+ // `runMetricWidget` below. `rows` is re-keyed from column-array form to
290
+ // column-name-keyed objects, so each row's field set is query-dependent.
291
+ const metricRunWidgetResultSchema = z.object({
292
+ columns: z.array(z.string()),
293
+ rows: z.array(z.record(z.string(), z.unknown())),
294
+ elapsed: z.number(),
295
+ total: z.number(),
296
+ truncated: z.boolean(),
297
+ maxRows: z.number(),
298
+ });
299
+
300
+ // Full `runMetric()` response — see that function below.
301
+ const metricRunResultSchema = z.object({
302
+ metric: MetricSchema,
303
+ variables: z.record(z.string(), z.union([z.string(), z.number(), z.boolean(), z.null()])),
304
+ widgets: z.array(
305
+ z.object({
306
+ widget: MetricWidgetSchema,
307
+ result: metricRunWidgetResultSchema,
308
+ }),
309
+ ),
310
+ // Kept as the first widget result for older callers during the PR cycle.
311
+ result: metricRunWidgetResultSchema.optional(),
312
+ });
313
+
266
314
  const runMetricRoute = route({
267
315
  method: "post",
268
316
  path: "/api/metrics/definitions/{id}/run",
@@ -272,7 +320,7 @@ const runMetricRoute = route({
272
320
  params: z.object({ id: z.string() }),
273
321
  body: MetricRunBodySchema,
274
322
  responses: {
275
- 200: { description: "Metric result" },
323
+ 200: { description: "Metric result", schema: metricRunResultSchema },
276
324
  400: { description: "Invalid or disallowed query" },
277
325
  404: { description: "Metric not found" },
278
326
  },
@@ -286,7 +334,10 @@ const listMetricVersionsRoute = route({
286
334
  tags: ["Metrics"],
287
335
  params: z.object({ id: z.string() }),
288
336
  responses: {
289
- 200: { description: "Metric version list" },
337
+ 200: {
338
+ description: "Metric version list",
339
+ schema: z.object({ versions: z.array(MetricVersionSchema) }),
340
+ },
290
341
  404: { description: "Metric not found" },
291
342
  },
292
343
  });
@@ -299,11 +350,18 @@ const getMetricVersionRoute = route({
299
350
  tags: ["Metrics"],
300
351
  params: z.object({ id: z.string(), version: z.coerce.number().int().min(1) }),
301
352
  responses: {
302
- 200: { description: "Metric version" },
353
+ 200: { description: "Metric version", schema: MetricVersionSchema },
303
354
  404: { description: "Metric or version not found" },
304
355
  },
305
356
  });
306
357
 
358
+ // z.toJSONSchema() output is always a JSON object at the root (draft-7 for an
359
+ // object-typed zod schema); nested shape is JSON-Schema-generic, not modeled
360
+ // field-by-field here.
361
+ const metricJsonSchemaResponseSchema = z.object({
362
+ schema: z.record(z.string(), z.unknown()),
363
+ });
364
+
307
365
  const metricSchemaRoute = route({
308
366
  method: "get",
309
367
  path: "/api/metrics/schema",
@@ -311,7 +369,7 @@ const metricSchemaRoute = route({
311
369
  summary: "Get the metric definition JSON Schema",
312
370
  tags: ["Metrics"],
313
371
  responses: {
314
- 200: { description: "Metric definition JSON Schema" },
372
+ 200: { description: "Metric definition JSON Schema", schema: metricJsonSchemaResponseSchema },
315
373
  },
316
374
  });
317
375
 
@@ -328,7 +386,9 @@ export async function handleMetrics(
328
386
  myAgentId: string | undefined,
329
387
  ): Promise<boolean> {
330
388
  if (metricSchemaRoute.match(req.method, pathSegments)) {
331
- json(res, { schema: z.toJSONSchema(MetricDefinitionSchema, { target: "draft-7" }) });
389
+ metricSchemaRoute.respond(res, 200, {
390
+ schema: z.toJSONSchema(MetricDefinitionSchema, { target: "draft-7" }),
391
+ });
332
392
  return true;
333
393
  }
334
394
 
@@ -347,7 +407,7 @@ export async function handleMetrics(
347
407
  description: parsed.body.description ?? undefined,
348
408
  definition,
349
409
  });
350
- json(res, { id: metric.id, version: 1 }, 201);
410
+ createMetricRoute.respond(res, 201, { id: metric.id, version: 1 });
351
411
  } catch (err) {
352
412
  const msg = err instanceof Error ? err.message : String(err);
353
413
  if (msg.includes("UNIQUE")) {
@@ -379,7 +439,7 @@ export async function handleMetrics(
379
439
  : listAllMetrics(limit, offset, { slim: true });
380
440
  total = countAllMetrics();
381
441
  }
382
- json(res, { metrics, total, limit, offset });
442
+ listMetricsRoute.respond(res, 200, { metrics, total, limit, offset });
383
443
  return true;
384
444
  }
385
445
 
@@ -393,7 +453,7 @@ export async function handleMetrics(
393
453
  return true;
394
454
  }
395
455
  try {
396
- json(res, runMetric(metric, parsed.body?.variables ?? {}));
456
+ runMetricRoute.respond(res, 200, runMetric(metric, parsed.body?.variables ?? {}));
397
457
  } catch (err) {
398
458
  jsonError(res, err instanceof Error ? err.message : String(err));
399
459
  }
@@ -414,7 +474,7 @@ export async function handleMetrics(
414
474
  res.end();
415
475
  return true;
416
476
  }
417
- json(res, MetricVersionSchema.parse(version));
477
+ getMetricVersionRoute.respond(res, 200, MetricVersionSchema.parse(version));
418
478
  return true;
419
479
  }
420
480
 
@@ -426,7 +486,7 @@ export async function handleMetrics(
426
486
  res.end();
427
487
  return true;
428
488
  }
429
- json(res, { versions: getMetricVersions(parsed.params.id) });
489
+ listMetricVersionsRoute.respond(res, 200, { versions: getMetricVersions(parsed.params.id) });
430
490
  return true;
431
491
  }
432
492
 
@@ -439,7 +499,7 @@ export async function handleMetrics(
439
499
  res.end();
440
500
  return true;
441
501
  }
442
- json(res, metric);
502
+ getMetricRoute.respond(res, 200, metric);
443
503
  return true;
444
504
  }
445
505
 
@@ -472,7 +532,10 @@ export async function handleMetrics(
472
532
  res.end();
473
533
  return true;
474
534
  }
475
- json(res, { id: updated.id, version: metricEditCounter(updated.id) });
535
+ updateMetricRoute.respond(res, 200, {
536
+ id: updated.id,
537
+ version: metricEditCounter(updated.id),
538
+ });
476
539
  } catch (err) {
477
540
  jsonError(res, err instanceof Error ? err.message : String(err));
478
541
  }
@@ -17,7 +17,7 @@ import { getPublicMcpBaseUrl } from "@/utils/constants";
17
17
  import { registerVolatileSecret, scrubSecrets } from "@/utils/secret-scrubber";
18
18
  import { completeMcpOAuthCallback } from "./mcp-oauth";
19
19
  import { route } from "./route-def";
20
- import { json, jsonError } from "./utils";
20
+ import { jsonError } from "./utils";
21
21
 
22
22
  // ─── The single static callback + redirect-uri display ───────────────────────
23
23
 
@@ -41,7 +41,10 @@ const callbackRoute = route({
41
41
  error_description: z.string().optional(),
42
42
  }),
43
43
  responses: {
44
- 200: { description: "OAuth authorization completed" },
44
+ 200: {
45
+ description: "OAuth authorization completed",
46
+ unstructured: "HTML success page (sendAuthorizedHtml) — not JSON",
47
+ },
45
48
  302: { description: "Redirect back to the final destination" },
46
49
  400: { description: "Missing or invalid OAuth callback parameters" },
47
50
  404: { description: "OAuth app not configured" },
@@ -57,7 +60,10 @@ const redirectUriRoute = route({
57
60
  summary: "The static OAuth callback URL to register with providers (pre-creation display)",
58
61
  tags: ["OAuth"],
59
62
  responses: {
60
- 200: { description: "{ redirectUri: string }" },
63
+ 200: {
64
+ description: "{ redirectUri: string }",
65
+ schema: z.object({ redirectUri: z.string() }),
66
+ },
61
67
  },
62
68
  });
63
69
 
@@ -280,7 +286,7 @@ export async function handleOAuthCallback(
280
286
  queryParams: URLSearchParams,
281
287
  ): Promise<boolean> {
282
288
  if (redirectUriRoute.match(req.method, pathSegments)) {
283
- json(res, { redirectUri: staticOAuthCallbackUri() });
289
+ redirectUriRoute.respond(res, 200, { redirectUri: staticOAuthCallbackUri() });
284
290
  return true;
285
291
  }
286
292
 
@@ -34,7 +34,11 @@ const genericOAuthCallbackRoute = route({
34
34
  error_description: z.string().optional(),
35
35
  }),
36
36
  responses: {
37
- 200: { description: "OAuth authorization completed" },
37
+ 200: {
38
+ description: "OAuth authorization completed",
39
+ unstructured:
40
+ "delegates to completeGenericOAuthCallback, which sends an HTML success page or a 302 redirect to the caller-supplied finalRedirect — never JSON",
41
+ },
38
42
  400: { description: "Missing or invalid OAuth callback parameters" },
39
43
  404: { description: "OAuth app not configured" },
40
44
  502: { description: "Token exchange failed" },
@@ -2,7 +2,7 @@ import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import { z } from "zod";
3
3
  import { acquireOAuthRefreshLock, releaseOAuthRefreshLock } from "../be/db-queries/oauth";
4
4
  import { route } from "./route-def";
5
- import { json, jsonError } from "./utils";
5
+ import { jsonError } from "./utils";
6
6
 
7
7
  /**
8
8
  * HTTP surface for the cross-process OAuth refresh-token lock
@@ -39,7 +39,10 @@ const acquireLockRoute = route({
39
39
  params: LockKeySchema,
40
40
  body: acquireBodySchema,
41
41
  responses: {
42
- 200: { description: "Lock acquired; returns the owner token" },
42
+ 200: {
43
+ description: "Lock acquired; returns the owner token",
44
+ schema: z.object({ owner: z.string() }),
45
+ },
43
46
  409: { description: "Lock is currently held by another caller" },
44
47
  400: { description: "Validation error" },
45
48
  },
@@ -73,7 +76,7 @@ export async function handleOAuthLocks(
73
76
  jsonError(res, "lock is held by another caller", 409);
74
77
  return true;
75
78
  }
76
- json(res, { owner });
79
+ acquireLockRoute.respond(res, 200, { owner });
77
80
  return true;
78
81
  }
79
82
 
@@ -1,13 +1,17 @@
1
- import {
2
- extendZodWithOpenApi,
3
- OpenAPIRegistry,
4
- OpenApiGeneratorV31,
5
- } from "@asteasolutions/zod-to-openapi";
6
- import { z } from "zod";
1
+ import { OpenAPIRegistry, OpenApiGeneratorV31 } from "@asteasolutions/zod-to-openapi";
7
2
  import { getPublicMcpBaseUrl } from "../utils/constants";
3
+ import { z } from "../utils/zod-openapi";
8
4
  import { routeRegistry } from "./route-def";
9
5
 
10
- extendZodWithOpenApi(z);
6
+ /**
7
+ * Default component for 4xx/5xx responses that declare no schema of their own:
8
+ * the `jsonError()` envelope every error path emits. Loose because a few
9
+ * handlers attach extra diagnostic fields (e.g. TriggerSchemaError's
10
+ * `message`/`details`) — those may declare an explicit schema instead.
11
+ */
12
+ const ErrorResponseSchema = z
13
+ .looseObject({ error: z.string() })
14
+ .openapi("ErrorResponse", { description: "Standard error envelope" });
11
15
 
12
16
  let cachedSpec: string | null = null;
13
17
 
@@ -43,11 +47,16 @@ export function generateOpenApiSpec(opts: OpenApiOptions): string {
43
47
  { description: string; content?: Record<string, { schema: z.ZodType }> }
44
48
  > = {};
45
49
  for (const [code, resDef] of Object.entries(routeDef.responses)) {
50
+ const schema =
51
+ resDef.schema ??
52
+ // Untyped error responses default to the shared jsonError envelope;
53
+ // `unstructured` (non-JSON body) opts out.
54
+ (Number(code) >= 400 && !resDef.unstructured ? ErrorResponseSchema : undefined);
46
55
  responses[code] = {
47
56
  description: resDef.description,
48
- ...(resDef.schema && {
57
+ ...(schema && {
49
58
  content: {
50
- "application/json": { schema: resDef.schema },
59
+ "application/json": { schema },
51
60
  },
52
61
  }),
53
62
  };
@@ -77,6 +86,42 @@ export function generateOpenApiSpec(opts: OpenApiOptions): string {
77
86
 
78
87
  const serverUrl = opts.serverUrl || getPublicMcpBaseUrl();
79
88
 
89
+ /**
90
+ * zod-to-openapi emits `.nullable()` on an `.extend()`ed (or `$ref`ed)
91
+ * schema as `allOf: [{$ref}, {type: ["object","null"], ...delta}]` — an
92
+ * allOf member allowing null is semantically wrong (the intersection with
93
+ * the non-null base can never be null), and TS-definition generators
94
+ * (fumadocs-openapi) collapse it to `never`. Rewrite to the intended
95
+ * `anyOf: [<intersection>, {type: "null"}]`.
96
+ */
97
+ function fixNullableAllOf(node: unknown): void {
98
+ if (!node || typeof node !== "object") return;
99
+ if (Array.isArray(node)) {
100
+ for (const item of node) fixNullableAllOf(item);
101
+ return;
102
+ }
103
+ const obj = node as Record<string, unknown>;
104
+ const allOf = obj.allOf;
105
+ if (Array.isArray(allOf)) {
106
+ let hadNull = false;
107
+ for (const member of allOf) {
108
+ if (member && Array.isArray(member.type) && member.type.includes("null")) {
109
+ hadNull = true;
110
+ const rest = member.type.filter((t: string) => t !== "null");
111
+ member.type = rest.length === 1 ? rest[0] : rest;
112
+ }
113
+ }
114
+ if (hadNull) {
115
+ // Drop members reduced to a bare `{type: "object"}` (no constraints left).
116
+ const members = allOf.filter((m) => !(Object.keys(m).length === 1 && m.type === "object"));
117
+ const intersection = members.length === 1 ? members[0] : { allOf: members };
118
+ delete obj.allOf;
119
+ obj.anyOf = [intersection, { type: "null" }];
120
+ }
121
+ }
122
+ for (const value of Object.values(obj)) fixNullableAllOf(value);
123
+ }
124
+
80
125
  const generator = new OpenApiGeneratorV31(registry.definitions);
81
126
  const doc = generator.generateDocument({
82
127
  openapi: "3.1.0",
@@ -96,6 +141,9 @@ export function generateOpenApiSpec(opts: OpenApiOptions): string {
96
141
  ],
97
142
  });
98
143
 
144
+ fixNullableAllOf(doc.paths);
145
+ fixNullableAllOf(doc.components);
146
+
99
147
  cachedSpec = JSON.stringify(doc, null, 2);
100
148
  return cachedSpec;
101
149
  }
@@ -40,7 +40,11 @@ route({
40
40
  summary: "Cookie-gated proxy to the swarm API (used by db-backed page iframes)",
41
41
  tags: ["Pages"],
42
42
  responses: {
43
- 200: { description: "Proxied response from the underlying /api/* endpoint" },
43
+ 200: {
44
+ description: "Proxied response from the underlying /api/* endpoint",
45
+ unstructured:
46
+ "Body is forwarded verbatim from the proxied /api/* endpoint; shape and content-type vary per target route",
47
+ },
44
48
  401: { description: "No or invalid page-session cookie" },
45
49
  404: { description: "Page referenced by the cookie no longer exists" },
46
50
  },
@@ -54,7 +58,11 @@ route({
54
58
  summary: "Cookie-gated proxy to the swarm API (POST)",
55
59
  tags: ["Pages"],
56
60
  responses: {
57
- 200: { description: "Proxied response" },
61
+ 200: {
62
+ description: "Proxied response",
63
+ unstructured:
64
+ "Body is forwarded verbatim from the proxied /api/* endpoint; shape and content-type vary per target route",
65
+ },
58
66
  401: { description: "No or invalid page-session cookie" },
59
67
  },
60
68
  auth: { apiKey: false },
@@ -67,7 +75,11 @@ route({
67
75
  summary: "Cookie-gated proxy to the swarm API (PUT)",
68
76
  tags: ["Pages"],
69
77
  responses: {
70
- 200: { description: "Proxied response" },
78
+ 200: {
79
+ description: "Proxied response",
80
+ unstructured:
81
+ "Body is forwarded verbatim from the proxied /api/* endpoint; shape and content-type vary per target route",
82
+ },
71
83
  401: { description: "No or invalid page-session cookie" },
72
84
  },
73
85
  auth: { apiKey: false },
@@ -80,7 +92,11 @@ route({
80
92
  summary: "Cookie-gated proxy to the swarm API (DELETE)",
81
93
  tags: ["Pages"],
82
94
  responses: {
83
- 200: { description: "Proxied response" },
95
+ 200: {
96
+ description: "Proxied response",
97
+ unstructured:
98
+ "Body is forwarded verbatim from the proxied /api/* endpoint; shape and content-type vary per target route",
99
+ },
84
100
  401: { description: "No or invalid page-session cookie" },
85
101
  },
86
102
  auth: { apiKey: false },
@@ -93,7 +109,11 @@ route({
93
109
  summary: "Cookie-gated proxy to the swarm API (PATCH)",
94
110
  tags: ["Pages"],
95
111
  responses: {
96
- 200: { description: "Proxied response" },
112
+ 200: {
113
+ description: "Proxied response",
114
+ unstructured:
115
+ "Body is forwarded verbatim from the proxied /api/* endpoint; shape and content-type vary per target route",
116
+ },
97
117
  401: { description: "No or invalid page-session cookie" },
98
118
  },
99
119
  auth: { apiKey: false },
@@ -24,7 +24,7 @@ import type { IncomingMessage, ServerResponse } from "node:http";
24
24
  import { z } from "zod";
25
25
  import { BROWSER_SDK_JS, SWARM_UI_JS } from "../artifact-sdk/browser-sdk";
26
26
  import { getPage, incrementPageViewCount } from "../be/db";
27
- import type { Page } from "../types";
27
+ import { type Page, PageSchema } from "../types";
28
28
  import { getAppUrl, getConfiguredAppUrls } from "../utils/constants";
29
29
  import { extractAndVerifyCookie, issuePageSessionCookie } from "../utils/page-session";
30
30
  import { scrubSecrets } from "../utils/secret-scrubber";
@@ -32,6 +32,22 @@ import { route } from "./route-def";
32
32
 
33
33
  // ─── Route definitions (registered with auth: { apiKey: false }) ────────────
34
34
 
35
+ /**
36
+ * Wire shape sent by the `/p/{id}.json` 200 response — a subset of `Page`
37
+ * fields plus a hardcoded `version: 1` (edit-counter is API-internal; the SPA
38
+ * reads real version numbers via `/api/pages/:id/versions`, NOT this field).
39
+ */
40
+ const PublicPageJsonSchema = PageSchema.pick({
41
+ id: true,
42
+ title: true,
43
+ description: true,
44
+ contentType: true,
45
+ authMode: true,
46
+ body: true,
47
+ }).extend({
48
+ version: z.literal(1),
49
+ });
50
+
35
51
  const publicPageRoute = route({
36
52
  method: "get",
37
53
  path: "/p/{id}",
@@ -40,7 +56,11 @@ const publicPageRoute = route({
40
56
  tags: ["Pages"],
41
57
  params: z.object({ id: z.string() }),
42
58
  responses: {
43
- 200: { description: "Rendered HTML page" },
59
+ 200: {
60
+ description: "Rendered HTML page",
61
+ unstructured:
62
+ "Serves the page's raw agent-authored HTML body (SDK-injected) or, for ?print=1 on a JSON-content page, a standalone printable HTML document — not a JSON shape",
63
+ },
44
64
  302: { description: "Redirect to SPA for JSON content" },
45
65
  401: { description: "Page requires an authenticated session" },
46
66
  403: { description: "Cookie does not match this page id" },
@@ -57,7 +77,7 @@ const publicPageJsonRoute = route({
57
77
  tags: ["Pages"],
58
78
  params: z.object({ id: z.string() }),
59
79
  responses: {
60
- 200: { description: "Page JSON" },
80
+ 200: { description: "Page JSON", schema: PublicPageJsonSchema },
61
81
  401: { description: "Page requires an authenticated session" },
62
82
  403: { description: "Cookie does not match this page id" },
63
83
  404: { description: "Page not found" },
@@ -545,23 +565,20 @@ export async function handlePagesPublic(
545
565
  // Returns the current head state (no version history). Body included
546
566
  // verbatim. NOTE: passwordHash / agentId are NOT exposed here — these
547
567
  // are private. step-4 may revisit if needed.
548
- const headers: Record<string, string> = {
549
- "Content-Type": "application/json",
550
- "Cache-Control": "no-store",
551
- };
552
- if (inlineSetCookie) headers["Set-Cookie"] = inlineSetCookie;
553
- res.writeHead(200, headers);
554
- res.end(
555
- JSON.stringify({
556
- id: page.id,
557
- version: 1, // edit-counter is API-internal; SPA reads via /api/pages/:id/versions
558
- title: page.title,
559
- description: page.description,
560
- contentType: page.contentType,
561
- authMode: page.authMode,
562
- body: page.body,
563
- }),
564
- );
568
+ // `res.setHeader` merges with the `Content-Type: application/json` header
569
+ // that `respond()` writes via `res.writeHead()` — Node gives the latter
570
+ // precedence, which is the value we want here anyway.
571
+ res.setHeader("Cache-Control", "no-store");
572
+ if (inlineSetCookie) res.setHeader("Set-Cookie", inlineSetCookie);
573
+ publicPageJsonRoute.respond(res, 200, {
574
+ id: page.id,
575
+ version: 1, // edit-counter is API-internal; SPA reads via /api/pages/:id/versions
576
+ title: page.title,
577
+ description: page.description,
578
+ contentType: page.contentType,
579
+ authMode: page.authMode,
580
+ body: page.body,
581
+ });
565
582
  bumpViewCount(page.id);
566
583
  return true;
567
584
  }