@desplega.ai/agent-swarm 1.113.0 → 1.115.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/README.md +2 -2
  2. package/dist/{actions-q4n7cz6e.js → actions-17ya5wde.js} +8 -9
  3. package/dist/{app-es4nzc71.js → app-4f718yk0.js} +6 -6
  4. package/dist/{assistant-0x8ey0vs.js → assistant-ah7csnjz.js} +19 -18
  5. package/dist/{boot-reembed-69agkhv6.js → boot-reembed-4jpxhb25.js} +6 -6
  6. package/dist/{boot-reembed-p0ny05t8.js → boot-reembed-qata34xa.js} +5 -5
  7. package/dist/{boot-scrub-logs-yybrkmvr.js → boot-scrub-logs-tv48nn3z.js} +4 -4
  8. package/dist/{claude-adapter-mma9a1yr.js → claude-adapter-3csn5bks.js} +3 -3
  9. package/dist/{claude-managed-adapter-pv2nbqy4.js → claude-managed-adapter-kysmkz89.js} +2 -2
  10. package/dist/cli-0pygq9r1.js +429 -0
  11. package/dist/{cli-hsetkkd3.js → cli-12vb0xg6.js} +33 -9
  12. package/dist/{cli-ncfgsqbd.js → cli-15y7qhzp.js} +262 -33
  13. package/dist/{cli-7ga43spr.js → cli-1yjrbg1f.js} +1 -1
  14. package/dist/{cli-36ymmpmp.js → cli-3samkyap.js} +2 -2
  15. package/dist/cli-52fg7s7x.js +269 -0
  16. package/dist/{cli-hjhvekf4.js → cli-65ekje01.js} +11 -8
  17. package/dist/{cli-h3k622d0.js → cli-6aq8xhvm.js} +90 -3
  18. package/dist/{cli-f3qa069v.js → cli-7xje5amz.js} +1 -1
  19. package/dist/cli-86fdnerz.js +405 -0
  20. package/dist/{cli-9cy6nc5m.js → cli-8fjwym53.js} +1 -1
  21. package/dist/{cli-c3frqf65.js → cli-b23axgz4.js} +3 -3
  22. package/dist/{cli-a0t7vapf.js → cli-bc89my0z.js} +1 -1
  23. package/dist/{cli-bnzbn8j8.js → cli-beea4a3y.js} +73 -24
  24. package/dist/{cli-q21d49ac.js → cli-bw80ck94.js} +1 -1
  25. package/dist/{cli-pwc5e83c.js → cli-cchksf5q.js} +12 -7
  26. package/dist/{cli-30bbaveh.js → cli-cmw9rzy5.js} +2 -2
  27. package/dist/{cli-y6mrptcn.js → cli-cnwssmsr.js} +1 -1
  28. package/dist/{cli-r5g6ngtn.js → cli-dcca6j8m.js} +46 -5
  29. package/dist/{cli-tkmm5nf9.js → cli-e61fs2hp.js} +59 -2
  30. package/dist/{cli-6wscj9z0.js → cli-e7306swv.js} +14 -14
  31. package/dist/{cli-dcfyyh3t.js → cli-ftyf58xj.js} +2 -2
  32. package/dist/{cli-4xyj9jtq.js → cli-hrhye2rm.js} +1062 -478
  33. package/dist/cli-jkhwmmgg.js +136 -0
  34. package/dist/{cli-y4ycrnjc.js → cli-k3y4z3h5.js} +1 -1
  35. package/dist/{cli-6xd0bvf8.js → cli-ma04faw5.js} +1 -1
  36. package/dist/{cli-2fc97xm5.js → cli-mnbgv3w7.js} +4 -4
  37. package/dist/{cli-trmapn9k.js → cli-pet3aev6.js} +95 -10
  38. package/dist/{cli-z46pnksz.js → cli-q1rx7g0d.js} +175 -28
  39. package/dist/{cli-qwgtacd6.js → cli-r9f6ngrf.js} +1244 -336
  40. package/dist/{cli-gtkqc144.js → cli-shvaxw27.js} +13 -7
  41. package/dist/{cli-ywez6yps.js → cli-sq7f72d6.js} +1 -1
  42. package/dist/cli-t0vqevnm.js +393 -0
  43. package/dist/{cli-tbw13sx8.js → cli-tece6zgm.js} +1 -1
  44. package/dist/{cli-bgvskydy.js → cli-tffvrh6j.js} +1 -1
  45. package/dist/cli-wh91zby6.js +187222 -0
  46. package/dist/{cli-anrj584m.js → cli-xz9aq0rf.js} +1 -1
  47. package/dist/{cli-mq580e06.js → cli-zsrdty74.js} +18 -18
  48. package/dist/cli.js +32 -15
  49. package/dist/{codex-adapter-5cmy858w.js → codex-adapter-de3w0nch.js} +10 -10
  50. package/dist/{codex-session-runner-46q8mrba.js → codex-session-runner-bnw8cysk.js} +10 -10
  51. package/dist/{commands-z5dbwta3.js → commands-hch4e2k1.js} +4 -4
  52. package/dist/{db-bbgahh4z.js → db-p3h2j06x.js} +14 -4
  53. package/dist/{handlers-6b44317z.js → handlers-9er4zx9q.js} +17 -31
  54. package/dist/{hook-sbp5fmps.js → hook-4axs756z.js} +7 -7
  55. package/dist/{http-2xz43gz3.js → http-809fvtz2.js} +1934 -886
  56. package/dist/{index-wz1ea15p.js → index-2dgfj9av.js} +3 -3
  57. package/dist/{index-nhat35em.js → index-47em3d2n.js} +14 -11
  58. package/dist/{index-5ghxn8s6.js → index-9fqaktjr.js} +12 -9
  59. package/dist/{index-0gzmqj4k.js → index-9ht8gr56.js} +13 -10
  60. package/dist/{index-vfedgdw6.js → index-rm85cfdx.js} +368 -40
  61. package/dist/{index-ykvs43z4.js → index-t2snazez.js} +4 -4
  62. package/dist/{keepalive-62gfj91d.js → keepalive-3cps6e3e.js} +7 -6
  63. package/dist/{lead-ag0akn0v.js → lead-fkk95wgq.js} +36 -32
  64. package/dist/{maintenance-xxrrzk5x.js → maintenance-fweqh0yj.js} +6 -6
  65. package/dist/{mistral-conversations-26yrnbf1.js → mistral-conversations-7gqnz2wt.js} +7 -7
  66. package/dist/oauth-refresh-sweep-sycqtbph.js +114 -0
  67. package/dist/{onboard-64zyfcbs.js → onboard-cy0rkgvx.js} +2 -2
  68. package/dist/{opencode-adapter-mqsxr170.js → opencode-adapter-p6bxnbm4.js} +2 -2
  69. package/dist/{otel-zjd9zaxs.js → otel-g299k41b.js} +2 -2
  70. package/dist/{otel-impl-b86qseaa.js → otel-impl-wbns8hf7.js} +1 -1
  71. package/dist/{pi-mono-adapter-kxjxpfgy.js → pi-mono-adapter-05sck1ds.js} +17 -109
  72. package/dist/{pricing-refresh-cgtkffpa.js → pricing-refresh-0hvazmkf.js} +6 -6
  73. package/dist/rbac-roles-kfesjy9y.js +28 -0
  74. package/dist/rbac-roles-mwcwhbxc.js +283 -0
  75. package/dist/{seed-pricing-1r1y1xkq.js → seed-pricing-h2kdq4cq.js} +5 -5
  76. package/dist/{setup-mkcgkjf7.js → setup-8506q97w.js} +2 -2
  77. package/dist/{worker-351sze63.js → worker-4r5c6zyb.js} +36 -32
  78. package/openapi.json +1327 -20
  79. package/package.json +3 -3
  80. package/src/agentmail/handlers.ts +16 -6
  81. package/src/agentmail/templates.ts +25 -5
  82. package/src/be/audit-user.ts +28 -4
  83. package/src/be/db-queries/oauth.ts +42 -0
  84. package/src/be/db.ts +314 -28
  85. package/src/be/identity.ts +79 -0
  86. package/src/be/mcp-proxy.ts +132 -0
  87. package/src/be/migrations/109_rbac_roles.sql +79 -0
  88. package/src/be/migrations/111_oauth_credential_bindings.sql +6 -0
  89. package/src/be/migrations/112_script_connections_graphql.sql +96 -0
  90. package/src/be/migrations/113_task_routing_affinity.sql +9 -0
  91. package/src/be/migrations/114_backfill_gpt_5_6_pricing.sql +15 -0
  92. package/src/be/modelsdev-cache.json +135 -0
  93. package/src/be/oauth-credential-bindings.ts +35 -0
  94. package/src/be/oauth-refresh-sweep.ts +159 -0
  95. package/src/be/rbac-audit.ts +52 -13
  96. package/src/be/rbac-roles.ts +424 -0
  97. package/src/be/script-connections.ts +459 -40
  98. package/src/be/script-credential-broker.ts +29 -3
  99. package/src/be/scripts/typecheck.ts +51 -7
  100. package/src/be/seed-scripts/catalog/catalog-report.inline.ts +325 -24
  101. package/src/be/seed-scripts/catalog/catalog-report.ts +325 -24
  102. package/src/be/users.ts +24 -0
  103. package/src/cli.tsx +17 -0
  104. package/src/github/handlers.ts +10 -9
  105. package/src/github/templates.ts +45 -9
  106. package/src/gitlab/handlers.ts +20 -8
  107. package/src/gitlab/templates.ts +18 -6
  108. package/src/heartbeat/heartbeat.ts +143 -21
  109. package/src/http/all-routes.ts +3 -0
  110. package/src/http/approval-requests.ts +12 -0
  111. package/src/http/config.ts +44 -10
  112. package/src/http/core.ts +30 -3
  113. package/src/http/favorites.ts +1 -5
  114. package/src/http/index.ts +25 -1
  115. package/src/http/mcp-oauth.ts +2 -0
  116. package/src/http/mcp-servers.ts +44 -3
  117. package/src/http/oauth-generic.ts +105 -0
  118. package/src/http/poll.ts +20 -3
  119. package/src/http/script-connection-proxy.ts +116 -0
  120. package/src/http/script-connections.ts +1657 -0
  121. package/src/http/scripts.ts +27 -7
  122. package/src/http/skills.ts +11 -0
  123. package/src/http/tasks.ts +27 -13
  124. package/src/http/x.ts +6 -2
  125. package/src/integrations/kapso/inbound.ts +23 -6
  126. package/src/jira/sync.ts +111 -17
  127. package/src/jira/templates.ts +10 -2
  128. package/src/mcp-client/http-client.ts +194 -0
  129. package/src/oauth/app-validation.ts +16 -0
  130. package/src/oauth/ensure-token.ts +69 -14
  131. package/src/oauth/mcp-wrapper.ts +15 -0
  132. package/src/oauth/wrapper.ts +48 -20
  133. package/src/prompts/session-templates.ts +3 -3
  134. package/src/providers/codex-models.ts +100 -30
  135. package/src/providers/modelsdev-reasoning.json +51 -0
  136. package/src/providers/pi-mono-mcp-client.ts +2 -133
  137. package/src/providers/reasoning-effort.ts +11 -5
  138. package/src/rbac/admission.ts +68 -0
  139. package/src/rbac/index.ts +2 -0
  140. package/src/rbac/legacy-policy.ts +12 -0
  141. package/src/rbac/permissions.ts +48 -0
  142. package/src/scheduler/scheduler.ts +9 -1
  143. package/src/scripts-runtime/api-client.ts +151 -19
  144. package/src/scripts-runtime/api-types.ts +38 -6
  145. package/src/scripts-runtime/credential-broker/default-bindings.ts +1 -0
  146. package/src/scripts-runtime/credential-broker/types.ts +8 -0
  147. package/src/scripts-runtime/ctx.ts +11 -1
  148. package/src/scripts-runtime/eval-harness.ts +5 -1
  149. package/src/scripts-runtime/executors/types.ts +2 -1
  150. package/src/scripts-runtime/loader.ts +3 -1
  151. package/src/scripts-runtime/mcp-client.ts +87 -0
  152. package/src/scripts-runtime/sdk-allowlist.ts +1 -0
  153. package/src/scripts-runtime/types/stdlib.d.ts +6 -0
  154. package/src/scripts-runtime/types/swarm-sdk.d.ts +19 -0
  155. package/src/server-user.ts +114 -59
  156. package/src/server.ts +12 -0
  157. package/src/slack/assistant.ts +14 -6
  158. package/src/slack/enrich.ts +17 -0
  159. package/src/slack/handlers.ts +8 -22
  160. package/src/slack/responses.ts +91 -1
  161. package/src/slack/thread-buffer.ts +6 -3
  162. package/src/slack/watcher.ts +9 -6
  163. package/src/tasks/worker-follow-up.ts +100 -3
  164. package/src/tests/agentmail-handlers.test.ts +32 -3
  165. package/src/tests/approval-requests.test.ts +61 -0
  166. package/src/tests/audit-user.test.ts +47 -1
  167. package/src/tests/codex-adapter.test.ts +46 -23
  168. package/src/tests/credential-broker.test.ts +5 -2
  169. package/src/tests/delete-page-tool.test.ts +197 -0
  170. package/src/tests/github-handlers.test.ts +28 -2
  171. package/src/tests/gitlab-handlers.test.ts +125 -0
  172. package/src/tests/heartbeat.test.ts +48 -3
  173. package/src/tests/identity.test.ts +117 -0
  174. package/src/tests/jira-sync.test.ts +99 -1
  175. package/src/tests/kapso-inbound.test.ts +7 -0
  176. package/src/tests/mcp-tools-user.test.ts +60 -6
  177. package/src/tests/model-control.test.ts +8 -2
  178. package/src/tests/oauth-credential-bindings.test.ts +490 -0
  179. package/src/tests/oauth-refresh-sweep.test.ts +160 -0
  180. package/src/tests/pool-affinity.test.ts +401 -0
  181. package/src/tests/prompt-template-remaining.test.ts +2 -2
  182. package/src/tests/rbac-admission-e2e.test.ts +386 -0
  183. package/src/tests/rbac-admission.test.ts +492 -0
  184. package/src/tests/rbac-e2e-helpers.ts +3 -0
  185. package/src/tests/rbac-engine.test.ts +50 -0
  186. package/src/tests/rbac-mcp-admission-e2e.test.ts +362 -0
  187. package/src/tests/rbac-roles.test.ts +328 -0
  188. package/src/tests/reasoning-effort.test.ts +15 -2
  189. package/src/tests/schedule-target-type.test.ts +50 -0
  190. package/src/tests/scheduled-tasks.test.ts +40 -0
  191. package/src/tests/script-apis-mcp.test.ts +37 -13
  192. package/src/tests/script-connections-http.test.ts +1158 -0
  193. package/src/tests/script-connections-mcp.test.ts +894 -0
  194. package/src/tests/script-connections.test.ts +754 -6
  195. package/src/tests/scripts-external-api.test.ts +49 -1
  196. package/src/tests/scripts-runtime-secret-egress.test.ts +1 -0
  197. package/src/tests/scripts-runtime.test.ts +291 -0
  198. package/src/tests/seed-scripts.test.ts +36 -0
  199. package/src/tests/slack-identity-resolution.test.ts +37 -3
  200. package/src/tests/slack-inline-output.test.ts +65 -0
  201. package/src/tests/slack-thread-buffer.test.ts +28 -0
  202. package/src/tests/tool-annotations.test.ts +1 -0
  203. package/src/tests/user-token-rest-auth.test.ts +140 -1
  204. package/src/tests/workflow-swarm-script.test.ts +44 -0
  205. package/src/tests/workflow-triggers-v2.test.ts +300 -1
  206. package/src/tools/credential-bindings/tool.ts +277 -18
  207. package/src/tools/delete-page.ts +145 -0
  208. package/src/tools/mcp-servers/mcp-server-delete.ts +7 -4
  209. package/src/tools/request-human-input.ts +2 -0
  210. package/src/tools/resolve-user.ts +77 -27
  211. package/src/tools/script-apis.ts +62 -3
  212. package/src/tools/script-connections/tool.ts +275 -31
  213. package/src/tools/send-task.ts +15 -1
  214. package/src/tools/task-action.ts +34 -2
  215. package/src/tools/templates.ts +53 -1
  216. package/src/tools/tool-config.ts +2 -1
  217. package/src/tools/utils.ts +2 -0
  218. package/src/tools/workflows/create-workflow.ts +8 -1
  219. package/src/tools/workflows/update-workflow.ts +11 -2
  220. package/src/types.ts +90 -11
  221. package/src/utils/scoped-resource.ts +25 -0
  222. package/src/workflows/executors/human-in-the-loop.ts +1 -0
  223. package/src/workflows/executors/swarm-script.ts +7 -1
  224. package/src/workflows/triggers.ts +146 -18
  225. package/templates/skills/swarm-scripts/content.md +33 -8
  226. package/dist/cli-bgef578c.js +0 -771
  227. package/dist/cli-c9mtm09b.js +0 -43
  228. package/dist/cli-ceewqajw.js +0 -1698
  229. package/dist/{anthropic-messages-8nc6r2dw.js → anthropic-messages-kq3pjhbp.js} +6 -6
  230. package/dist/{azure-openai-responses-6yfzw3qw.js → azure-openai-responses-gm1ejtmn.js} +7 -7
  231. package/dist/{cli-vbraamxr.js → cli-6b4wwmm8.js} +9 -9
  232. package/dist/{cli-2r7s8jhp.js → cli-sznberje.js} +3 -3
  233. package/dist/{google-generative-ai-vfq6ymz9.js → google-generative-ai-hyrra2jm.js} +3 -3
  234. package/dist/{google-vertex-mj5y5rfj.js → google-vertex-8bs2pzgn.js} +3 -3
  235. package/dist/{openai-codex-responses-8h6kvfv8.js → openai-codex-responses-3nkvje79.js} +4 -4
  236. package/dist/{openai-completions-x6kjnqjd.js → openai-completions-0asftkwg.js} +11 -11
  237. package/dist/{openai-responses-7jwyz3ds.js → openai-responses-pmatek45.js} +10 -10
