@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
@@ -23,12 +23,15 @@ import {
23
23
  upsertSwarmConfig,
24
24
  } from "../be/db";
25
25
  import { reasoningCapability } from "../providers/reasoning-effort";
26
- import { getEnabledCapabilities } from "../server";
26
+ import { ALL_CAPABILITIES, getEnabledCapabilities } from "../server";
27
27
  import { telemetry } from "../telemetry";
28
28
  import {
29
29
  AgentAvatarSchema,
30
30
  AgentCredStatusSchema,
31
31
  AgentLatestModelSchema,
32
+ AgentSchema,
33
+ AgentStatusSchema,
34
+ AgentWithTasksSchema,
32
35
  type ProviderName,
33
36
  ProviderNameSchema,
34
37
  ReasoningEffortSchema,
@@ -39,6 +42,47 @@ import { agentWithCapacity, json, jsonError } from "./utils";
39
42
 
40
43
  // ─── Route Definitions ───────────────────────────────────────────────────────
41
44
 
45
+ /** Mirrors `CAPABILITIES_T` (src/server.ts) — the server's registered MCP tool-group flags. */
46
+ const CapabilitySchema = z.enum(ALL_CAPABILITIES);
47
+
48
+ const AgentCapacitySchema = z.object({
49
+ current: z.number().int(),
50
+ max: z.number().int(),
51
+ available: z.number().int(),
52
+ });
53
+
54
+ /** Shape sent by `agentWithCapacity()` (src/http/utils.ts) for a plain Agent row. */
55
+ const AgentWithCapacitySchema = AgentSchema.extend({
56
+ capacity: AgentCapacitySchema,
57
+ });
58
+
59
+ /**
60
+ * Same as `AgentWithCapacitySchema`, plus the `tasks` array added by
61
+ * `getAgentWithTasks`/`getAllAgentsWithTasks` when `?include=tasks` is used.
62
+ * Reuses the canonical `AgentWithTasksSchema` (src/types.ts) rather than
63
+ * re-declaring the `tasks` field inline.
64
+ */
65
+ const AgentWithCapacityAndTasksSchema = AgentWithTasksSchema.extend({
66
+ capacity: AgentCapacitySchema,
67
+ });
68
+
69
+ /** POST /api/agents response: the agent row plus the server's capability flags. */
70
+ const RegisterAgentResponseSchema = AgentSchema.extend({
71
+ enabledCapabilities: z.array(CapabilitySchema),
72
+ });
73
+
74
+ /** Shape shared by the single- and bulk- credential-status endpoints. */
75
+ const AgentCredentialStatusEntrySchema = z.object({
76
+ agentId: z.string(),
77
+ name: z.string(),
78
+ status: AgentStatusSchema,
79
+ missing: z.array(z.string()),
80
+ provider: ProviderNameSchema.nullable(),
81
+ harnessProvider: ProviderNameSchema.nullable(),
82
+ credStatus: AgentCredStatusSchema.nullable(),
83
+ lastCheckedAt: z.string(),
84
+ });
85
+
42
86
  const registerAgent = route({
43
87
  method: "post",
44
88
  path: "/api/agents",
@@ -64,8 +108,12 @@ const registerAgent = route({
64
108
  200: {
65
109
  description:
66
110
  "Agent re-registered (already existed). Response includes `enabledCapabilities` — the server's capability flags (registered MCP tool groups), not the agent's declared skill tags.",
111
+ schema: RegisterAgentResponseSchema,
112
+ },
113
+ 201: {
114
+ description: "Agent created. Response includes `enabledCapabilities` (see 200).",
115
+ schema: RegisterAgentResponseSchema,
67
116
  },
68
- 201: { description: "Agent created. Response includes `enabledCapabilities` (see 200)." },
69
117
  400: { description: "Validation error" },
70
118
  },
71
119
  });
@@ -83,7 +131,7 @@ const setAgentHarnessProviderRoute = route({
83
131
  harness_provider: ProviderNameSchema,
84
132
  }),
85
133
  responses: {
86
- 200: { description: "Updated agent row" },
134
+ 200: { description: "Updated agent row", schema: AgentWithCapacitySchema },
87
135
  400: { description: "Validation error (unknown provider)" },
88
136
  404: { description: "Agent not found" },
89
137
  },
@@ -107,7 +155,7 @@ const updateAgentRuntimeRoute = route({
107
155
  reasoning_effort: ReasoningEffortSchema.nullable().optional(),
108
156
  }),
109
157
  responses: {
110
- 200: { description: "Updated agent row" },
158
+ 200: { description: "Updated agent row", schema: AgentWithCapacitySchema },
111
159
  400: { description: "Validation error" },
112
160
  404: { description: "Agent not found" },
113
161
  },
@@ -127,7 +175,10 @@ const listAgents = route({
127
175
  fields: z.enum(["full", "slim"]).optional(),
128
176
  }),
129
177
  responses: {
130
- 200: { description: "Agent list with capacity info" },
178
+ 200: {
179
+ description: "Agent list with capacity info",
180
+ schema: z.object({ agents: z.array(AgentWithCapacityAndTasksSchema) }),
181
+ },
131
182
  },
132
183
  });
