@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
@@ -22,9 +22,14 @@ import {
22
22
  InputValueSchema,
23
23
  TriggerConfigSchema,
24
24
  WorkflowDefinitionSchema,
25
+ WorkflowEdgeSchema,
25
26
  WorkflowNodePatchSchema,
26
27
  WorkflowPatchSchema,
28
+ WorkflowRunSchema,
27
29
  WorkflowRunStatusSchema,
30
+ WorkflowRunStepSchema,
31
+ WorkflowSchema,
32
+ WorkflowVersionSchema,
28
33
  } from "../types";
29
34
  import { getRequestAuth } from "../utils/request-auth-context";
30
35
  import { getExecutorRegistry, startWorkflowExecution } from "../workflows";
@@ -41,7 +46,45 @@ import { handleWebhookTrigger, WebhookError } from "../workflows/triggers";
41
46
  import { snapshotWorkflow } from "../workflows/version";
42
47
  import { resolveHttpFavoriteOwner } from "./favorite-owner";
43
48
  import { route } from "./route-def";
44
- import { json, jsonError, parseBody, triggerSchemaErrorResponse } from "./utils";
49
+ import { jsonError, parseBody, triggerSchemaErrorResponse } from "./utils";
50
+
51
+ // ─── Response Schemas ────────────────────────────────────────────────────────
52
+
53
+ /** `Workflow` decorated with the caller-scoped favorite flag (always set once `withFavoriteFlags` runs). */
54
+ const WorkflowWithFavoriteSchema = WorkflowSchema.extend({ favorite: z.boolean() });
55
+
56
+ /** `/api/workflows` slim list item — mirrors `WorkflowSummary` in src/types.ts (no exported schema there). */
57
+ const WorkflowSummarySchema = WorkflowSchema.omit({
58
+ definition: true,
59
+ triggers: true,
60
+ cooldown: true,
61
+ input: true,
62
+ triggerSchema: true,
63
+ })
64
+ .extend({ nodeCount: z.number().int() })
65
+ .extend({ favorite: z.boolean() });
66
+
67
+ /** Mirrors `ExecutorTypeInfo` in src/workflows/executors/registry.ts. */
68
+ const ExecutorTypeInfoSchema = z.object({
69
+ type: z.string(),
70
+ mode: z.enum(["instant", "async"]),
71
+ configSchema: z.record(z.string(), z.unknown()),
72
+ outputSchema: z.record(z.string(), z.unknown()),
73
+ });
74
+
75
+ /** Mirrors `WorkflowRunPage` in src/be/db.ts. */
76
+ const WorkflowRunPageSchema = z.object({
77
+ runs: z.array(WorkflowRunSchema),
78
+ page: z.object({
79
+ limit: z.number().int(),
80
+ offset: z.number().int(),
81
+ total: z.number().int(),
82
+ hasMore: z.boolean(),
83
+ nextOffset: z.number().int().optional(),
84
+ }),
85
+ });
86
+
87
+ const SuccessResponseSchema = z.object({ success: z.literal(true) });
45
88
 
46
89
  // ─── Route Definitions ───────────────────────────────────────────────────────
47
90
 
@@ -66,7 +109,10 @@ const listWorkflowsRoute = route({
66
109
  fields: z.enum(["full", "slim"]).optional(),
67
110
  }),
68
111
  responses: {
69
- 200: { description: "Workflow list" },
112
+ 200: {
113
+ description: "Workflow list",
114
+ schema: z.union([z.array(WorkflowSummarySchema), z.array(WorkflowWithFavoriteSchema)]),
115
+ },
70
116
  },
71
117
  });
72
118
 
@@ -89,7 +135,7 @@ const createWorkflowRoute = route({
89
135
  vcsRepo: z.string().min(1).optional(),
90
136
  }),
91
137
  responses: {
92
- 201: { description: "Workflow created" },
138
+ 201: { description: "Workflow created", schema: WorkflowSchema },
93
139
  400: { description: "Invalid definition" },
94
140
  },
95
141
  });
@@ -102,7 +148,16 @@ const getWorkflowRoute = route({
102
148
  tags: ["Workflows"],
103
149
  params: z.object({ id: z.string() }),
104
150
  responses: {
105
- 200: { description: "Workflow details with auto-generated edges" },
151
+ 200: {
152
+ description: "Workflow details with auto-generated edges",
153
+ // `favorite` stays optional here (unlike the list routes) because the
154
+ // handler falls back to the undecorated `workflow` (no `favorite`) if
155
+ // `withFavoriteFlags` ever returns an empty array for a truthy input.
156
+ schema: WorkflowSchema.extend({
157
+ favorite: z.boolean().optional(),
158
+ edges: z.array(WorkflowEdgeSchema),
159
+ }),
160
+ },
106
161
  404: { description: "Workflow not found" },
107
162
  },
108
163
  });
