@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.
- package/README.md +2 -2
- package/dist/{actions-q4n7cz6e.js → actions-17ya5wde.js} +8 -9
- package/dist/{app-es4nzc71.js → app-4f718yk0.js} +6 -6
- package/dist/{assistant-0x8ey0vs.js → assistant-ah7csnjz.js} +19 -18
- package/dist/{boot-reembed-69agkhv6.js → boot-reembed-4jpxhb25.js} +6 -6
- package/dist/{boot-reembed-p0ny05t8.js → boot-reembed-qata34xa.js} +5 -5
- package/dist/{boot-scrub-logs-yybrkmvr.js → boot-scrub-logs-tv48nn3z.js} +4 -4
- package/dist/{claude-adapter-mma9a1yr.js → claude-adapter-3csn5bks.js} +3 -3
- package/dist/{claude-managed-adapter-pv2nbqy4.js → claude-managed-adapter-kysmkz89.js} +2 -2
- package/dist/cli-0pygq9r1.js +429 -0
- package/dist/{cli-hsetkkd3.js → cli-12vb0xg6.js} +33 -9
- package/dist/{cli-ncfgsqbd.js → cli-15y7qhzp.js} +262 -33
- package/dist/{cli-7ga43spr.js → cli-1yjrbg1f.js} +1 -1
- package/dist/{cli-36ymmpmp.js → cli-3samkyap.js} +2 -2
- package/dist/cli-52fg7s7x.js +269 -0
- package/dist/{cli-hjhvekf4.js → cli-65ekje01.js} +11 -8
- package/dist/{cli-h3k622d0.js → cli-6aq8xhvm.js} +90 -3
- package/dist/{cli-f3qa069v.js → cli-7xje5amz.js} +1 -1
- package/dist/cli-86fdnerz.js +405 -0
- package/dist/{cli-9cy6nc5m.js → cli-8fjwym53.js} +1 -1
- package/dist/{cli-c3frqf65.js → cli-b23axgz4.js} +3 -3
- package/dist/{cli-a0t7vapf.js → cli-bc89my0z.js} +1 -1
- package/dist/{cli-bnzbn8j8.js → cli-beea4a3y.js} +73 -24
- package/dist/{cli-q21d49ac.js → cli-bw80ck94.js} +1 -1
- package/dist/{cli-pwc5e83c.js → cli-cchksf5q.js} +12 -7
- package/dist/{cli-30bbaveh.js → cli-cmw9rzy5.js} +2 -2
- package/dist/{cli-y6mrptcn.js → cli-cnwssmsr.js} +1 -1
- package/dist/{cli-r5g6ngtn.js → cli-dcca6j8m.js} +46 -5
- package/dist/{cli-tkmm5nf9.js → cli-e61fs2hp.js} +59 -2
- package/dist/{cli-6wscj9z0.js → cli-e7306swv.js} +14 -14
- package/dist/{cli-dcfyyh3t.js → cli-ftyf58xj.js} +2 -2
- package/dist/{cli-4xyj9jtq.js → cli-hrhye2rm.js} +1062 -478
- package/dist/cli-jkhwmmgg.js +136 -0
- package/dist/{cli-y4ycrnjc.js → cli-k3y4z3h5.js} +1 -1
- package/dist/{cli-6xd0bvf8.js → cli-ma04faw5.js} +1 -1
- package/dist/{cli-2fc97xm5.js → cli-mnbgv3w7.js} +4 -4
- package/dist/{cli-trmapn9k.js → cli-pet3aev6.js} +95 -10
- package/dist/{cli-z46pnksz.js → cli-q1rx7g0d.js} +175 -28
- package/dist/{cli-qwgtacd6.js → cli-r9f6ngrf.js} +1244 -336
- package/dist/{cli-gtkqc144.js → cli-shvaxw27.js} +13 -7
- package/dist/{cli-ywez6yps.js → cli-sq7f72d6.js} +1 -1
- package/dist/cli-t0vqevnm.js +393 -0
- package/dist/{cli-tbw13sx8.js → cli-tece6zgm.js} +1 -1
- package/dist/{cli-bgvskydy.js → cli-tffvrh6j.js} +1 -1
- package/dist/cli-wh91zby6.js +187222 -0
- package/dist/{cli-anrj584m.js → cli-xz9aq0rf.js} +1 -1
- package/dist/{cli-mq580e06.js → cli-zsrdty74.js} +18 -18
- package/dist/cli.js +32 -15
- package/dist/{codex-adapter-5cmy858w.js → codex-adapter-de3w0nch.js} +10 -10
- package/dist/{codex-session-runner-46q8mrba.js → codex-session-runner-bnw8cysk.js} +10 -10
- package/dist/{commands-z5dbwta3.js → commands-hch4e2k1.js} +4 -4
- package/dist/{db-bbgahh4z.js → db-p3h2j06x.js} +14 -4
- package/dist/{handlers-6b44317z.js → handlers-9er4zx9q.js} +17 -31
- package/dist/{hook-sbp5fmps.js → hook-4axs756z.js} +7 -7
- package/dist/{http-2xz43gz3.js → http-809fvtz2.js} +1934 -886
- package/dist/{index-wz1ea15p.js → index-2dgfj9av.js} +3 -3
- package/dist/{index-nhat35em.js → index-47em3d2n.js} +14 -11
- package/dist/{index-5ghxn8s6.js → index-9fqaktjr.js} +12 -9
- package/dist/{index-0gzmqj4k.js → index-9ht8gr56.js} +13 -10
- package/dist/{index-vfedgdw6.js → index-rm85cfdx.js} +368 -40
- package/dist/{index-ykvs43z4.js → index-t2snazez.js} +4 -4
- package/dist/{keepalive-62gfj91d.js → keepalive-3cps6e3e.js} +7 -6
- package/dist/{lead-ag0akn0v.js → lead-fkk95wgq.js} +36 -32
- package/dist/{maintenance-xxrrzk5x.js → maintenance-fweqh0yj.js} +6 -6
- package/dist/{mistral-conversations-26yrnbf1.js → mistral-conversations-7gqnz2wt.js} +7 -7
- package/dist/oauth-refresh-sweep-sycqtbph.js +114 -0
- package/dist/{onboard-64zyfcbs.js → onboard-cy0rkgvx.js} +2 -2
- package/dist/{opencode-adapter-mqsxr170.js → opencode-adapter-p6bxnbm4.js} +2 -2
- package/dist/{otel-zjd9zaxs.js → otel-g299k41b.js} +2 -2
- package/dist/{otel-impl-b86qseaa.js → otel-impl-wbns8hf7.js} +1 -1
- package/dist/{pi-mono-adapter-kxjxpfgy.js → pi-mono-adapter-05sck1ds.js} +17 -109
- package/dist/{pricing-refresh-cgtkffpa.js → pricing-refresh-0hvazmkf.js} +6 -6
- package/dist/rbac-roles-kfesjy9y.js +28 -0
- package/dist/rbac-roles-mwcwhbxc.js +283 -0
- package/dist/{seed-pricing-1r1y1xkq.js → seed-pricing-h2kdq4cq.js} +5 -5
- package/dist/{setup-mkcgkjf7.js → setup-8506q97w.js} +2 -2
- package/dist/{worker-351sze63.js → worker-4r5c6zyb.js} +36 -32
- package/openapi.json +1327 -20
- package/package.json +3 -3
- package/src/agentmail/handlers.ts +16 -6
- package/src/agentmail/templates.ts +25 -5
- package/src/be/audit-user.ts +28 -4
- package/src/be/db-queries/oauth.ts +42 -0
- package/src/be/db.ts +314 -28
- package/src/be/identity.ts +79 -0
- package/src/be/mcp-proxy.ts +132 -0
- package/src/be/migrations/109_rbac_roles.sql +79 -0
- package/src/be/migrations/111_oauth_credential_bindings.sql +6 -0
- package/src/be/migrations/112_script_connections_graphql.sql +96 -0
- package/src/be/migrations/113_task_routing_affinity.sql +9 -0
- package/src/be/migrations/114_backfill_gpt_5_6_pricing.sql +15 -0
- package/src/be/modelsdev-cache.json +135 -0
- package/src/be/oauth-credential-bindings.ts +35 -0
- package/src/be/oauth-refresh-sweep.ts +159 -0
- package/src/be/rbac-audit.ts +52 -13
- package/src/be/rbac-roles.ts +424 -0
- package/src/be/script-connections.ts +459 -40
- package/src/be/script-credential-broker.ts +29 -3
- package/src/be/scripts/typecheck.ts +51 -7
- package/src/be/seed-scripts/catalog/catalog-report.inline.ts +325 -24
- package/src/be/seed-scripts/catalog/catalog-report.ts +325 -24
- package/src/be/users.ts +24 -0
- package/src/cli.tsx +17 -0
- package/src/github/handlers.ts +10 -9
- package/src/github/templates.ts +45 -9
- package/src/gitlab/handlers.ts +20 -8
- package/src/gitlab/templates.ts +18 -6
- package/src/heartbeat/heartbeat.ts +143 -21
- package/src/http/all-routes.ts +3 -0
- package/src/http/approval-requests.ts +12 -0
- package/src/http/config.ts +44 -10
- package/src/http/core.ts +30 -3
- package/src/http/favorites.ts +1 -5
- package/src/http/index.ts +25 -1
- package/src/http/mcp-oauth.ts +2 -0
- package/src/http/mcp-servers.ts +44 -3
- package/src/http/oauth-generic.ts +105 -0
- package/src/http/poll.ts +20 -3
- package/src/http/script-connection-proxy.ts +116 -0
- package/src/http/script-connections.ts +1657 -0
- package/src/http/scripts.ts +27 -7
- package/src/http/skills.ts +11 -0
- package/src/http/tasks.ts +27 -13
- package/src/http/x.ts +6 -2
- package/src/integrations/kapso/inbound.ts +23 -6
- package/src/jira/sync.ts +111 -17
- package/src/jira/templates.ts +10 -2
- package/src/mcp-client/http-client.ts +194 -0
- package/src/oauth/app-validation.ts +16 -0
- package/src/oauth/ensure-token.ts +69 -14
- package/src/oauth/mcp-wrapper.ts +15 -0
- package/src/oauth/wrapper.ts +48 -20
- package/src/prompts/session-templates.ts +3 -3
- package/src/providers/codex-models.ts +100 -30
- package/src/providers/modelsdev-reasoning.json +51 -0
- package/src/providers/pi-mono-mcp-client.ts +2 -133
- package/src/providers/reasoning-effort.ts +11 -5
- package/src/rbac/admission.ts +68 -0
- package/src/rbac/index.ts +2 -0
- package/src/rbac/legacy-policy.ts +12 -0
- package/src/rbac/permissions.ts +48 -0
- package/src/scheduler/scheduler.ts +9 -1
- package/src/scripts-runtime/api-client.ts +151 -19
- package/src/scripts-runtime/api-types.ts +38 -6
- package/src/scripts-runtime/credential-broker/default-bindings.ts +1 -0
- package/src/scripts-runtime/credential-broker/types.ts +8 -0
- package/src/scripts-runtime/ctx.ts +11 -1
- package/src/scripts-runtime/eval-harness.ts +5 -1
- package/src/scripts-runtime/executors/types.ts +2 -1
- package/src/scripts-runtime/loader.ts +3 -1
- package/src/scripts-runtime/mcp-client.ts +87 -0
- package/src/scripts-runtime/sdk-allowlist.ts +1 -0
- package/src/scripts-runtime/types/stdlib.d.ts +6 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +19 -0
- package/src/server-user.ts +114 -59
- package/src/server.ts +12 -0
- package/src/slack/assistant.ts +14 -6
- package/src/slack/enrich.ts +17 -0
- package/src/slack/handlers.ts +8 -22
- package/src/slack/responses.ts +91 -1
- package/src/slack/thread-buffer.ts +6 -3
- package/src/slack/watcher.ts +9 -6
- package/src/tasks/worker-follow-up.ts +100 -3
- package/src/tests/agentmail-handlers.test.ts +32 -3
- package/src/tests/approval-requests.test.ts +61 -0
- package/src/tests/audit-user.test.ts +47 -1
- package/src/tests/codex-adapter.test.ts +46 -23
- package/src/tests/credential-broker.test.ts +5 -2
- package/src/tests/delete-page-tool.test.ts +197 -0
- package/src/tests/github-handlers.test.ts +28 -2
- package/src/tests/gitlab-handlers.test.ts +125 -0
- package/src/tests/heartbeat.test.ts +48 -3
- package/src/tests/identity.test.ts +117 -0
- package/src/tests/jira-sync.test.ts +99 -1
- package/src/tests/kapso-inbound.test.ts +7 -0
- package/src/tests/mcp-tools-user.test.ts +60 -6
- package/src/tests/model-control.test.ts +8 -2
- package/src/tests/oauth-credential-bindings.test.ts +490 -0
- package/src/tests/oauth-refresh-sweep.test.ts +160 -0
- package/src/tests/pool-affinity.test.ts +401 -0
- package/src/tests/prompt-template-remaining.test.ts +2 -2
- package/src/tests/rbac-admission-e2e.test.ts +386 -0
- package/src/tests/rbac-admission.test.ts +492 -0
- package/src/tests/rbac-e2e-helpers.ts +3 -0
- package/src/tests/rbac-engine.test.ts +50 -0
- package/src/tests/rbac-mcp-admission-e2e.test.ts +362 -0
- package/src/tests/rbac-roles.test.ts +328 -0
- package/src/tests/reasoning-effort.test.ts +15 -2
- package/src/tests/schedule-target-type.test.ts +50 -0
- package/src/tests/scheduled-tasks.test.ts +40 -0
- package/src/tests/script-apis-mcp.test.ts +37 -13
- package/src/tests/script-connections-http.test.ts +1158 -0
- package/src/tests/script-connections-mcp.test.ts +894 -0
- package/src/tests/script-connections.test.ts +754 -6
- package/src/tests/scripts-external-api.test.ts +49 -1
- package/src/tests/scripts-runtime-secret-egress.test.ts +1 -0
- package/src/tests/scripts-runtime.test.ts +291 -0
- package/src/tests/seed-scripts.test.ts +36 -0
- package/src/tests/slack-identity-resolution.test.ts +37 -3
- package/src/tests/slack-inline-output.test.ts +65 -0
- package/src/tests/slack-thread-buffer.test.ts +28 -0
- package/src/tests/tool-annotations.test.ts +1 -0
- package/src/tests/user-token-rest-auth.test.ts +140 -1
- package/src/tests/workflow-swarm-script.test.ts +44 -0
- package/src/tests/workflow-triggers-v2.test.ts +300 -1
- package/src/tools/credential-bindings/tool.ts +277 -18
- package/src/tools/delete-page.ts +145 -0
- package/src/tools/mcp-servers/mcp-server-delete.ts +7 -4
- package/src/tools/request-human-input.ts +2 -0
- package/src/tools/resolve-user.ts +77 -27
- package/src/tools/script-apis.ts +62 -3
- package/src/tools/script-connections/tool.ts +275 -31
- package/src/tools/send-task.ts +15 -1
- package/src/tools/task-action.ts +34 -2
- package/src/tools/templates.ts +53 -1
- package/src/tools/tool-config.ts +2 -1
- package/src/tools/utils.ts +2 -0
- package/src/tools/workflows/create-workflow.ts +8 -1
- package/src/tools/workflows/update-workflow.ts +11 -2
- package/src/types.ts +90 -11
- package/src/utils/scoped-resource.ts +25 -0
- package/src/workflows/executors/human-in-the-loop.ts +1 -0
- package/src/workflows/executors/swarm-script.ts +7 -1
- package/src/workflows/triggers.ts +146 -18
- package/templates/skills/swarm-scripts/content.md +33 -8
- package/dist/cli-bgef578c.js +0 -771
- package/dist/cli-c9mtm09b.js +0 -43
- package/dist/cli-ceewqajw.js +0 -1698
- package/dist/{anthropic-messages-8nc6r2dw.js → anthropic-messages-kq3pjhbp.js} +6 -6
- package/dist/{azure-openai-responses-6yfzw3qw.js → azure-openai-responses-gm1ejtmn.js} +7 -7
- package/dist/{cli-vbraamxr.js → cli-6b4wwmm8.js} +9 -9
- package/dist/{cli-2r7s8jhp.js → cli-sznberje.js} +3 -3
- package/dist/{google-generative-ai-vfq6ymz9.js → google-generative-ai-hyrra2jm.js} +3 -3
- package/dist/{google-vertex-mj5y5rfj.js → google-vertex-8bs2pzgn.js} +3 -3
- package/dist/{openai-codex-responses-8h6kvfv8.js → openai-codex-responses-3nkvje79.js} +4 -4
- package/dist/{openai-completions-x6kjnqjd.js → openai-completions-0asftkwg.js} +11 -11
- 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 {
|
|
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
|
+
});
|