133
184
 
@@ -140,7 +191,7 @@ const updateAgentNameRoute = route({
140
191
  params: z.object({ id: z.string() }),
141
192
  body: z.object({ name: z.string().min(1) }),
142
193
  responses: {
143
- 200: { description: "Agent updated" },
194
+ 200: { description: "Agent updated", schema: AgentWithCapacitySchema },
144
195
  404: { description: "Agent not found" },
145
196
  409: { description: "Name conflict" },
146
197
  },
@@ -154,7 +205,13 @@ const getAgentSetupScript = route({
154
205
  tags: ["Agents"],
155
206
  params: z.object({ id: z.string() }),
156
207
  responses: {
157
- 200: { description: "Setup scripts" },
208
+ 200: {
209
+ description: "Setup scripts",
210
+ schema: z.object({
211
+ setupScript: z.string().nullable(),
212
+ globalSetupScript: z.string().nullable(),
213
+ }),
214
+ },
158
215
  404: { description: "Agent not found" },
159
216
  },
160
217
  });
@@ -183,7 +240,7 @@ const updateAgentProfileRoute = route({
183
240
  changeReason: z.string().optional(),
184
241
  }),
185
242
  responses: {
186
- 200: { description: "Profile updated" },
243
+ 200: { description: "Profile updated", schema: AgentWithCapacitySchema },
187
244
  400: { description: "Validation error" },
188
245
  404: { description: "Agent not found" },
189
246
  },
@@ -212,7 +269,7 @@ const getAgent = route({
212
269
  include: z.enum(["tasks"]).optional(),
213
270
  }),
214
271
  responses: {
215
- 200: { description: "Agent with capacity info" },
272
+ 200: { description: "Agent with capacity info", schema: AgentWithCapacityAndTasksSchema },
216
273
  404: { description: "Agent not found" },
217
274
  },
218
275
  });
@@ -246,7 +303,10 @@ const updateAgentCredentialStatusRoute = route({
246
303
  params: z.object({ id: z.string() }),
247
304
  body: credentialStatusBody,
248
305
  responses: {
249
- 200: { description: "State updated; returns the agent row." },
306
+ 200: {
307
+ description: "State updated; returns the agent row.",
308
+ schema: AgentWithCapacitySchema,
309
+ },
250
310
  404: { description: "Agent not found" },
251
311
  },
252
312
  });
@@ -259,7 +319,7 @@ const getAgentCredentialStatusRoute = route({
259
319
  tags: ["Agents"],
260
320
  params: z.object({ id: z.string() }),
261
321
  responses: {
262
- 200: { description: "Credential status payload" },
322
+ 200: { description: "Credential status payload", schema: AgentCredentialStatusEntrySchema },
263
323
  404: { description: "Agent not found" },
264
324
  },
265
325
  });
@@ -274,7 +334,10 @@ const listCredentialStatusRoute = route({
274
334
  status: z.enum(["idle", "busy", "offline", "waiting_for_credentials"]).optional(),
275
335
  }),