@@ -6,7 +6,7 @@ import {
6
6
  type Server,
7
7
  type ServerResponse,
8
8
  } from "node:http";
9
- import { closeDb, createAgent, createUser, getDb, initDb } from "../be/db";
9
+ import { closeDb, createAgent, createTaskExtended, createUser, getDb, initDb } from "../be/db";
10
10
  import { type IdentityActor, mintToken, revokeToken } from "../be/users";
11
11
  import { handleCore } from "../http/core";
12
12
  import { handleTasks } from "../http/tasks";
@@ -110,6 +110,145 @@ describe("normal REST API user-bound token auth", () => {
110
110
  expect(body.requestedByUserId).toBeUndefined();
111
111
  });
112
112
 
113
+ test("global API key caller cannot spoof requestedByUserId via body — falls back to owned task context", async () => {
114
+ const legitRequester = createUser({ name: "Legit Requester" });
115
+ const attacker = createUser({ name: "Attacker" });
116
+ const agent = createAgent({ name: "spoof-test-agent", isLead: false, status: "idle" });
117
+ const ownedTask = createTaskExtended("owned task for spoof test", {
118
+ agentId: agent.id,
119
+ requestedByUserId: legitRequester.id,
120
+ });
121
+
122
+ const res = await fetch(`http://localhost:${port}/api/tasks`, {
123
+ method: "POST",
124
+ headers: {
125
+ Authorization: `Bearer ${API_KEY}`,
126
+ "Content-Type": "application/json",
127
+ "x-agent-id": agent.id,
128
+ "x-source-task-id": ownedTask.id,
129
+ },
130
+ body: JSON.stringify({
131
+ task: "created through global key with spoofed requestedByUserId",
132
+ requestedByUserId: attacker.id,
133
+ }),
134
+ });
135
+
136
+ expect(res.status).toBe(201);
137
+ const body = (await res.json()) as { id: string; requestedByUserId?: string };
138
+ expect(body.requestedByUserId).toBe(legitRequester.id);
139
+ expect(body.requestedByUserId).not.toBe(attacker.id);
140
+ });
141
+
142
+ test("global API key caller + body requestedByUserId, flag OFF (default) → stays unattributed, body ignored", async () => {
143
+ // Default posture: the body fallback is off, so an operator/global-key
144
+ // caller cannot spoof attribution via the request body. This is the
145
+ // anti-spoofing behavior upstream #939 introduced.
146
+ expect(process.env.TRUST_BODY_REQUESTED_BY_USER_ID).toBeUndefined();
147
+ const someUser = createUser({ name: "Some User (flag off)" });
148
+
149
+ const res = await fetch(`http://localhost:${port}/api/tasks`, {
150
+ method: "POST",
151
+ headers: {
152
+ Authorization: `Bearer ${API_KEY}`,
153
+ "Content-Type": "application/json",
154
+ },
155
+ body: JSON.stringify({
156
+ task: "created from the UI session view, flag off",
157
+ requestedByUserId: someUser.id,
158
+ }),
159
+ });
160
+
161
+ expect(res.status).toBe(201);
162
+ const body = (await res.json()) as { id: string; requestedByUserId?: string };
163
+ expect(body.requestedByUserId).toBeUndefined();
164
+ });
165
+
166
+ describe("TRUST_BODY_REQUESTED_BY_USER_ID=true (opt-in for shared-key deployments)", () => {
167
+ beforeAll(() => {
168
+ process.env.TRUST_BODY_REQUESTED_BY_USER_ID = "true";
169
+ });
170
+
171
+ afterAll(() => {
172
+ delete process.env.TRUST_BODY_REQUESTED_BY_USER_ID;
173
+ });
174
+
175
+ test("global API key caller + valid body requestedByUserId (no owned task context) → attributed to that user", async () => {
176
+ // Fork-specific opt-in: this org's UI shares one operator key across
177
+ // all users, so there is no ownership-gated task context to fall back
178
+ // to — the body-supplied id is the only signal available, and it is
179
+ // trusted once validated against a real user row, but only when the
180
+ // deployment has explicitly opted into this flag.
181
+ const uiUser = createUser({ name: "UI Picker User" });
182
+
183
+ const res = await fetch(`http://localhost:${port}/api/tasks`, {
184
+ method: "POST",
185
+ headers: {
186
+ Authorization: `Bearer ${API_KEY}`,
187
+ "Content-Type": "application/json",
188
+ },
189
+ body: JSON.stringify({
190
+ task: "created from the UI session view",
191
+ requestedByUserId: uiUser.id,
192
+ }),
193
+ });
194
+
195
+ expect(res.status).toBe(201);
196
+ const body = (await res.json()) as { id: string; requestedByUserId?: string };
197
+ expect(body.requestedByUserId).toBe(uiUser.id);
198
+ });
199
+
200
+ test("global API key caller + bogus body requestedByUserId → stays unattributed (NULL), not a crash", async () => {
201
+ const res = await fetch(`http://localhost:${port}/api/tasks`, {
202
+ method: "POST",
203
+ headers: {
204
+ Authorization: `Bearer ${API_KEY}`,
205
+ "Content-Type": "application/json",
206
+ },
207
+ body: JSON.stringify({
208
+ task: "created with a nonexistent requestedByUserId",
209
+ requestedByUserId: "does-not-exist",
210
+ }),
211
+ });
212
+
213
+ expect(res.status).toBe(201);
214
+ const body = (await res.json()) as { id: string; requestedByUserId?: string };
215
+ expect(body.requestedByUserId).toBeUndefined();
216
+ });
217
+
218
+ test("owned task context still takes precedence over body even when flag is on", async () => {
219
+ const legitRequester = createUser({ name: "Legit Requester (flag on)" });
220
+ const attacker = createUser({ name: "Attacker (flag on)" });
221
+ const agent = createAgent({
222
+ name: "spoof-test-agent-flag-on",
223
+ isLead: false,
224
+ status: "idle",
225
+ });
226
+ const ownedTask = createTaskExtended("owned task for spoof test, flag on", {
227
+ agentId: agent.id,
228
+ requestedByUserId: legitRequester.id,
229
+ });
230
+
231
+ const res = await fetch(`http://localhost:${port}/api/tasks`, {
232
+ method: "POST",
233
+ headers: {
234
+ Authorization: `Bearer ${API_KEY}`,
235
+ "Content-Type": "application/json",
236
+ "x-agent-id": agent.id,
237
+ "x-source-task-id": ownedTask.id,
238
+ },
239
+ body: JSON.stringify({
240
+ task: "created through global key with spoofed requestedByUserId, flag on",
241
+ requestedByUserId: attacker.id,
242
+ }),
243
+ });
244
+
245
+ expect(res.status).toBe(201);
246
+ const body = (await res.json()) as { id: string; requestedByUserId?: string };
247
+ expect(body.requestedByUserId).toBe(legitRequester.id);
248
+ expect(body.requestedByUserId).not.toBe(attacker.id);
249
+ });
250
+ });
251
+
113
252
  test("revoked user token is unauthorized for normal API", async () => {
114
253
  const user = createUser({ name: "Revoked REST User" });
115
254
  const { tokenId, plaintext } = mintToken(user.id, "revoked", ACTOR);
@@ -10,6 +10,7 @@ import {
10
10
  getWorkflowRunStepsByRunId,
11
11
  initDb,
12
12
  } from "../be/db";
13
+ import { upsertScriptConnection } from "../be/script-connections";
13
14
  import { upsertScriptByName } from "../be/scripts/db";
14
15
  import { setScriptEmbeddingProviderForTests } from "../be/scripts/embeddings";
15
16
  import type { Workflow, WorkflowDefinition } from "../types";
@@ -143,6 +144,7 @@ beforeEach(() => {
143
144
  getDb().run("DELETE FROM workflow_run_steps");
144
145
  getDb().run("DELETE FROM workflow_runs");
145
146
  getDb().run("DELETE FROM scripts");
147
+ getDb().run("DELETE FROM script_connections");
146
148
  getDb().run("DELETE FROM workflows");
147
149
  });
148
150
 
@@ -204,6 +206,48 @@ describe("SwarmScriptExecutor", () => {
204
206
  expect(result.output?.scriptName).toBe("add-one");
205
207
  });
206
208
 
209
+ test("swarm-script executor includes API connection descriptors in ctx.api", async () => {
210
+ await upsertScriptConnection({
211
+ slug: "workflowVendor",
212
+ kind: "openapi",
213
+ scope: "global",
214
+ baseUrl: "https://api.workflow-vendor.test",
215
+ openapiSpecJson: JSON.stringify({
216
+ openapi: "3.0.0",
217
+ info: { title: "WorkflowVendor", version: "1.0.0" },
218
+ paths: {
219
+ "/items": {
220
+ get: {
221
+ operationId: "listItems",
222
+ responses: { "200": { description: "ok" } },
223
+ },
224
+ },
225
+ },
226
+ }),
227
+ });
228
+ await saveScript(
229
+ "ctx-api-keys",
230
+ `export default async (_args, ctx) => ({ apiKeys: Object.keys(ctx.api).sort() });`,
231
+ );
232
+
233
+ const executor = new SwarmScriptExecutor(deps);
234
+ const wf = makeWorkflow({ nodes: [] });
235
+ const result = await executor.run({
236
+ config: { scriptName: "ctx-api-keys" },
237
+ context: {},
238
+ meta: {
239
+ runId: crypto.randomUUID(),
240
+ stepId: crypto.randomUUID(),
241
+ nodeId: "script",
242
+ workflowId: wf.id,
243
+ dryRun: false,
244
+ },
245
+ });
246
+
247
+ expect(result.status).toBe("success");
248
+ expect(result.output?.result).toEqual({ apiKeys: ["workflowVendor"] });
249
+ });
250
+
207
251
  test("pinHash correctly resolves to a historic script_versions row", async () => {
208
252
  const first = await saveScript("versioned", `export default async () => ({ version: "old" });`);
209
253
  await saveScript("versioned", `export default async () => ({ version: "new" });`);
@@ -11,10 +11,17 @@ import {
11
11
  upsertSwarmConfig,
12
12
  } from "../be/db";
13
13
  import type { Workflow } from "../types";
14
+ import { TriggerConfigSchema } from "../types";
14
15
  import { startWorkflowExecution } from "../workflows/engine";
15
16
  import { BaseExecutor, type ExecutorResult } from "../workflows/executors/base";
16
17
  import { ExecutorRegistry } from "../workflows/executors/registry";
17
- import { handleWebhookTrigger, verifyHmacSignature, WebhookError } from "../workflows/triggers";
18
+ import {
19
+ handleWebhookTrigger,
20
+ verifyHmacSignature,
21
+ verifyTimestampedHmacSignature,
22
+ verifyTokenEquality,
23
+ WebhookError,
24
+ } from "../workflows/triggers";
18
25
 
19
26
  const TEST_DB_PATH = "./test-workflow-triggers-v2.sqlite";
20
27
 
@@ -111,6 +118,93 @@ describe("verifyHmacSignature", () => {
111
118
  });
112
119
  });
113
120
 
121
+ describe("verifyTimestampedHmacSignature", () => {
122
+ const secret = "timestamped-secret";
123
+ const body = '{"event":"finding.triage_completed"}';
124
+ const timestamp = 1_700_000_000;
125
+ const nowMs = timestamp * 1000;
126
+
127
+ function sign(ts: number, signingSecret = secret): string {
128
+ return crypto.createHmac("sha256", signingSecret).update(`${ts}.${body}`).digest("hex");
129
+ }
130
+
131
+ test("valid timestamped signature passes", () => {
132
+ const header = `t=${timestamp},v1=${sign(timestamp)}`;
133
+
134
+ expect(verifyTimestampedHmacSignature(secret, body, header, {}, nowMs)).toBe(true);
135
+ });
136
+
137
+ test("wrong secret fails", () => {
138
+ const header = `t=${timestamp},v1=${sign(timestamp, "wrong-secret")}`;
139
+
140
+ expect(verifyTimestampedHmacSignature(secret, body, header, {}, nowMs)).toBe(false);
141
+ });
142
+
143
+ test("expired timestamp fails", () => {
144
+ const oldTimestamp = timestamp - 301;
145
+ const header = `t=${oldTimestamp},v1=${sign(oldTimestamp)}`;
146
+
147
+ expect(verifyTimestampedHmacSignature(secret, body, header, {}, nowMs)).toBe(false);
148
+ });
149
+
150
+ test("future timestamp beyond tolerance fails", () => {
151
+ const futureTimestamp = timestamp + 301;
152
+ const header = `t=${futureTimestamp},v1=${sign(futureTimestamp)}`;
153
+
154
+ expect(verifyTimestampedHmacSignature(secret, body, header, {}, nowMs)).toBe(false);
155
+ });
156
+
157
+ test("missing or garbled timestamp fails", () => {
158
+ expect(verifyTimestampedHmacSignature(secret, body, `v1=${sign(timestamp)}`, {}, nowMs)).toBe(
159
+ false,
160
+ );
161
+ expect(
162
+ verifyTimestampedHmacSignature(
163
+ secret,
164
+ body,
165
+ `t=not-a-number,v1=${sign(timestamp)}`,
166
+ {},
167
+ nowMs,
168
+ ),
169
+ ).toBe(false);
170
+ });
171
+
172
+ test("multiple signature entries pass when any one matches", () => {
173
+ const header = `t=${timestamp},v1=deadbeef,v1=${sign(timestamp)}`;
174
+
175
+ expect(verifyTimestampedHmacSignature(secret, body, header, {}, nowMs)).toBe(true);
176
+ });
177
+
178
+ test("custom timestamp and signature keys are supported", () => {
179
+ const header = `ts=${timestamp},sig=${sign(timestamp)}`;
180
+
181
+ expect(
182
+ verifyTimestampedHmacSignature(
183
+ secret,
184
+ body,
185
+ header,
186
+ { timestampKey: "ts", signatureKey: "sig", toleranceSeconds: 300 },
187
+ nowMs,
188
+ ),
189
+ ).toBe(true);
190
+ });
191
+ });
192
+
193
+ describe("verifyTokenEquality", () => {
194
+ test("matching token passes", () => {
195
+ expect(verifyTokenEquality("shared-token", "shared-token")).toBe(true);
196
+ });
197
+
198
+ test("same-length non-matching token fails", () => {
199
+ expect(verifyTokenEquality("shared-token", "shared-tokem")).toBe(false);
200
+ });
201
+
202
+ test("wrong-length non-matching token fails without throwing", () => {
203
+ expect(() => verifyTokenEquality("shared-token", "short")).not.toThrow();
204
+ expect(verifyTokenEquality("shared-token", "short")).toBe(false);
205
+ });
206
+ });
207
+
114
208
  // ─── Webhook Trigger ────────────────────────────────────────
115
209
 
116
210
  describe("handleWebhookTrigger", () => {
@@ -312,6 +406,179 @@ describe("handleWebhookTrigger — custom hmacHeader", () => {
312
406
 
313
407
  expect(result.runId).toBeDefined();
314
408
  });
409
+
410
+ test("explicit hmac-sha256 verification does not use fallback headers", async () => {
411
+ const secret = "explicit-hmac-secret";
412
+ const workflow = makeWorkflow({
413
+ triggers: [
414
+ {
415
+ type: "webhook",
416
+ hmacSecret: secret,
417
+ verification: { format: "hmac-sha256", header: "X-Primary-Signature" },
418
+ },
419
+ ],
420
+ });
421
+
422
+ const body = '{"event":"explicit"}';
423
+ try {
424
+ await handleWebhookTrigger(
425
+ workflow.id,
426
+ body,
427
+ { "x-signature": signRaw(secret, body) },
428
+ registry,
429
+ );
430
+ expect(true).toBe(false);
431
+ } catch (err) {
432
+ expect(err).toBeInstanceOf(WebhookError);
433
+ expect((err as WebhookError).statusCode).toBe(401);
434
+ }
435
+ });
436
+ });
437
+
438
+ describe("handleWebhookTrigger — verification formats", () => {
439
+ function signTimestamped(secret: string, body: string, timestamp: number): string {
440
+ return crypto.createHmac("sha256", secret).update(`${timestamp}.${body}`).digest("hex");
441
+ }
442
+
443
+ test("timestamped-hmac-sha256 starts workflow for Superagent-shaped request", async () => {
444
+ const secret = "superagent-secret";
445
+ const timestamp = Math.floor(Date.now() / 1000);
446
+ const workflow = makeWorkflow({
447
+ triggers: [
448
+ {
449
+ type: "webhook",
450
+ hmacSecret: secret,
451
+ verification: {
452
+ format: "timestamped-hmac-sha256",
453
+ header: "X-Superagent-Signature",
454
+ toleranceSeconds: 300,
455
+ },
456
+ },
457
+ ],
458
+ });
459
+
460
+ const body = '{"type":"finding.triage_completed","finding":{"id":"finding-123"}}';
461
+ const result = await handleWebhookTrigger(
462
+ workflow.id,
463
+ body,
464
+ {
465
+ "x-superagent-signature": `t=${timestamp},v1=${signTimestamped(secret, body, timestamp)}`,
466
+ },
467
+ registry,
468
+ );
469
+
470
+ expect(result.runId).toBeDefined();
471
+ const run = getWorkflowRun(result.runId);
472
+ expect(run).not.toBeNull();
473
+ expect(run!.triggerData).toEqual({
474
+ type: "finding.triage_completed",
475
+ finding: { id: "finding-123" },
476
+ });
477
+ });
478
+
479
+ test("timestamped-hmac-sha256 rejects signatures on fallback headers", async () => {
480
+ const secret = "timestamped-no-fallback-secret";
481
+ const timestamp = Math.floor(Date.now() / 1000);
482
+ const workflow = makeWorkflow({
483
+ triggers: [
484
+ {
485
+ type: "webhook",
486
+ hmacSecret: secret,
487
+ verification: {
488
+ format: "timestamped-hmac-sha256",
489
+ header: "X-Superagent-Signature",
490
+ },
491
+ },
492
+ ],
493
+ });
494
+
495
+ const body = '{"event":"fallback-blocked"}';
496
+ try {
497
+ await handleWebhookTrigger(
498
+ workflow.id,
499
+ body,
500
+ { "x-signature": `t=${timestamp},v1=${signTimestamped(secret, body, timestamp)}` },
501
+ registry,
502
+ );
503
+ expect(true).toBe(false);
504
+ } catch (err) {
505
+ expect(err).toBeInstanceOf(WebhookError);
506
+ expect((err as WebhookError).statusCode).toBe(401);
507
+ }
508
+ });
509
+
510
+ test("token-equality starts workflow when shared token matches", async () => {
511
+ const workflow = makeWorkflow({
512
+ triggers: [
513
+ {
514
+ type: "webhook",
515
+ hmacSecret: "gitlab-token",
516
+ verification: { format: "token-equality", header: "X-Gitlab-Token" },
517
+ },
518
+ ],
519
+ });
520
+
521
+ const result = await handleWebhookTrigger(
522
+ workflow.id,
523
+ '{"event":"push"}',
524
+ { "x-gitlab-token": "gitlab-token" },
525
+ registry,
526
+ );
527
+
528
+ expect(result.runId).toBeDefined();
529
+ });
530
+
531
+ test("token-equality rejects a wrong token", async () => {
532
+ const workflow = makeWorkflow({
533
+ triggers: [
534
+ {
535
+ type: "webhook",
536
+ hmacSecret: "gitlab-token",
537
+ verification: { format: "token-equality", header: "X-Gitlab-Token" },
538
+ },
539
+ ],
540
+ });
541
+
542
+ try {
543
+ await handleWebhookTrigger(
544
+ workflow.id,
545
+ '{"event":"push"}',
546
+ { "x-gitlab-token": "wrong-token" },
547
+ registry,
548
+ );
549
+ expect(true).toBe(false);
550
+ } catch (err) {
551
+ expect(err).toBeInstanceOf(WebhookError);
552
+ expect((err as WebhookError).statusCode).toBe(401);
553
+ }
554
+ });
555
+
556
+ test("verification configured without hmacSecret fails closed instead of accepting the request", async () => {
557
+ // Bypasses the create/update Zod schema (which now also rejects this shape) to
558
+ // exercise the runtime guard directly — e.g. for data written before this fix,
559
+ // or any future write path that skips schema validation.
560
+ const workflow = makeWorkflow({
561
+ triggers: [
562
+ {
563
+ type: "webhook",
564
+ verification: { format: "token-equality", header: "X-Gitlab-Token" },
565
+ },
566
+ ],
567
+ });
568
+
569
+ try {
570
+ await handleWebhookTrigger(
571
+ workflow.id,
572
+ '{"event":"push"}',
573
+ { "x-gitlab-token": "anything" },
574
+ registry,
575
+ );
576
+ expect(true).toBe(false);
577
+ } catch (err) {
578
+ expect(err).toBeInstanceOf(WebhookError);
579
+ expect((err as WebhookError).statusCode).toBe(500);
580
+ }
581
+ });
315
582
  });
316
583
 
317
584
  describe("handleWebhookTrigger — hmacSecret references", () => {
@@ -503,3 +770,35 @@ describe("cooldown", () => {
503
770
  expect(run2!.status).toBe("completed");
504
771
  });
505
772
  });
773
+
774
+ // ─── TriggerConfigSchema validation ──────────────────────────
775
+
776
+ describe("TriggerConfigSchema", () => {
777
+ test("rejects verification configured without hmacSecret", () => {
778
+ const result = TriggerConfigSchema.safeParse({
779
+ type: "webhook",
780
+ verification: { format: "token-equality", header: "X-Gitlab-Token" },
781
+ });
782
+
783
+ expect(result.success).toBe(false);
784
+ if (!result.success) {
785
+ expect(result.error.issues.some((issue) => issue.path.includes("hmacSecret"))).toBe(true);
786
+ }
787
+ });
788
+
789
+ test("accepts verification configured with hmacSecret", () => {
790
+ const result = TriggerConfigSchema.safeParse({
791
+ type: "webhook",
792
+ hmacSecret: "gitlab-token",
793
+ verification: { format: "token-equality", header: "X-Gitlab-Token" },
794
+ });
795
+
796
+ expect(result.success).toBe(true);
797
+ });
798
+
799
+ test("accepts a webhook trigger with neither hmacSecret nor verification (intentionally unauthenticated)", () => {
800
+ const result = TriggerConfigSchema.safeParse({ type: "webhook" });
801
+
802
+ expect(result.success).toBe(true);
803
+ });
804
+ });