@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
package/src/http/users.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  *
10
10
  * Endpoint set:
11
11
  *
12
+ * GET /api/whoami
12
13
  * GET /api/users
13
14
  * POST /api/users
14
15
  * GET /api/users/unmapped (must precede /:id)
@@ -47,9 +48,11 @@ import {
47
48
  revokeToken,
48
49
  unlinkIdentity,
49
50
  } from "../be/users";
51
+ import { UserSchema } from "../types";
52
+ import { getRequestAuth } from "../utils/request-auth-context";
50
53
  import { getOperatorActor } from "./operator-actor";
51
54
  import { route } from "./route-def";
52
- import { json, jsonError } from "./utils";
55
+ import { jsonError } from "./utils";
53
56
 
54
57
  // ─── Response composition ────────────────────────────────────────────────────
55
58
 
@@ -78,8 +81,85 @@ function syncUserBudgetMirror(userId: string, dailyBudgetUsd: number | null | un
78
81
  upsertBudget("user", userId, dailyBudgetUsd);
79
82
  }
80
83
 
84
+ // ─── Response schemas ────────────────────────────────────────────────────────
85
+
86
+ const UserIdentityLinkSchema = z.object({
87
+ kind: z.string(),
88
+ externalId: z.string(),
89
+ });
90
+
91
+ const UserTokenSummarySchema = z.object({
92
+ id: z.string(),
93
+ userId: z.string(),
94
+ label: z.string().nullable(),
95
+ tokenPreview: z.string(),
96
+ createdAt: z.string(),
97
+ lastUsedAt: z.string().nullable(),
98
+ revokedAt: z.string().nullable(),
99
+ });
100
+
101
+ const IdentityEventSchema = z.object({
102
+ id: z.string(),
103
+ userId: z.string(),
104
+ // `IdentityEvent.eventType` is read back from the DB as a plain `string`
105
+ // (the CHECK constraint guarantees membership in `IdentityEventTypeSchema`,
106
+ // but the read path's static type is not narrowed to the enum).
107
+ eventType: z.string(),
108
+ actor: z.string(),
109
+ before: z.unknown().nullable(),
110
+ after: z.unknown().nullable(),
111
+ createdAt: z.string(),
112
+ });
113
+
114
+ /** `composeUser()`'s shape — base user row + identities + token summaries + recent events. */
115
+ const ComposedUserSchema = UserSchema.extend({
116
+ identities: z.array(UserIdentityLinkSchema),
117
+ tokens: z.array(UserTokenSummarySchema),
118
+ recentEvents: z.array(IdentityEventSchema),
119
+ });
120
+
121
+ /** `collectUnmappedForKind()`'s per-externalId grouped shape. */
122
+ const UnmappedIdentitySchema = z.object({
123
+ kind: z.string(),
124
+ externalId: z.string(),
125
+ lastSeenAt: z.string().nullable(),
126
+ count: z.number(),
127
+ sampleEventType: z.string().nullable(),
128
+ sampleContext: z.unknown().nullable(),
129
+ });
130
+
81
131
  // ─── Route Definitions ───────────────────────────────────────────────────────
82
132
 