276
336
  responses: {
277
- 200: { description: "List of {agentId, status, missing[], lastCheckedAt}" },
337
+ 200: {
338
+ description: "List of {agentId, status, missing[], lastCheckedAt}",
339
+ schema: z.object({ agents: z.array(AgentCredentialStatusEntrySchema) }),
340
+ },
278
341
  },
279
342
  });
280
343
 
@@ -375,11 +438,15 @@ export async function handleAgentRegister(
375
438
  // `enabledCapabilities` = the server's capability flags (which MCP tool
376
439
  // groups are registered), NOT the agent's declared skill tags. Workers use
377
440
  // it to drop prompt sections that instruct unregistered tools.
378
- json(
379
- res,
380
- { ...result.agent, enabledCapabilities: getEnabledCapabilities() },
381
- result.created ? 201 : 200,
382
- );
441
+ // Non-null assertion: `result.agent` is only `| null` in the TS union
442
+ // because the re-registration branch re-fetches by id inside the same
443
+ // transaction right after confirming the row exists — it cannot actually
444
+ // be null here. (Same value as before; this only affects the compile-time
445
+ // type used to check the response schema.)
446
+ registerAgent.respond(res, result.created ? 201 : 200, {
447
+ ...result.agent!,
448
+ enabledCapabilities: getEnabledCapabilities(),
449
+ });
383
450
  return true;
384
451
  }
385
452
 
@@ -401,7 +468,7 @@ export async function handleAgentsRest(
401
468
  const slim = parsed.query.fields !== "full";
402
469
  const agents = includeTasks ? getAllAgentsWithTasks({ slim }) : getAllAgents({ slim });
403
470
  const agentsWithCapacity = agents.map(agentWithCapacity);
404
- json(res, { agents: agentsWithCapacity });
471
+ listAgents.respond(res, 200, { agents: agentsWithCapacity });
405
472
  return true;
406
473
  }
407
474
 
@@ -415,7 +482,7 @@ export async function handleAgentsRest(
415
482
  jsonError(res, "Agent not found", 404);
416
483
  return true;
417
484
  }
418
- json(res, agentWithCapacity(agent));
485
+ updateAgentNameRoute.respond(res, 200, agentWithCapacity(agent));
419
486
  } catch (error) {
420
487
  jsonError(res, (error as Error).message, 409);
421
488
  }
@@ -432,7 +499,7 @@ export async function handleAgentsRest(
432
499
  }
433
500
  const globalConfigs = getSwarmConfigs({ scope: "global", key: "SETUP_SCRIPT" });
434
501
  const globalSetupScript = globalConfigs[0]?.value ?? null;
435
- json(res, {
502
+ getAgentSetupScript.respond(res, 200, {
436
503
  setupScript: agent.setupScript ?? null,
437
504
  globalSetupScript,
438
505
  });
@@ -502,7 +569,7 @@ export async function handleAgentsRest(
502
569
  return true;
503
570
  }
504
571
 
505
- json(res, agentWithCapacity(agent));
572
+ updateAgentProfileRoute.respond(res, 200, agentWithCapacity(agent));
506
573
  return true;
507
574
  }
508
575
 
@@ -533,7 +600,7 @@ export async function handleAgentsRest(
533
600
  value: parsed.body.harness_provider,
534
601
  description: "Set via PATCH /api/agents/{id}/harness-provider",
535
602
  });
536
- json(res, agentWithCapacity(agent));
603
+ setAgentHarnessProviderRoute.respond(res, 200, agentWithCapacity(agent));
537
604
  return true;
538
605
  }
539
606
 
@@ -624,7 +691,7 @@ export async function handleAgentsRest(
624
691
  jsonError(res, "Agent not found", 404);
625
692
  return true;
626
693
  }
627
- json(res, agentWithCapacity(agent));
694
+ updateAgentRuntimeRoute.respond(res, 200, agentWithCapacity(agent));
628
695
  return true;
629
696
  }
630
697
 
@@ -646,7 +713,7 @@ export async function handleAgentsRest(
646
713
  credStatus: a.credStatus ?? null,
647
714
  lastCheckedAt: a.lastUpdatedAt,
648
715
  }));