@@ -128,7 +183,7 @@ const updateWorkflowRoute = route({
128
183
  enabled: z.boolean().optional(),
129
184
  }),
130
185
  responses: {
131
- 200: { description: "Workflow updated (version snapshot created)" },
186
+ 200: { description: "Workflow updated (version snapshot created)", schema: WorkflowSchema },
132
187
  400: { description: "Invalid definition" },
133
188
  404: { description: "Workflow not found" },
134
189
  },
@@ -143,7 +198,7 @@ const patchWorkflowRoute = route({
143
198
  params: z.object({ id: z.string() }),
144
199
  body: WorkflowPatchSchema.extend({ key: AssetKeySchema.optional() }),
145
200
  responses: {
146
- 200: { description: "Workflow patched (version snapshot created)" },
201
+ 200: { description: "Workflow patched (version snapshot created)", schema: WorkflowSchema },
147
202
  400: { description: "Invalid patch or resulting definition" },
148
203
  404: { description: "Workflow not found" },
149
204
  },
@@ -158,7 +213,7 @@ const patchWorkflowNodeRoute = route({
158
213
  params: z.object({ id: z.string(), nodeId: z.string() }),
159
214
  body: WorkflowNodePatchSchema,
160
215
  responses: {
161
- 200: { description: "Node patched (version snapshot created)" },
216
+ 200: { description: "Node patched (version snapshot created)", schema: WorkflowSchema },
162
217
  400: { description: "Invalid patch or resulting definition" },
163
218
  404: { description: "Workflow or node not found" },
164
219
  },
@@ -185,7 +240,10 @@ const triggerWorkflowRoute = route({
185
240
  tags: ["Workflows"],
186
241
  params: z.object({ id: z.string() }),
187
242
  responses: {
188
- 201: { description: "Workflow run started (or skipped if cooldown active)" },
243
+ 201: {
244
+ description: "Workflow run started (or skipped if cooldown active)",
245
+ schema: z.object({ runId: z.string(), skipped: z.boolean() }),
246
+ },
189
247
  400: { description: "Workflow is disabled" },
190
248
  401: { description: "Unauthorized" },
191
249
  404: { description: "Workflow not found" },
@@ -200,7 +258,10 @@ const validateTriggerRoute = route({
200
258
  tags: ["Workflows"],
201
259
  params: z.object({ id: z.string() }),
202
260
  responses: {
203
- 200: { description: "Payload matches the workflow's triggerSchema (or workflow has none)" },
261
+ 200: {
262
+ description: "Payload matches the workflow's triggerSchema (or workflow has none)",
263
+ schema: z.object({ valid: z.literal(true), schema: z.null().optional() }),
264
+ },
204
265
  400: { description: "Payload failed validation; body matches the TriggerSchemaError contract" },
205
266
  404: { description: "Workflow not found" },
206
267
  },
@@ -219,7 +280,10 @@ const listWorkflowRunsRoute = route({
219
280
  offset: z.coerce.number().int().min(0).optional(),
220
281
  }),
221
282
  responses: {
222
- 200: { description: "Workflow run list" },
283
+ 200: {
284
+ description: "Workflow run list",
285
+ schema: z.union([z.array(WorkflowRunSchema), WorkflowRunPageSchema]),
286
+ },
223
287
  },
224
288
  });
225
289
 
@@ -231,7 +295,10 @@ const getWorkflowRunRoute = route({
231
295
  tags: ["Workflows"],
232
296
  params: z.object({ id: z.string() }),
233
297
  responses: {
234
- 200: { description: "Workflow run details with steps including retry info" },
298
+ 200: {
299
+ description: "Workflow run details with steps including retry info",
300
+ schema: z.object({ run: WorkflowRunSchema, steps: z.array(WorkflowRunStepSchema) }),
301
+ },
235
302
  404: { description: "Run not found" },
236
303
  },
237
304
  });
@@ -244,7 +311,7 @@ const retryWorkflowRunRoute = route({
244
311
  tags: ["Workflows"],
245
312
  params: z.object({ id: z.string() }),
246
313
  responses: {
247
- 200: { description: "Retry started" },
314
+ 200: { description: "Retry started", schema: SuccessResponseSchema },
248
315
  400: { description: "Cannot retry" },
249
316
  },
250
317
  });
@@ -258,7 +325,7 @@ const cancelWorkflowRunRoute = route({
258
325
  params: z.object({ id: z.string() }),
259
326
  body: z.object({ reason: z.string().optional() }).optional(),
260
327
  responses: {
261
- 200: { description: "Run cancelled" },
328
+ 200: { description: "Run cancelled", schema: SuccessResponseSchema },
262
329
  400: { description: "Cannot cancel" },
263
330
  },
264
331
  auth: { apiKey: true },
@@ -271,7 +338,10 @@ const listExecutorTypesRoute = route({
271
338
  summary: "List all executor types with their config and output schemas",
272
339
  tags: ["Workflows"],
273
340
  responses: {
274
- 200: { description: "List of executor types with schemas" },
341
+ 200: {
342
+ description: "List of executor types with schemas",
343
+ schema: z.object({ executorTypes: z.array(ExecutorTypeInfoSchema) }),
344
+ },
275
345
  },
276
346
  });
277
347
 
@@ -283,7 +353,7 @@ const getExecutorTypeRoute = route({
283
353
  tags: ["Workflows"],
284
354
  params: z.object({ type: z.string() }),
285
355
  responses: {
286
- 200: { description: "Executor type details" },
356
+ 200: { description: "Executor type details", schema: ExecutorTypeInfoSchema },
287
357
  404: { description: "Executor type not found" },
288
358
  },
289
359
  });
@@ -297,7 +367,7 @@ const webhookTriggerRoute = route({
297
367
  params: z.object({ workflowId: z.string() }),
298
368
  auth: { apiKey: false },
299
369
  responses: {
300
- 201: { description: "Webhook processed" },
370
+ 201: { description: "Webhook processed", schema: z.object({ runId: z.string() }) },
301
371
  401: { description: "Invalid signature" },
302
372
  404: { description: "Workflow not found" },
303
373
  },
@@ -313,7 +383,10 @@ const listWorkflowVersionsRoute = route({
313
383
  tags: ["Workflows"],
314
384
  params: z.object({ id: z.string() }),
315
385
  responses: {
316
- 200: { description: "Version list (newest first)" },
386
+ 200: {
387
+ description: "Version list (newest first)",
388
+ schema: z.object({ versions: z.array(WorkflowVersionSchema) }),
389
+ },
317
390
  404: { description: "Workflow not found" },
318
391
  },
319
392
  });
@@ -326,7 +399,7 @@ const getWorkflowVersionRoute = route({
326
399
  tags: ["Workflows"],
327
400
  params: z.object({ id: z.string(), version: z.coerce.number().int().min(1) }),
328
401
  responses: {
329
- 200: { description: "Version snapshot" },
402
+ 200: { description: "Version snapshot", schema: WorkflowVersionSchema },
330
403
  404: { description: "Version not found" },
331
404
  },
332
405
  });
@@ -343,7 +416,7 @@ export async function handleWorkflows(
343
416
  // Executor type schemas
344
417
  if (listExecutorTypesRoute.match(req.method, pathSegments)) {
345
418
  const registry = getExecutorRegistry();
346
- json(res, { executorTypes: registry.describeAll() });
419
+ listExecutorTypesRoute.respond(res, 200, { executorTypes: registry.describeAll() });
347
420
  return true;
348
421
  }
349
422
 
@@ -356,7 +429,7 @@ export async function handleWorkflows(
356
429
  res.end();
357
430
  return true;
358
431
  }
359
- json(res, registry.describe(parsed.params.type));
432
+ getExecutorTypeRoute.respond(res, 200, registry.describe(parsed.params.type));
360
433
  return true;
361
434
  }
362
435
 
@@ -371,14 +444,14 @@ export async function handleWorkflows(
371
444
  }
372
445
  const rawBody = Buffer.concat(chunks).toString();
373
446
 
447
+ let result: Awaited<ReturnType<typeof handleWebhookTrigger>>;
374
448
  try {
375
- const result = await handleWebhookTrigger(
449
+ result = await handleWebhookTrigger(
376
450
  workflowId,
377
451
  rawBody, // Raw body string — HMAC is verified against raw bytes; JSON parsing happens inside
378
452
  req.headers, // Full header bag — signature header resolved per trigger config
379
453
  getExecutorRegistry(),
380
454
  );
381
- json(res, result, 201);
382
455
  } catch (err) {
383
456
  if (err instanceof TriggerSchemaError) {
384
457
  triggerSchemaErrorResponse(res, err.message, err.validationErrors);
@@ -387,7 +460,12 @@ export async function handleWorkflows(
387
460
  } else {
388
461
  jsonError(res, String(err), 500);
389
462
  }
463
+ return true;
390
464
  }
465
+ // Egress stays OUTSIDE the try: `respond()` runtime-validates in dev/test
466
+ // and must fail loudly rather than be reported as a webhook failure for a
467
+ // run that already started.
468
+ webhookTriggerRoute.respond(res, 201, result);
391
469
  return true;
392
470
  }
393
471
 
@@ -402,7 +480,7 @@ export async function handleWorkflows(
402
480
  res.end();
403
481
  return true;
404
482
  }
405
- json(res, version);
483
+ getWorkflowVersionRoute.respond(res, 200, version);
406
484
  return true;
407
485
  }
408
486
 
@@ -416,7 +494,7 @@ export async function handleWorkflows(
416
494
  return true;
417
495
  }
418
496
  const versions = getWorkflowVersions(parsed.params.id);
419
- json(res, { versions });
497
+ listWorkflowVersionsRoute.respond(res, 200, { versions });
420
498
  return true;
421
499
  }
422
500
 
@@ -433,10 +511,15 @@ export async function handleWorkflows(
433
511
  };
434
512
  // List responses default to slim (no `definition`); `?fields=full` restores it.
435
513
  if (parsed.query.fields === "full") {
436
- json(res, withFavoriteFlags(listWorkflows(filters), { favoriteScope, itemType: "workflow" }));
514
+ listWorkflowsRoute.respond(
515
+ res,
516
+ 200,
517
+ withFavoriteFlags(listWorkflows(filters), { favoriteScope, itemType: "workflow" }),
518
+ );
437
519
  } else {
438
- json(
520
+ listWorkflowsRoute.respond(
439
521
  res,
522
+ 200,
440
523
  withFavoriteFlags(listWorkflows(filters, { slim: true }), {
441
524
  favoriteScope,
442
525
  itemType: "workflow",
@@ -486,7 +569,7 @@ export async function handleWorkflows(
486
569
  },
487
570
  "api",
488
571
  );
489
- json(res, workflow, 201);
572
+ createWorkflowRoute.respond(res, 201, workflow);
490
573
  return true;
491
574
  }
492
575
 
@@ -503,7 +586,7 @@ export async function handleWorkflows(
503
586
  const edges = generateEdges(workflow.definition);
504
587
  const favoriteScope = resolveHttpFavoriteOwner(req, myAgentId)?.scope;
505
588
  const [decorated] = withFavoriteFlags([workflow], { favoriteScope, itemType: "workflow" });
506
- json(res, { ...(decorated ?? workflow), edges });
589
+ getWorkflowRoute.respond(res, 200, { ...(decorated ?? workflow), edges });
507
590
  return true;
508
591
  }
509
592
 
@@ -553,7 +636,7 @@ export async function handleWorkflows(
553
636
  res.end();
554
637
  return true;
555
638
  }
556
- json(res, workflow);
639
+ patchWorkflowNodeRoute.respond(res, 200, workflow);
557
640
  return true;
558
641
  }
559
642
 
@@ -616,7 +699,7 @@ export async function handleWorkflows(
616
699
  res.end();
617
700
  return true;
618
701
  }
619
- json(res, workflow);
702
+ patchWorkflowRoute.respond(res, 200, workflow);
620
703
  return true;
621
704
  }
622
705
 
@@ -684,7 +767,7 @@ export async function handleWorkflows(
684
767
  res.end();
685
768
  return true;
686
769
  }
687
- json(res, workflow);
770
+ updateWorkflowRoute.respond(res, 200, workflow);
688
771
  return true;
689
772
  }
690
773
 
@@ -714,7 +797,7 @@ export async function handleWorkflows(
714
797
  const body = await parseBody<Record<string, unknown>>(req);
715
798
  const triggerData = (body?.triggerData ?? body) as unknown;
716
799
  if (!workflow.triggerSchema) {
717
- json(res, { valid: true, schema: null });
800
+ validateTriggerRoute.respond(res, 200, { valid: true, schema: null });
718
801
  return true;
719
802
  }
720
803
  const errors = validateJsonSchema(workflow.triggerSchema, triggerData);
@@ -726,7 +809,7 @@ export async function handleWorkflows(
726
809
  );
727
810
  return true;
728
811
  }
729
- json(res, { valid: true });
812
+ validateTriggerRoute.respond(res, 200, { valid: true });
730
813
  return true;
731
814
  }
732
815
 
@@ -764,7 +847,7 @@ export async function handleWorkflows(
764
847
  const run = getWorkflowRun(runId);
765
848
  const skipped = run?.status === "skipped";
766
849
 
767
- json(res, { runId, skipped }, 201);
850
+ triggerWorkflowRoute.respond(res, 201, { runId, skipped });
768
851
  return true;
769
852
  }
770
853
 
@@ -779,13 +862,13 @@ export async function handleWorkflows(
779
862
  limit: parsed.query?.limit ?? 20,
780
863
  offset: parsed.query?.offset ?? 0,
781
864
  });
782
- json(res, page);
865
+ listWorkflowRunsRoute.respond(res, 200, page);
783
866
  return true;
784
867
  }
785
868
  // Preserve the pre-pagination response for the UI when limit/offset are
786
869
  // omitted: a bare array containing every matching run.
787
870
  const runs = listWorkflowRuns(parsed.params.id, { status: parsed.query?.status });
788
- json(res, runs);
871
+ listWorkflowRunsRoute.respond(res, 200, runs);
789
872
  return true;
790
873
  }
791
874
 
@@ -799,7 +882,7 @@ export async function handleWorkflows(
799
882
  return true;
800
883
  }
801
884
  const steps = getWorkflowRunStepsByRunId(parsed.params.id);
802
- json(res, { run, steps });
885
+ getWorkflowRunRoute.respond(res, 200, { run, steps });
803
886
  return true;
804
887
  }
805
888
 
@@ -808,10 +891,13 @@ export async function handleWorkflows(
808
891
  if (!parsed) return true;
809
892
  try {
810
893
  await retryFailedRun(parsed.params.id, getExecutorRegistry());
811
- json(res, { success: true });
812
894
  } catch (err) {
813
895
  jsonError(res, String(err), 400);
896
+ return true;
814
897
  }
898
+ // Outside the try — a response-schema violation must not be reported as a
899
+ // 400 "cannot retry" after the retry already started.
900
+ retryWorkflowRunRoute.respond(res, 200, { success: true });
815
901
  return true;
816
902
  }
817
903
 
@@ -820,10 +906,13 @@ export async function handleWorkflows(
820
906
  if (!parsed) return true;
821
907
  try {
822
908
  cancelWorkflowRun(parsed.params.id, parsed.body?.reason);
823
- json(res, { success: true });
824
909
  } catch (err) {
825
910
  jsonError(res, String(err), 400);
911
+ return true;
826
912
  }
913
+ // Outside the try — a response-schema violation must not be reported as a
914
+ // 400 "cannot cancel" after the run was already cancelled.
915
+ cancelWorkflowRunRoute.respond(res, 200, { success: true });
827
916
  return true;
828
917
  }
829
918
 
package/src/http/x.ts CHANGED
@@ -48,6 +48,24 @@ const TIMEOUT_HEADER = "x-swarm-timeout-ms";
48
48
  // otherwise fully unauthenticated.
49
49
  const MAX_BODY_BYTES = 1 * 1024 * 1024;
50
50
 
51
+ // Mirrors the local `ExternalError` type below — kept as a separate schema
52
+ // since that type is TS-only and not exported for reuse.
53
+ const XScriptErrorSchema = z.object({
54
+ type: z.string(),
55
+ message: z.string(),
56
+ details: z.array(z.string()).optional(),
57
+ });
58
+
59
+ // `result` is `unknown | undefined` on `RunScriptOutput` — when `undefined`,
60
+ // `JSON.stringify` drops the key entirely, so callers may see it absent as
61
+ // well as present-with-any-value (including `null` on the failure paths).
62
+ const XScriptResponseSchema = z.object({
63
+ ok: z.boolean(),
64
+ result: z.unknown().optional(),
65
+ error: XScriptErrorSchema.nullable(),
66
+ durationMs: z.number(),
67
+ });
68
+
51
69
  const scriptApiRoute = route({
52
70
  method: "post",
53
71
  path: "/api/x/script/{endpointId}",
@@ -64,9 +82,21 @@ const scriptApiRoute = route({
64
82
  params: z.object({ endpointId: z.string() }),
65
83
  auth: { apiKey: false },
66
84
  responses: {
67
- 200: { description: "Script executed — see `ok` in the envelope" },
68
- 401: { description: "Missing or invalid bearer token" },
69
- 404: { description: "Endpoint not found or disabled" },
85
+ 200: {
86
+ description: "Script executed — see `ok` in the envelope",
87
+ schema: XScriptResponseSchema,
88
+ },
89
+ // This surface uses a NESTED error envelope `{error: {type, message}}`
90
+ // (public API, richer machine-readable type) — declare it explicitly so
91
+ // the standard flat ErrorResponse fallback doesn't misdocument it.
92
+ 401: {
93
+ description: "Missing or invalid bearer token",
94
+ schema: z.object({ error: XScriptErrorSchema }),
95
+ },
96
+ 404: {
97
+ description: "Endpoint not found or disabled",
98
+ schema: z.object({ error: XScriptErrorSchema }),
99
+ },
70
100
  501: { description: "workspace-rw scripts are not supported" },
71
101
  },
72
102
  });
@@ -154,7 +184,7 @@ export async function handleX(
154
184
  try {
155
185
  args = await parseBody(req);
156
186
  } catch {
157
- json(res, {
187
+ scriptApiRoute.respond(res, 200, {
158
188
  ok: false,
159
189
  result: null,
160
190
  error: { type: "invalid_json", message: "Request body must be valid JSON" },
@@ -182,7 +212,7 @@ export async function handleX(
182
212
  const schema = JSON.parse(script.argsJsonSchema) as Record<string, unknown>;
183
213
  const errors = validateJsonSchema(schema, args ?? null);
184
214
  if (errors.length > 0) {
185
- json(res, {
215
+ scriptApiRoute.respond(res, 200, {
186
216
  ok: false,
187
217
  result: null,
188
218
  error: { type: "args_validation", message: errors.join("; "), details: errors },
@@ -246,8 +276,9 @@ export async function handleX(
246
276
  }
247
277
 
248
278
  // Never expose stdout/stderr to external callers.
249
- json(
279
+ scriptApiRoute.respond(
250
280
  res,
281
+ 200,
251
282
  scrubObject({ ok, result: ok ? output.result : null, error, durationMs: output.durationMs }),
252
283
  );
253
284
  return true;
@@ -1,11 +1,17 @@
1
1
  import {
2
2
  applyMcpOAuthRefresh,
3
3
  getMcpOAuthToken,
4
+ invalidateMcpOAuthClient,
4
5
  isMcpTokenExpiringSoon,
5
6
  type McpOAuthToken,
6
7
  markMcpOAuthTokenStatus,
7
8
  } from "../be/db-queries/mcp-oauth";
8
- import { computeExpiresAt, refreshMcpToken } from "./mcp-wrapper";
9
+ import {
10
+ authMethodForStoredClient,
11
+ computeExpiresAt,
12
+ isInvalidClientError,
13
+ refreshMcpToken,
14
+ } from "./mcp-wrapper";
9
15
 
10
16
  /**
11
17
  * Per-mcpServerId in-memory mutex to serialize concurrent refreshes.
@@ -57,6 +63,7 @@ export async function ensureMcpToken(
57
63
  tokenUrl: token.tokenUrl,
58
64
  clientId: token.dcrClientId ?? "",
59
65
  clientSecret: token.dcrClientSecret ?? undefined,
66
+ tokenEndpointAuthMethod: authMethodForStoredClient(token.tokenEndpointAuthMethod),
60
67
  refreshToken: token.refreshToken,
61
68
  resource: token.resourceUrl,
62
69
  scopes: token.scope ? token.scope.split(" ").filter(Boolean) : undefined,
@@ -73,6 +80,14 @@ export async function ensureMcpToken(
73
80
  return getMcpOAuthToken(mcpServerId, userId);
74
81
  } catch (err) {
75
82
  const message = err instanceof Error ? err.message : String(err);
83
+ if (isInvalidClientError(message)) {
84
+ // The provider has disowned this client. The explicit POST /refresh
85
+ // route already invalidates on this exact error; automatic refreshes
86
+ // through this path (the proxy / server resolution) must do the same,
87
+ // otherwise a revoked client stays "reusable" forever and every
88
+ // subsequent /authorize call keeps selecting it.
89
+ invalidateMcpOAuthClient(mcpServerId, userId);
90
+ }
76
91
  markMcpOAuthTokenStatus(token.id, "error", message);
77
92
  console.error(`[mcp-oauth] refresh failed for ${mcpServerId}: ${message}`);
78
93
  return { ...token, status: "error" as const, lastErrorMessage: message };