133
+ /**
134
+ * DES-771: identity resolution for embedded dashboards. A client holding a
135
+ * user-bound `aswt_` token learns which user the server attributes its
136
+ * requests to (the token forces requester/audit attribution server-side, so
137
+ * this is the only identity the client can act as). The operator key resolves
138
+ * to `kind: "operator"` with no bound user.
139
+ */
140
+ const whoamiRoute = route({
141
+ method: "get",
142
+ path: "/api/whoami",
143
+ pattern: ["api", "whoami"],
144
+ summary: "Resolve the authenticated principal behind the presented bearer",
145
+ tags: ["Users"],
146
+ responses: {
147
+ 200: {
148
+ description: "Authenticated principal",
149
+ schema: z.object({
150
+ kind: z.enum(["operator", "user"]),
151
+ // `z.union([UserSchema, z.null()])`, NOT `UserSchema.nullable()`:
152
+ // .nullable() on a registered schema rewrites the shared
153
+ // `components.schemas.User` to `object | null` for every consumer;
154
+ // the union keeps nullability local to this response field.
155
+ user: z.union([UserSchema, z.null()]),
156
+ }),
157
+ },
158
+ 401: { description: "Unauthorized" },
159
+ },
160
+ auth: { apiKey: true },
161
+ });
162
+
83
163
  const listUsers = route({
84
164
  method: "get",
85
165
  path: "/api/users",
@@ -90,7 +170,10 @@ const listUsers = route({
90
170
  recentEvents: z.coerce.number().int().min(0).max(50).optional(),
91
171
  }),
92
172
  responses: {
93
- 200: { description: "List of users" },
173
+ 200: {
174
+ description: "List of users",
175
+ schema: z.object({ users: z.array(ComposedUserSchema.nullable()) }),
176
+ },
94
177
  401: { description: "Unauthorized" },
95
178
  },
96
179
  auth: { apiKey: true },
@@ -118,7 +201,10 @@ const createUserRoute = route({
118
201
  .optional(),
119
202
  }),
120
203
  responses: {
121
- 200: { description: "User created" },
204
+ 200: {
205
+ description: "User created",
206
+ schema: z.object({ user: ComposedUserSchema.nullable() }),
207
+ },
122
208
  400: { description: "Validation error" },
123
209
  401: { description: "Unauthorized" },
124
210
  },
@@ -136,7 +222,10 @@ const listUnmapped = route({
136
222
  limit: z.coerce.number().int().min(1).max(1000).optional(),
137
223
  }),
138
224
  responses: {
139
- 200: { description: "List of unmapped identities sorted by count DESC, lastSeenAt DESC" },
225
+ 200: {
226
+ description: "List of unmapped identities sorted by count DESC, lastSeenAt DESC",
227
+ schema: z.object({ unmapped: z.array(UnmappedIdentitySchema) }),
228
+ },
140
229
  401: { description: "Unauthorized" },
141
230
  },
142
231
  auth: { apiKey: true },
@@ -158,7 +247,10 @@ const resolveUnmapped = route({
158
247
  }),
159
248
  ]),