649
- json(res, { agents });
716
+ listCredentialStatusRoute.respond(res, 200, { agents });
650
717
  return true;
651
718
  }
652
719
 
@@ -709,7 +776,7 @@ export async function handleAgentsRest(
709
776
  latestModel: parsed.body.latest_model,
710
777
  }) ?? agent;
711
778
  }
712
- json(res, agentWithCapacity(finalAgent));
779
+ updateAgentCredentialStatusRoute.respond(res, 200, agentWithCapacity(finalAgent));
713
780
  return true;
714
781
  }
715
782
 
@@ -721,7 +788,7 @@ export async function handleAgentsRest(
721
788
  jsonError(res, "Agent not found", 404);
722
789
  return true;
723
790
  }
724
- json(res, {
791
+ getAgentCredentialStatusRoute.respond(res, 200, {
725
792
  agentId: agent.id,
726
793
  name: agent.name,
727
794
  status: agent.status,
@@ -747,7 +814,7 @@ export async function handleAgentsRest(
747
814
  return true;
748
815
  }
749
816
 
750
- json(res, agentWithCapacity(agent));
817
+ getAgent.respond(res, 200, agentWithCapacity(agent));
751
818
  return true;
752
819
  }
753
820
 
@@ -11,10 +11,16 @@ import {
11
11
  setApiKeyName,
12
12
  } from "../be/db";
13
13
  import { route } from "./route-def";
14
- import { json, jsonError } from "./utils";
14
+ import { jsonError } from "./utils";
15
15
 
16
16
  // ─── Route Definitions ───────────────────────────────────────────────────────
17
17
 
18
+ /** Wire shape sent by every `report-*` acknowledgement route. */
19
+ const successMessageSchema = z.object({
20
+ success: z.literal(true),
21
+ message: z.string(),
22
+ });
23
+
18
24
  const reportUsage = route({
19
25
  method: "post",
20
26
  path: "/api/keys/report-usage",
@@ -30,7 +36,7 @@ const reportUsage = route({
30
36
  scopeId: z.string().optional(),
31
37
  }),
32
38
  responses: {
33
- 200: { description: "Usage recorded" },
39
+ 200: { description: "Usage recorded", schema: successMessageSchema },
34
40
  400: { description: "Validation error" },
35
41
  401: { description: "Unauthorized" },
36
42
  },
@@ -52,7 +58,7 @@ const reportRateLimit = route({
52
58
  scopeId: z.string().optional(),
53
59
  }),
54
60
  responses: {
55
- 200: { description: "Key marked as rate-limited" },
61
+ 200: { description: "Key marked as rate-limited", schema: successMessageSchema },
56
62
  400: { description: "Validation error" },
57
63
  401: { description: "Unauthorized" },
58
64
  },
@@ -83,7 +89,10 @@ const reportRateLimitWindows = route({
83
89
  scopeId: z.string().optional(),
84
90
  }),
85
91
  responses: {
86
- 200: { description: "Rate-limit window telemetry recorded" },
92
+ 200: {
93
+ description: "Rate-limit window telemetry recorded",
94
+ schema: successMessageSchema,
95
+ },
87
96
  400: { description: "Validation error" },
88
97
  401: { description: "Unauthorized" },
89
98
  },
@@ -103,13 +112,44 @@ const getAvailable = route({
103
112
  scopeId: z.string().optional(),
104
113
  }),
105
114
  responses: {
106
- 200: { description: "List of available key indices" },
115
+ 200: {
116
+ description: "List of available key indices",
117
+ schema: z.object({
118
+ success: z.literal(true),
119
+ availableIndices: z.array(z.number().int()),
120
+ totalKeys: z.number().int(),
121
+ }),
122
+ },
107
123
  400: { description: "Validation error" },
108
124
  401: { description: "Unauthorized" },
109
125
  },
110
126
  auth: { apiKey: true },
111
127
  });
112
128
 
129
+ /** Mirrors `ApiKeyStatus` in `src/be/db.ts` (not exported from `src/types.ts`). */
130
+ const ApiKeyStatusSchema = z.object({
131
+ id: z.string(),
132
+ keyType: z.string(),
133
+ keySuffix: z.string(),
134
+ keyIndex: z.number().int(),
135
+ scope: z.string(),
136
+ scopeId: z.string().nullable(),
137
+ status: z.string(),
138
+ rateLimitedUntil: z.string().nullable(),
139
+ lastUsedAt: z.string().nullable(),
140
+ lastRateLimitAt: z.string().nullable(),
141
+ totalUsageCount: z.number().int(),
142
+ rateLimitCount: z.number().int(),
143
+ /** Optional human-friendly label set from the dashboard. */
144
+ name: z.string().nullable(),
145
+ /** Auto-derived harness provider (claude/pi/codex/...). */
146
+ provider: z.string(),
147
+ /** Latest provider-emitted rate-limit window snapshots, keyed by window type. */
148
+ rateLimitWindows: z.record(z.string(), rateLimitWindowSchema),
149
+ createdAt: z.string(),
150
+ updatedAt: z.string(),
151
+ });
152
+
113
153
  const listStatuses = route({
114
154
  method: "get",
115
155
  path: "/api/keys/status",
@@ -122,12 +162,25 @@ const listStatuses = route({
122
162
  scopeId: z.string().optional(),
123
163
  }),
124
164
  responses: {
125
- 200: { description: "List of key status records" },
165
+ 200: {
166
+ description: "List of key status records",
167
+ schema: z.object({ success: z.literal(true), keys: z.array(ApiKeyStatusSchema) }),
168
+ },
126
169
  401: { description: "Unauthorized" },
127
170
  },
128
171
  auth: { apiKey: true },
129
172
  });
130
173
 
174
+ /** Mirrors `KeyCostSummary` in `src/be/db.ts`. */
175
+ const KeyCostSummarySchema = z.object({
176
+ keyType: z.string(),
177
+ keySuffix: z.string(),
178
+ totalCost: z.number().nonnegative(),
179
+ totalInputTokens: z.number().int().nonnegative(),
180
+ totalOutputTokens: z.number().int().nonnegative(),
181
+ taskCount: z.number().int().nonnegative(),
182
+ });
183
+
131
184
  const getCosts = route({
132
185
  method: "get",
133
186
  path: "/api/keys/costs",
@@ -138,7 +191,10 @@ const getCosts = route({
138
191
  keyType: z.string().optional(),
139
192
  }),
140
193
  responses: {
141
- 200: { description: "Per-key cost aggregation" },
194
+ 200: {
195
+ description: "Per-key cost aggregation",
196
+ schema: z.object({ success: z.literal(true), costs: z.array(KeyCostSummarySchema) }),
197
+ },
142
198
  401: { description: "Unauthorized" },
143
199
  },
144
200
  auth: { apiKey: true },
@@ -159,7 +215,15 @@ const setKeyName = route({
159
215
  scopeId: z.string().optional(),
160
216
  }),
161
217
  responses: {
162
- 200: { description: "Name updated" },
218
+ 200: {
219
+ description: "Name updated",
220
+ schema: z.object({
221
+ success: z.literal(true),
222
+ keyType: z.string(),
223
+ keySuffix: z.string(),
224
+ name: z.string().nullable(),
225
+ }),
226
+ },
163
227
  400: { description: "Validation error" },
164
228
  401: { description: "Unauthorized" },
165
229
  404: { description: "Key not found" },
@@ -180,7 +244,14 @@ const clearRateLimitRoute = route({
180
244
  scopeId: z.string().optional(),
181
245
  }),
182
246
  responses: {
183
- 200: { description: "Rate limit cleared (or key was not rate-limited)" },
247
+ 200: {
248
+ description: "Rate limit cleared (or key was not rate-limited)",
249
+ schema: z.object({
250
+ success: z.literal(true),
251
+ cleared: z.boolean(),
252
+ message: z.string(),
253
+ }),
254
+ },
184
255
  400: { description: "Validation error" },
185
256
  401: { description: "Unauthorized" },
186
257
  },
@@ -203,7 +274,7 @@ export async function handleApiKeys(
203
274
  const { keyType, keySuffix, keyIndex, taskId, scope, scopeId } = parsed.body;
204
275
  try {
205
276
  recordKeyUsage(keyType, keySuffix, keyIndex, taskId ?? null, scope, scopeId ?? null);
206
- json(res, { success: true, message: "Key usage recorded" });
277
+ reportUsage.respond(res, 200, { success: true, message: "Key usage recorded" });
207
278
  } catch (err) {
208
279
  jsonError(res, err instanceof Error ? err.message : "Failed to record usage", 500);
209
280
  }
@@ -218,7 +289,7 @@ export async function handleApiKeys(
218
289
  const { keyType, keySuffix, keyIndex, rateLimitedUntil, scope, scopeId } = parsed.body;
219
290
  try {
220
291
  markKeyRateLimited(keyType, keySuffix, keyIndex, rateLimitedUntil, scope, scopeId ?? null);
221
- json(res, {
292
+ reportRateLimit.respond(res, 200, {
222
293
  success: true,
223
294
  message: `Key ...${keySuffix} marked as rate-limited until ${rateLimitedUntil}`,
224
295
  });
@@ -236,7 +307,10 @@ export async function handleApiKeys(
236
307
  const { keyType, keySuffix, keyIndex, windows, scope, scopeId } = parsed.body;
237
308
  try {
238
309
  recordKeyRateLimitWindows(keyType, keySuffix, keyIndex, windows, scope, scopeId ?? null);
239
- json(res, { success: true, message: `Rate-limit windows recorded for ...${keySuffix}` });
310
+ reportRateLimitWindows.respond(res, 200, {
311
+ success: true,
312
+ message: `Rate-limit windows recorded for ...${keySuffix}`,
313
+ });
240
314
  } catch (err) {
241
315
  jsonError(
242
316
  res,
@@ -255,7 +329,7 @@ export async function handleApiKeys(
255
329
  const { keyType, totalKeys, scope, scopeId } = parsed.query;
256
330
  try {
257
331
  const indices = getAvailableKeyIndices(keyType, totalKeys, scope, scopeId ?? null);
258
- json(res, { success: true, availableIndices: indices, totalKeys });
332
+ getAvailable.respond(res, 200, { success: true, availableIndices: indices, totalKeys });
259
333
  } catch (err) {
260
334
  jsonError(res, err instanceof Error ? err.message : "Failed to get available keys", 500);
261
335
  }
@@ -270,7 +344,7 @@ export async function handleApiKeys(
270
344
  const { keyType } = parsed.query;
271
345
  try {
272
346
  const costs = getKeyCostSummary(keyType);
273
- json(res, { success: true, costs });
347
+ getCosts.respond(res, 200, { success: true, costs });
274
348
  } catch (err) {
275
349
  jsonError(res, err instanceof Error ? err.message : "Failed to get key costs", 500);
276
350
  }
@@ -285,7 +359,7 @@ export async function handleApiKeys(
285
359
  const { keyType, scope, scopeId } = parsed.query;
286
360
  try {
287
361
  const statuses = getKeyStatuses(keyType, scope, scopeId ?? null);
288
- json(res, { success: true, keys: statuses });
362
+ listStatuses.respond(res, 200, { success: true, keys: statuses });
289
363
  } catch (err) {
290
364
  jsonError(res, err instanceof Error ? err.message : "Failed to get key statuses", 500);
291
365
  }
@@ -307,7 +381,7 @@ export async function handleApiKeys(
307
381
  jsonError(res, `No key matching ${keyType} ...${keySuffix}`, 404);
308
382
  return true;
309
383
  }
310
- json(res, { success: true, keyType, keySuffix, name: value });
384
+ setKeyName.respond(res, 200, { success: true, keyType, keySuffix, name: value });
311
385
  } catch (err) {
312
386
  jsonError(res, err instanceof Error ? err.message : "Failed to set key name", 500);
313
387
  }
@@ -322,7 +396,7 @@ export async function handleApiKeys(
322
396
  const { keyType, keySuffix, scope, scopeId } = parsed.body;
323
397
  try {
324
398
  const cleared = clearKeyRateLimit(keyType, keySuffix, scope, scopeId ?? null);
325
- json(res, {
399
+ clearRateLimitRoute.respond(res, 200, {
326
400
  success: true,
327
401
  cleared,
328
402
  message: cleared
@@ -2,6 +2,7 @@ import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import { z } from "zod";
3
3
  import { resolveTaskAuditUserId } from "../be/audit-user";
4
4
  import {
5
+ type ApprovalRequest,
5
6
  createApprovalRequest,
6
7
  createTaskExtended,
7
8
  getApprovalRequestById,
@@ -13,7 +14,7 @@ import { resolveTemplate } from "../prompts/resolver";
13
14
  import { getRequestAuth } from "../utils/request-auth-context";
14
15
  import { workflowEventBus } from "../workflows/event-bus";
15
16
  import { route } from "./route-def";
16
- import { json, jsonError } from "./utils";
17
+ import { jsonError } from "./utils";
17
18
 
18
19
  // ─── Route Definitions ───────────────────────────────────────────────────────
19
20
 
@@ -41,6 +42,69 @@ const QuestionSchema = z.object({
41
42
 
42
43
  export type ApprovalQuestion = z.infer<typeof QuestionSchema>;
43
44
 
45
+ // ─── Response Schemas ────────────────────────────────────────────────────────
46
+ // `ApprovalRequest` (src/be/db.ts) types `questions`/`approvers`/`responses`/
47
+ // `notificationChannels` as `unknown` — the DB layer stores opaque JSON blobs.
48
+ // Reading every writer (this file's create route, tools/request-human-input.ts,
49
+ // workflows/executors/human-in-the-loop.ts) confirms they always conform to the
50
+ // shapes below, so we describe them precisely here and cast at the call sites
51
+ // (matching the existing `existing.questions as ApprovalQuestion[]` pattern in
52
+ // this file) rather than degrading the documented contract to `z.unknown()`.
53
+
54
+ const ApproversSchema = z.object({
55
+ users: z.array(z.string()).optional(),
56
+ roles: z.array(z.string()).optional(),
57
+ policy: z.union([z.literal("any"), z.literal("all"), z.object({ min: z.number().int().min(1) })]),
58
+ });
59
+ type ApproversShape = z.infer<typeof ApproversSchema>;
60
+
61
+ const NotificationChannelSchema = z.object({
62
+ channel: z.enum(["slack", "email"]),
63
+ target: z.string(),
64
+ // Added post-creation by workflows/executors/human-in-the-loop.ts once the
65
+ // notification message is sent (see `updateApprovalRequestNotifications`).
66
+ messageTs: z.string().optional(),
67
+ });
68
+ type NotificationChannelShape = z.infer<typeof NotificationChannelSchema>;
69
+
70
+ const ApprovalRequestSchema = z.object({
71
+ id: z.string(),
72
+ title: z.string(),
73
+ questions: z.array(QuestionSchema),
74
+ workflowRunId: z.string().nullable(),
75
+ workflowRunStepId: z.string().nullable(),
76
+ sourceTaskId: z.string().nullable(),
77
+ approvers: ApproversSchema,
78
+ status: z.enum(["pending", "approved", "rejected", "timeout"]),
79
+ responses: z.record(z.string(), z.unknown()).nullable(),
80
+ resolvedBy: z.string().nullable(),
81
+ resolvedAt: z.string().nullable(),
82
+ timeoutSeconds: z.number().nullable(),
83
+ expiresAt: z.string().nullable(),
84
+ notificationChannels: z.array(NotificationChannelSchema).nullable(),
85
+ createdBy: z.string().optional(),
86
+ createdAt: z.string(),
87
+ updatedAt: z.string(),
88
+ });
89
+
90
+ /**
91
+ * Reshapes a DB `ApprovalRequest` row for `respond()` — identical values,
92
+ * narrowed from the DB layer's `unknown` fields to the precise wire shape
93
+ * (see comment above `ApproversSchema`). Not a behavior change: same object
94
+ * contents, serialized the same way.
95
+ */
96
+ function toApprovalRequestResponse(
97
+ request: ApprovalRequest,
98
+ ): z.infer<typeof ApprovalRequestSchema> {
99
+ return {
100
+ ...request,
101
+ questions: request.questions as ApprovalQuestion[],
102
+ approvers: request.approvers as ApproversShape,
103
+ responses: request.responses as Record<string, unknown> | null,
104
+ notificationChannels: request.notificationChannels as NotificationChannelShape[] | null,
105
+ };
106
+ }
107
+
44
108
  function hasRequiredResponse(question: ApprovalQuestion, response: unknown): boolean {
45
109
  switch (question.type) {
46
110
  case "approval":
@@ -118,7 +182,10 @@ const createRoute = route({
118
182
  .optional(),
119
183
  }),
120
184
  responses: {
121
- 201: { description: "Approval request created" },
185
+ 201: {
186
+ description: "Approval request created",
187
+ schema: z.object({ approvalRequest: ApprovalRequestSchema }),
188
+ },
122
189
  400: { description: "Validation error" },
123
190
  },
124
191
  auth: { apiKey: true },
@@ -132,7 +199,10 @@ const getByIdRoute = route({
132
199
  tags: ["ApprovalRequests"],
133
200
  params: z.object({ id: z.string().uuid() }),
134
201
  responses: {
135
- 200: { description: "Approval request details" },
202
+ 200: {
203
+ description: "Approval request details",
204
+ schema: z.object({ approvalRequest: ApprovalRequestSchema }),
205
+ },
136
206
  404: { description: "Not found" },
137
207
  },
138
208
  auth: { apiKey: true },
@@ -150,7 +220,10 @@ const respondRoute = route({
150
220
  respondedBy: z.string().optional(),
151
221
  }),
152
222
  responses: {
153
- 200: { description: "Response recorded" },
223
+ 200: {
224
+ description: "Response recorded",
225
+ schema: z.object({ approvalRequest: ApprovalRequestSchema }),
226
+ },
154
227
  400: { description: "Validation error" },
155
228
  404: { description: "Not found" },
156
229
  409: { description: "Already resolved" },
@@ -170,7 +243,10 @@ const listRoute = route({
170
243
  limit: z.coerce.number().optional(),
171
244
  }),
172
245
  responses: {
173
- 200: { description: "List of approval requests" },
246
+ 200: {
247
+ description: "List of approval requests",
248
+ schema: z.object({ approvalRequests: z.array(ApprovalRequestSchema) }),
249
+ },
174
250
  },
175
251
  auth: { apiKey: true },
176
252
  });
@@ -278,7 +354,7 @@ export async function handleApprovalRequests(
278
354
  }
279
355
  }
280
356
 
281
- json(res, { approvalRequest: updated });
357
+ respondRoute.respond(res, 200, { approvalRequest: toApprovalRequestResponse(updated) });
282
358
  return true;
283
359
  }
284
360
 
@@ -293,7 +369,7 @@ export async function handleApprovalRequests(
293
369
  return true;
294
370
  }
295
371
 
296
- json(res, { approvalRequest: request });
372
+ getByIdRoute.respond(res, 200, { approvalRequest: toApprovalRequestResponse(request) });
297
373
  return true;
298
374
  }
299
375
 
@@ -325,8 +401,7 @@ export async function handleApprovalRequests(
325
401
  createdBy,
326
402
  });
327
403
 
328
- res.writeHead(201, { "Content-Type": "application/json" });
329
- res.end(JSON.stringify({ approvalRequest: request }));
404
+ createRoute.respond(res, 201, { approvalRequest: toApprovalRequestResponse(request) });
330
405
  return true;
331
406
  }
332
407
 
@@ -341,7 +416,9 @@ export async function handleApprovalRequests(
341
416
  limit: parsed.query.limit || undefined,
342
417
  });
343
418
 
344
- json(res, { approvalRequests: requests });
419
+ listRoute.respond(res, 200, {
420
+ approvalRequests: requests.map(toApprovalRequestResponse),
421
+ });
345
422
  return true;
346
423
  }
347
424