160
249
  responses: {
161
- 200: { description: "Identity linked + kv entries cleared" },
250
+ 200: {
251
+ description: "Identity linked + kv entries cleared",
252
+ schema: z.object({ user: ComposedUserSchema.nullable() }),
253
+ },
162
254
  400: { description: "Validation error" },
163
255
  401: { description: "Unauthorized" },
164
256
  404: { description: "Target user not found" },
@@ -177,7 +269,10 @@ const getUserRoute = route({
177
269
  recentEvents: z.coerce.number().int().min(0).max(200).optional(),
178
270
  }),
179
271
  responses: {
180
- 200: { description: "User row" },
272
+ 200: {
273
+ description: "User row",
274
+ schema: z.object({ user: ComposedUserSchema }),
275
+ },
181
276
  401: { description: "Unauthorized" },
182
277
  404: { description: "User not found" },
183
278
  },
@@ -213,7 +308,10 @@ const updateUserRoute = route({
213
308
  message: "At least one field must be provided",
214
309
  }),
215
310
  responses: {
216
- 200: { description: "User updated" },
311
+ 200: {
312
+ description: "User updated",
313
+ schema: z.object({ user: ComposedUserSchema.nullable() }),
314
+ },
217
315
  400: { description: "Validation error or empty body" },
218
316
  401: { description: "Unauthorized" },
219
317
  404: { description: "User not found" },
@@ -234,7 +332,14 @@ const mintUserMcpTokenRoute = route({
234
332
  label: z.string().nullable().optional(),
235
333
  }),
236
334
  responses: {
237
- 200: { description: "Minted token plaintext, token summary and composed user" },
335
+ 200: {
336
+ description: "Minted token plaintext, token summary and composed user",
337
+ schema: z.object({
338
+ plaintext: z.string(),
339
+ token: UserTokenSummarySchema.optional(),
340
+ user: ComposedUserSchema.nullable(),
341
+ }),
342
+ },
238
343
  401: { description: "Unauthorized" },
239
344
  404: { description: "User not found" },
240
345
  },
@@ -249,7 +354,10 @@ const revokeUserMcpTokenRoute = route({
249
354
  tags: ["Users"],
250
355
  params: z.object({ id: z.string(), tokenId: z.string() }),
251
356
  responses: {
252
- 200: { description: "Composed user after token revocation" },
357
+ 200: {
358
+ description: "Composed user after token revocation",
359
+ schema: z.object({ user: ComposedUserSchema.nullable() }),
360
+ },
253
361
  401: { description: "Unauthorized" },
254
362
  404: { description: "User or token not found" },
255
363
  },
@@ -265,7 +373,10 @@ const mergeUsersRoute = route({
265
373
  params: z.object({ id: z.string() }),
266
374
  body: z.object({ sourceUserId: z.string().min(1) }),
267
375
  responses: {
268
- 200: { description: "Merged user" },
376
+ 200: {
377
+ description: "Merged user",
378
+ schema: z.object({ user: ComposedUserSchema.nullable() }),
379
+ },
269
380
  400: { description: "Validation error (e.g. target == source)" },
270
381
  401: { description: "Unauthorized" },
271
382
  404: { description: "Target or source user not found" },
@@ -285,7 +396,10 @@ const listEventsRoute = route({
285
396
  before: z.string().optional(),
286
397
  }),
287
398
  responses: {
288
- 200: { description: "Array of identity events" },
399
+ 200: {
400
+ description: "Array of identity events",
401
+ schema: z.object({ events: z.array(IdentityEventSchema) }),
402
+ },
289
403
  401: { description: "Unauthorized" },
290
404
  404: { description: "User not found" },
291
405
  },
@@ -301,7 +415,10 @@ const addIdentityRoute = route({
301
415
  params: z.object({ id: z.string() }),
302
416
  body: z.object({ kind: z.string().min(1), externalId: z.string().min(1) }),
303
417
  responses: {
304
- 200: { description: "Updated identity list" },
418
+ 200: {
419
+ description: "Updated identity list",
420
+ schema: z.object({ identities: z.array(UserIdentityLinkSchema) }),
421
+ },
305
422
  400: { description: "Validation error or PK collision" },
306
423
  401: { description: "Unauthorized" },
307
424
  404: { description: "User not found" },
@@ -317,7 +434,10 @@ const deleteIdentityRoute = route({
317
434
  tags: ["Users"],
318
435
  params: z.object({ id: z.string(), kind: z.string(), externalId: z.string() }),
319
436
  responses: {
320
- 200: { description: "Updated identity list" },
437
+ 200: {
438
+ description: "Updated identity list",
439
+ schema: z.object({ identities: z.array(UserIdentityLinkSchema) }),
440
+ },
321
441
  401: { description: "Unauthorized" },
322
442
  404: { description: "User not found" },
323
443
  },
@@ -403,13 +523,26 @@ export async function handleUsers(
403
523
  pathSegments: string[],
404
524
  queryParams: URLSearchParams,
405
525
  ): Promise<boolean> {
526
+ // ─── GET /api/whoami ───────────────────────────────────────────────────────
527
+ if (whoamiRoute.match(req.method, pathSegments)) {
528
+ const parsed = await whoamiRoute.parse(req, res, pathSegments, queryParams);
529
+ if (!parsed) return true;
530
+ const auth = getRequestAuth(req);
531
+ if (auth?.kind === "user") {
532
+ whoamiRoute.respond(res, 200, { kind: "user", user: auth.user });
533
+ } else {
534
+ whoamiRoute.respond(res, 200, { kind: "operator", user: null });
535
+ }
536
+ return true;
537
+ }
538
+
406
539
  // ─── GET /api/users ────────────────────────────────────────────────────────
407
540
  if (listUsers.match(req.method, pathSegments)) {
408
541
  const parsed = await listUsers.parse(req, res, pathSegments, queryParams);
409
542
  if (!parsed) return true;
410
543
  const recentLimit = parsed.query.recentEvents ?? 5;
411
544
  const users = getAllUsers().map((u) => composeUser(u.id, recentLimit));
412
- json(res, { users });
545
+ listUsers.respond(res, 200, { users });
413
546
  return true;
414
547
  }
415
548
 
@@ -433,7 +566,7 @@ export async function handleUsers(
433
566
  });
434
567
  }
435
568
  const composed = composeUser(user.id);
436
- json(res, { user: composed });
569
+ createUserRoute.respond(res, 200, { user: composed });
437
570
  } catch (err) {
438
571
  jsonError(res, err instanceof Error ? err.message : "Failed to create user", 500);
439
572
  }
@@ -454,7 +587,7 @@ export async function handleUsers(
454
587
  const bl = b.lastSeenAt ?? "";
455
588
  return bl.localeCompare(al);
456
589
  });
457
- json(res, { unmapped: rows.slice(0, limit) });
590
+ listUnmapped.respond(res, 200, { unmapped: rows.slice(0, limit) });
458
591
  return true;
459
592
  }
460
593
 
@@ -494,7 +627,7 @@ export async function handleUsers(
494
627
  deleteKv(ns, `${externalId}:meta`);
495
628
  deleteKv(ns, `${externalId}:count`);
496
629
  const user = composeUser(targetUserId);
497
- json(res, { user });
630
+ resolveUnmapped.respond(res, 200, { user });
498
631
  } catch (err) {
499
632
  jsonError(res, err instanceof Error ? err.message : "Failed to resolve unmapped", 500);
500
633
  }
@@ -513,7 +646,7 @@ export async function handleUsers(
513
646
  limit: parsed.query.limit,
514
647
  before: parsed.query.before,
515
648
  });
516
- json(res, { events });
649
+ listEventsRoute.respond(res, 200, { events });
517
650
  return true;
518
651
  }
519
652
 
@@ -531,7 +664,11 @@ export async function handleUsers(
531
664
  try {
532
665
  const { tokenId, plaintext } = mintToken(parsed.params.id, parsed.body.label ?? null, actor);
533
666
  const token = listUserTokens(parsed.params.id).find((t) => t.id === tokenId);
534
- json(res, { plaintext, token, user: composeUser(parsed.params.id) });
667
+ mintUserMcpTokenRoute.respond(res, 200, {
668
+ plaintext,
669
+ token,
670
+ user: composeUser(parsed.params.id),
671
+ });
535
672
  } catch (err) {
536
673
  jsonError(res, err instanceof Error ? err.message : "Failed to mint token", 500);
537
674
  }
@@ -559,7 +696,7 @@ export async function handleUsers(
559
696
 
560
697
  try {
561
698
  revokeToken(parsed.params.tokenId, actor);
562
- json(res, { user: composeUser(parsed.params.id) });
699
+ revokeUserMcpTokenRoute.respond(res, 200, { user: composeUser(parsed.params.id) });
563
700
  } catch (err) {
564
701
  const message = err instanceof Error ? err.message : "Failed to revoke token";
565
702
  if (message.includes("Token not found")) {
@@ -583,7 +720,7 @@ export async function handleUsers(
583
720
  }
584
721
  try {
585
722
  linkIdentity(parsed.params.id, parsed.body.kind, parsed.body.externalId, actor);
586
- json(res, { identities: getUserIdentities(parsed.params.id) });
723
+ addIdentityRoute.respond(res, 200, { identities: getUserIdentities(parsed.params.id) });
587
724
  } catch (err) {
588
725
  jsonError(res, err instanceof Error ? err.message : "Failed to link identity", 400);
589
726
  }
@@ -607,7 +744,7 @@ export async function handleUsers(
607
744
  const kind = decodeURIComponent(parsed.params.kind);
608
745
  const externalId = decodeURIComponent(parsed.params.externalId);
609
746
  unlinkIdentity(parsed.params.id, kind, externalId, actor);
610
- json(res, { identities: getUserIdentities(parsed.params.id) });
747
+ deleteIdentityRoute.respond(res, 200, { identities: getUserIdentities(parsed.params.id) });
611
748
  } catch (err) {
612
749
  jsonError(res, err instanceof Error ? err.message : "Failed to unlink identity", 500);
613
750
  }
@@ -692,7 +829,7 @@ export async function handleUsers(
692
829
 
693
830
  // Re-compose AFTER the event so the response surfaces the merge event in
694
831
  // recentEvents (otherwise the timeline is missing the event we just wrote).
695
- json(res, { user: composeUser(targetId) });
832
+ mergeUsersRoute.respond(res, 200, { user: composeUser(targetId) });
696
833
  } catch (err) {
697
834
  jsonError(res, err instanceof Error ? err.message : "Failed to merge users", 500);
698
835
  }
@@ -708,7 +845,7 @@ export async function handleUsers(
708
845
  jsonError(res, "User not found", 404);
709
846
  return true;
710
847
  }
711
- json(res, { user: composed });
848
+ getUserRoute.respond(res, 200, { user: composed });
712
849
  return true;
713
850
  }
714
851
 
@@ -826,7 +963,7 @@ export async function handleUsers(
826
963
  }
827
964
 
828
965
  const composed = composeUser(parsed.params.id);
829
- json(res, { user: composed });
966
+ updateUserRoute.respond(res, 200, { user: composed });
830
967
  } catch (err) {
831
968
  jsonError(res, err instanceof Error ? err.message : "Failed to update user", 500);
832
969
  }
package/src/http/utils.ts CHANGED
@@ -79,13 +79,23 @@ export function agentWithCapacity<T extends { id: string; maxTasks?: number }>(
79
79
  };
80
80
  }
81
81
 
82
- /** Parse JSON body from incoming request */
82
+ /**
83
+ * Parse the JSON body of an incoming request.
84
+ *
85
+ * An empty (or whitespace-only) body parses as `{}` rather than throwing:
86
+ * `JSON.parse("")` raises a SyntaxError, and the `route()` factory only
87
+ * converts ZodErrors — so a bodyless POST used to escape as a 500. Yielding
88
+ * `{}` lets an all-optional body schema accept a bodyless request and turns a
89
+ * required-body schema's failure into the honest 400 its validation produces.
90
+ */
83
91
  export async function parseBody<T = unknown>(req: IncomingMessage): Promise<T> {
84
92
  const chunks: Buffer[] = [];
85
93
  for await (const chunk of req) {
86
94
  chunks.push(chunk as Buffer);
87
95
  }
88
- return JSON.parse(Buffer.concat(chunks).toString()) as T;
96
+ const raw = Buffer.concat(chunks).toString();
97
+ if (raw.trim() === "") return {} as T;
98
+ return JSON.parse(raw) as T;
89
99
  }
90
100
 
91
101
  /**
@@ -1,4 +1,5 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import { z } from "zod";
2
3
  import type { AgentMailWebhookPayload } from "../agentmail";
3
4
  import {
4
5
  handleMessageReceived,
@@ -57,6 +58,16 @@ import { route } from "./route-def";
57
58
 
58
59
  // ─── Route Definitions (documentation only — webhooks handle their own body parsing) ─
59
60
 
61
+ /**
62
+ * Shared shape returned by the GitHub/GitLab dispatch handlers in
63
+ * ../github/handlers.ts and ../gitlab/handlers.ts — `{ created: boolean;
64
+ * taskId?: string }`, echoed back verbatim as the webhook ack body.
65
+ */
66
+ const WebhookDispatchResultSchema = z.object({
67
+ created: z.boolean(),
68
+ taskId: z.string().optional(),
69
+ });
70
+
60
71
  const githubWebhook = route({
61
72
  method: "post",
62
73
  path: "/api/github/webhook",
@@ -65,7 +76,10 @@ const githubWebhook = route({
65
76
  tags: ["Webhooks"],
66
77
  auth: { apiKey: false },
67
78
  responses: {
68
- 200: { description: "Event processed" },
79
+ 200: {
80
+ description: "Event processed",
81
+ schema: z.union([z.object({ message: z.literal("pong") }), WebhookDispatchResultSchema]),
82
+ },
69
83
  401: { description: "Invalid signature" },
70
84
  503: { description: "GitHub integration not configured" },
71
85
  },
@@ -79,7 +93,7 @@ const gitlabWebhook = route({
79
93
  tags: ["Webhooks"],
80
94
  auth: { apiKey: false },
81
95
  responses: {
82
- 200: { description: "Event processed" },
96
+ 200: { description: "Event processed", schema: WebhookDispatchResultSchema },
83
97
  401: { description: "Invalid token" },
84
98
  503: { description: "GitLab integration not configured" },
85
99
  },
@@ -93,7 +107,7 @@ const agentmailWebhook = route({
93
107
  tags: ["Webhooks"],
94
108
  auth: { apiKey: false },
95
109
  responses: {
96
- 200: { description: "Event received" },
110
+ 200: { description: "Event received", schema: z.object({ received: z.literal(true) }) },
97
111
  401: { description: "Invalid signature" },
98
112
  503: { description: "AgentMail integration not configured" },
99
113
  },
@@ -107,7 +121,13 @@ const kapsoWebhook = route({
107
121
  tags: ["Webhooks"],
108
122
  auth: { apiKey: false },
109
123
  responses: {
110
- 200: { description: "Event received" },
124
+ 200: {
125
+ description: "Event received",
126
+ schema: z.object({
127
+ received: z.literal(true),
128
+ routing: z.enum(["skip", "duplicate", "workflow", "task", "no_mapping", "error"]),
129
+ }),
130
+ },
111
131
  401: { description: "Invalid signature" },
112
132
  503: { description: "Kapso integration not configured" },
113
133
  },
@@ -213,8 +233,7 @@ export async function handleWebhooks(
213
233
 
214
234
  if (eventType === "ping") {
215
235
  console.log("[GitHub] Received ping event - webhook configured successfully");
216
- res.writeHead(200, { "Content-Type": "application/json" });
217
- res.end(JSON.stringify({ message: "pong" }));
236
+ githubWebhook.respond(res, 200, { message: "pong" });
218
237
  return true;
219
238
  }
220
239
 
@@ -309,8 +328,7 @@ export async function handleWebhooks(
309
328
  }
310
329
  }
311
330
 
312
- res.writeHead(200, { "Content-Type": "application/json" });
313
- res.end(JSON.stringify(result));
331
+ githubWebhook.respond(res, 200, result);
314
332
  } catch (err) {
315
333
  const errorMessage = err instanceof Error ? err.message : String(err);
316
334
  console.error(`[GitHub] Error handling ${eventType} event: ${errorMessage}`);
@@ -425,8 +443,7 @@ export async function handleWebhooks(
425
443
  }
426
444
  }
427
445
 
428
- res.writeHead(200, { "Content-Type": "application/json" });
429
- res.end(JSON.stringify(result));
446
+ gitlabWebhook.respond(res, 200, result);
430
447
  } catch (err) {
431
448
  const errorMessage = err instanceof Error ? err.message : String(err);
432
449
  console.error(`[GitLab] Error handling ${objectKind} event: ${errorMessage}`);
@@ -471,8 +488,7 @@ export async function handleWebhooks(
471
488
 
472
489
  // Return 200 immediately — Svix best practice to avoid retries.
473
490
  // Processing happens asynchronously below; dedup is handled in handlers.ts.
474
- res.writeHead(200, { "Content-Type": "application/json" });
475
- res.end(JSON.stringify({ received: true }));
491
+ agentmailWebhook.respond(res, 200, { received: true });
476
492
 
477
493
  const payload = verified as AgentMailWebhookPayload;
478
494
 
@@ -602,8 +618,7 @@ export async function handleWebhooks(
602
618
  console.log(`[Kapso] Skipping event: ${routing.reason}`);
603
619
  break;
604
620
  }
605
- res.writeHead(200, { "Content-Type": "application/json" });
606
- res.end(JSON.stringify({ received: true, routing: routing.kind }));
621
+ kapsoWebhook.respond(res, 200, { received: true, routing: routing.kind });
607
622
  } catch (err) {
608
623
  // Never fail the delivery on a downstream dispatch error — we already
609
624
  // verified + deduped. Log and ack so Kapso doesn't hammer retries.
@@ -613,8 +628,7 @@ export async function handleWebhooks(
613
628
  } else {
614
629
  console.error(`[Kapso] Error handling inbound event: ${errorMessage}`);
615
630
  }
616
- res.writeHead(200, { "Content-Type": "application/json" });
617
- res.end(JSON.stringify({ received: true, routing: "error" }));
631
+ kapsoWebhook.respond(res, 200, { received: true, routing: "error" });
618
632
  }
619
633
  return true;
620
634
  }
@@ -3,7 +3,7 @@ import { z } from "zod";
3
3
  import { getWorkflowRun } from "../be/db";
4
4
  import { workflowEventBus } from "../workflows/event-bus";
5
5
  import { route } from "./route-def";
6
- import { json, jsonError } from "./utils";
6
+ import { jsonError } from "./utils";
7
7
 
8
8
  // ─── Route Definitions ───────────────────────────────────────────────────────
9
9
 
@@ -30,7 +30,14 @@ const runScopedSignalRoute = route({
30
30
  payload: z.record(z.string(), z.unknown()).optional(),
31
31
  }),
32
32
  responses: {
33
- 200: { description: "Event emitted" },
33
+ 200: {
34
+ description: "Event emitted",
35
+ schema: z.object({
36
+ ok: z.literal(true),
37
+ name: z.string(),
38
+ runId: z.string(),
39
+ }),
40
+ },
34
41
  404: { description: "Workflow run not found" },
35
42
  400: { description: "Validation error" },
36
43
  },
@@ -56,7 +63,13 @@ const globalSignalRoute = route({
56
63
  payload: z.record(z.string(), z.unknown()).optional(),
57
64
  }),
58
65
  responses: {
59
- 200: { description: "Event emitted" },
66
+ 200: {
67
+ description: "Event emitted",
68
+ schema: z.object({
69
+ ok: z.literal(true),
70
+ name: z.string(),
71
+ }),
72
+ },
60
73
  400: { description: "Validation error" },
61
74
  },
62
75
  auth: { apiKey: true },
@@ -84,7 +97,7 @@ export async function handleWorkflowEvents(
84
97
  const payload = { ...(parsed.body.payload ?? {}), _runId: parsed.params.runId };
85
98
  workflowEventBus.emit(parsed.body.name, payload);
86
99
 
87
- json(res, {
100
+ runScopedSignalRoute.respond(res, 200, {
88
101
  ok: true,
89
102
  name: parsed.body.name,
90
103
  runId: parsed.params.runId,
@@ -99,7 +112,7 @@ export async function handleWorkflowEvents(
99
112
 
100
113
  workflowEventBus.emit(parsed.body.name, parsed.body.payload ?? {});
101
114
 
102
- json(res, { ok: true, name: parsed.body.name });
115
+ globalSignalRoute.respond(res, 200, { ok: true, name: parsed.body.name });
103
116
  return true;
104
117
  }
105
118