@desplega.ai/agent-swarm 2.4.5 → 2.5.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 +4 -2
- package/dist/{acp-adapter-2wdkktv6.js → acp-adapter-11gp67nr.js} +8 -6
- package/dist/{actions-c40ebcm4.js → actions-8p2kze8j.js} +23 -21
- package/dist/{amp-adapter-wb67m95z.js → amp-adapter-z4qvhacj.js} +2 -2
- package/dist/{anthropic-messages-g6pqmqk4.js → anthropic-messages-txs190dd.js} +17 -20
- package/dist/{app-353vre8g.js → app-0qp0a2gk.js} +3 -3
- package/dist/{assistant-sj1njh23.js → assistant-jedd3ktm.js} +24 -22
- package/dist/{azure-openai-responses-9dbmj346.js → azure-openai-responses-9kfwhmn7.js} +18 -13
- package/dist/{boot-reembed-qdkvxeem.js → boot-reembed-4xwq5tg0.js} +1 -1
- package/dist/{boot-reembed-b9c3az3c.js → boot-reembed-n4wq5dd2.js} +1 -1
- package/dist/{boot-scrub-sweep-m3wh8eks.js → boot-scrub-sweep-69h90xcq.js} +4 -4
- package/dist/{boot-seeds-j2273s54.js → boot-seeds-3vn0m9sg.js} +4 -4
- package/dist/{claude-adapter-m7q0a085.js → claude-adapter-221mvad5.js} +8 -6
- package/dist/{claude-managed-adapter-5ptb6tzs.js → claude-managed-adapter-gt6pa35d.js} +2 -2
- package/dist/{claude-managed-setup-smrqk5j5.js → claude-managed-setup-c3hp36ph.js} +4 -4
- package/dist/{claude-sdk-session-38xnz21e.js → claude-sdk-session-8thjsk5e.js} +9012 -8928
- package/dist/{cli-b9fcxxev.js → cli-007cnhm1.js} +50 -4
- package/dist/{cli-j4rqdf2n.js → cli-03vpq45j.js} +2 -2
- package/dist/{cli-dk44dxec.js → cli-0y6f0ckq.js} +1 -1
- package/dist/{cli-kpw04va0.js → cli-0z3vjrpm.js} +1 -1
- package/dist/{cli-94gwyg7m.js → cli-179xcyky.js} +9 -9
- package/dist/{cli-3r8samv3.js → cli-1f0gp9vz.js} +3 -2
- package/dist/{cli-4zfqgjc5.js → cli-1pr672hz.js} +2 -2
- package/dist/{cli-0bqteszy.js → cli-2nf3b08t.js} +328 -25
- package/dist/{cli-43x64hv3.js → cli-36j4jnk3.js} +4 -4
- package/dist/{cli-k8g16gz3.js → cli-3qv8naq8.js} +115 -119
- package/dist/cli-3zwye9sn.js +1193 -0
- package/dist/{cli-60cgh1rv.js → cli-459fk97x.js} +7 -7
- package/dist/{cli-v8fjakwf.js → cli-4w24f7z3.js} +1 -1
- package/dist/{cli-n72rg53p.js → cli-5kzk3472.js} +812 -418
- package/dist/{cli-h7gc0b4r.js → cli-5m84zd2k.js} +55 -18
- package/dist/{cli-j99azg3g.js → cli-5rvg1ddk.js} +5 -5
- package/dist/{cli-dvkmp9bt.js → cli-6ctwrf1z.js} +20 -10
- package/dist/{cli-848hxdwj.js → cli-6rg2jn2y.js} +2 -1
- package/dist/{cli-fv5hepyy.js → cli-7bwtc6yx.js} +3 -3
- package/dist/{cli-ms85309d.js → cli-7g40g8hq.js} +11 -11
- package/dist/{cli-xpa64rek.js → cli-92mqenkc.js} +2362 -159
- package/dist/{cli-018r4jq6.js → cli-9ce2xw98.js} +8 -8
- package/dist/{cli-1syn2xy1.js → cli-bgdtv62r.js} +32 -18
- package/dist/{cli-wxbn3yfx.js → cli-cz5nmdxt.js} +2 -2
- package/dist/{cli-wt645v7q.js → cli-dabehxhp.js} +1 -1
- package/dist/{cli-vfha21c9.js → cli-e5bxnjps.js} +2 -2
- package/dist/{cli-wt0jf9hm.js → cli-fcc5qe3f.js} +553 -193
- package/dist/{cli-ftc1g5tj.js → cli-fes51ya2.js} +3 -3
- package/dist/{cli-3a45zcgm.js → cli-ftmq4nvk.js} +2 -2
- package/dist/{cli-37j04k05.js → cli-fy1jyyym.js} +13 -13
- package/dist/{cli-s95dknxh.js → cli-g1cw7xtt.js} +1 -1
- package/dist/{cli-74c1wpm0.js → cli-gpv60278.js} +1 -1
- package/dist/{cli-pemywhfq.js → cli-ha9rmwgq.js} +2 -1
- package/dist/{cli-g35m2xwm.js → cli-j1eghqfm.js} +2 -2
- package/dist/{cli-qfxc84gh.js → cli-jpxsrxcb.js} +1 -1
- package/dist/{cli-ejs3m1sx.js → cli-kxpfx2xr.js} +1 -1
- package/dist/{cli-p14m2cnp.js → cli-mbfj11fq.js} +1 -1
- package/dist/{cli-pnp2j5zg.js → cli-mmhj8vd7.js} +2 -2
- package/dist/{cli-xf234ws8.js → cli-n4c8e3s5.js} +2 -0
- package/dist/{cli-f01h25yt.js → cli-n6kmqngw.js} +2 -2
- package/dist/{cli-v0yrfmdh.js → cli-nnvjb17s.js} +3 -3
- package/dist/{cli-1dmcq2eh.js → cli-nzfnt2gs.js} +1 -1
- package/dist/{cli-625tb7aq.js → cli-p859hcc8.js} +2 -2
- package/dist/{cli-smvcw9z5.js → cli-pasyvjmm.js} +1 -1
- package/dist/cli-pkaqk57w.js +128 -0
- package/dist/{cli-8nyhf7za.js → cli-qqzmjxkf.js} +129 -105
- package/dist/{cli-9yqgm9nd.js → cli-r25qpaxt.js} +161 -63
- package/dist/{cli-eww120g8.js → cli-rh5smt8t.js} +18 -4
- package/dist/{cli-qwkcwjdn.js → cli-rycdaj0a.js} +1 -1
- package/dist/{cli-a92r1127.js → cli-s4sntwt4.js} +2 -2
- package/dist/{cli-be2161h1.js → cli-segs7hx9.js} +6 -6
- package/dist/{cli-36ckw0wx.js → cli-stxdrqs2.js} +2 -2
- package/dist/{cli-z7d29rw9.js → cli-sy8f9705.js} +4 -4
- package/dist/{cli-1m17dc9d.js → cli-ta8bd7th.js} +2 -2
- package/dist/{cli-st8aw1m6.js → cli-we0ra057.js} +4 -4
- package/dist/{cli-7c9qs530.js → cli-x21dqn18.js} +5 -5
- package/dist/{cli-1sp861s4.js → cli-x6vf4sad.js} +11 -11
- package/dist/{cli-d2mb5s8a.js → cli-x77gen2p.js} +680 -440
- package/dist/{cli-tw4p90jb.js → cli-xawhpgc9.js} +5 -5
- package/dist/{cli-f0g3pgvf.js → cli-xd4eqtr2.js} +2 -2
- package/dist/{cli-1f3qzw6y.js → cli-xt768r5m.js} +2 -2
- package/dist/{cli-cxxe61f6.js → cli-y4x8ww6s.js} +2 -2
- package/dist/{cli-bnes4z6b.js → cli-y9jvxfhk.js} +3 -3
- package/dist/{cli-qwhy2qma.js → cli-z63a269p.js} +2 -2
- package/dist/{cli-sm8rses9.js → cli-zfdp01hn.js} +8 -5
- package/dist/cli.js +29 -27
- package/dist/{cloudflare-workers-ai-system-one-ch2wxfcp.js → cloudflare-workers-ai-system-one-43v9qkjk.js} +7 -2
- package/dist/{codex-adapter-tn6k5mkg.js → codex-adapter-tcg1q3jk.js} +6 -4
- package/dist/{codex-login-m819c41r.js → codex-login-rn7c5gxt.js} +1 -1
- package/dist/{codex-session-runner-7vjt7jbw.js → codex-session-runner-xfx4wd9x.js} +7 -5
- package/dist/{commands-t7rz9146.js → commands-ew83nj70.js} +2 -2
- package/dist/{controller-m2jasma5.js → controller-gnq1k1mf.js} +4 -4
- package/dist/{controllers-connect-9ftpex99.js → controllers-connect-tw1dan9r.js} +25 -23
- package/dist/{cursor-adapter-mjv79e4z.js → cursor-adapter-yq5xgfcg.js} +1 -1
- package/dist/{db-6sfrn9g3.js → db-7gbc53fd.js} +14 -2
- package/dist/{db-rn673r06.js → db-t86xrnes.js} +3 -3
- package/dist/{devin-adapter-zgqadvnm.js → devin-adapter-4ycy4vrf.js} +1 -1
- package/dist/{dispatcher-c155xz0p.js → dispatcher-64bdyhps.js} +22 -20
- package/dist/{docker-fkgtr75t.js → docker-nfy2gc8t.js} +2 -2
- package/dist/{dsh-adapter-3mmszc1x.js → dsh-adapter-tvhc4k2d.js} +1 -1
- package/dist/{e2b-16d6ch39.js → e2b-9gkdhm3c.js} +2 -2
- package/dist/{e2b-zn0b4w5w.js → e2b-cjtf8k1z.js} +2 -2
- package/dist/{easter-egg-3d-5cgynaja.js → easter-egg-3d-e4qv8dxj.js} +1 -1
- package/dist/{event-bus-9vpk8gmq.js → event-bus-3z78ep5n.js} +1 -1
- package/dist/{execute-t4e6m3rw.js → execute-0xpz8950.js} +85 -23
- package/dist/{external-channel-35k6tkv8.js → external-channel-e9nw24zv.js} +2 -2
- package/dist/{google-generative-ai-bmq8gkf3.js → google-generative-ai-mk9vcpe1.js} +15 -15
- package/dist/{google-vertex-b1rvp52s.js → google-vertex-q580kz8k.js} +20 -20
- package/dist/{grok-adapter-tfrq7tg1.js → grok-adapter-v82srhrx.js} +9 -7
- package/dist/handlers-c9ajzhbn.js +41 -0
- package/dist/{heartbeat-dyg4npkp.js → heartbeat-2vqs1319.js} +25 -23
- package/dist/{hook-x26b31bg.js → hook-01nk13wx.js} +7 -5
- package/dist/{hook-drspbwpe.js → hook-jzpheegg.js} +9 -7
- package/dist/{http-pk972xyw.js → http-fc1k6ec8.js} +544 -76
- package/dist/index-07ef7nkm.js +42 -0
- package/dist/{index-g6tbb6ve.js → index-31ceyf42.js} +1 -1
- package/dist/index-7yts8n9s.js +43 -0
- package/dist/index-q3t40ryv.js +46 -0
- package/dist/{index-arytgj5q.js → index-sepja052.js} +1 -1
- package/dist/{index-jwxkkex2.js → index-zdycv3ja.js} +13 -11
- package/dist/{index-g216q6jr.js → index-zsyx51ap.js} +1 -4
- package/dist/{keepalive-7yrng7px.js → keepalive-5nh2rhmr.js} +4 -4
- package/dist/lead-aayq64jn.js +79 -0
- package/dist/{llama-cpp-classify-vv43gswy.js → llama-cpp-classify-ee7vmd7q.js} +4 -1
- package/dist/{maintenance-e6cemdqt.js → maintenance-hjhzdt3k.js} +1 -1
- package/dist/{mistral-conversations-1medfwv7.js → mistral-conversations-sd0xf1tn.js} +13 -13
- package/dist/{node-de6akczr.js → node-jzjj7r9x.js} +1 -1
- package/dist/{oauth-refresh-sweep-yw3xdbcj.js → oauth-refresh-sweep-y3n1knmd.js} +3 -3
- package/dist/{onboard-0a9y5yrc.js → onboard-1zq3m7db.js} +2 -2
- package/dist/{openai-codex-responses-de9pvs4d.js → openai-codex-responses-79s8xnbh.js} +29 -23
- package/dist/{openai-completions-mtqge3nf.js → openai-completions-tdbyts0x.js} +21 -20
- package/dist/{openai-decisions-ygk4kkxd.js → openai-decisions-bqhnjyfz.js} +2 -1
- package/dist/{openai-responses-dkyng71g.js → openai-responses-dm3t5vqp.js} +26 -17
- package/dist/{opencode-adapter-x4tqzhkh.js → opencode-adapter-e56086m1.js} +7 -5
- package/dist/{openrouter-images-wvv7camj.js → openrouter-images-ssttr1y4.js} +6 -3
- package/dist/{otel-impl-trjpkfb7.js → otel-impl-h9yyza47.js} +2 -2
- package/dist/{pi-messages-nhh1pkz2.js → pi-messages-3nf8bq1a.js} +6 -4
- package/dist/{pi-mono-adapter-mcfkn9vf.js → pi-mono-adapter-tzps8f38.js} +9 -37
- package/dist/{pricing-refresh-d2b5my0y.js → pricing-refresh-nevhat8h.js} +3 -3
- package/dist/{rbac-roles-nxh5ahwk.js → rbac-roles-4y6ngvzq.js} +1 -1
- package/dist/{rbac-roles-hey3yv9d.js → rbac-roles-exbg8xb6.js} +2 -2
- package/dist/{render-v2-rr3p2atg.js → render-v2-cfsh13z3.js} +3 -3
- package/dist/{resume-2efkf1s1.js → resume-5s2t0dbs.js} +10 -8
- package/dist/{runtime-xe3aqy4x.js → runtime-bt5pkpm1.js} +1 -1
- package/dist/{session-access-s2mttxpw.js → session-access-dnbyp3cn.js} +6 -4
- package/dist/{setup-vypma5yt.js → setup-1wj1y77j.js} +1 -1
- package/dist/{typesafe-system-one-2hgpayzy.js → typesafe-system-one-bre38hvs.js} +4 -2
- package/dist/{virtual-modules-4r3zkc20.js → virtual-modules-mnfbh5mr.js} +8 -2
- package/dist/{work-objects-rvhckdkp.js → work-objects-7mg6216g.js} +25 -23
- package/dist/worker-nwb789mg.js +79 -0
- package/dist/{x-y45mthwn.js → x-pbpezr0s.js} +2 -2
- package/openapi.json +785 -5
- package/package.json +9 -9
- package/src/assets/key.ts +17 -0
- package/src/azure-devops/handlers.ts +1 -1
- package/src/be/db/approvals.ts +18 -0
- package/src/be/db/session-moves.ts +164 -0
- package/src/be/db/session-tree.ts +384 -0
- package/src/be/db/tasks/read.ts +40 -4
- package/src/be/db/workflow-runs.ts +96 -0
- package/src/be/db.ts +80 -236
- package/src/be/onboarding.ts +3 -0
- package/src/be/skill-sync.ts +43 -2
- package/src/be/swarm-config-guard.ts +1 -0
- package/src/bitbucket/api.ts +81 -0
- package/src/bitbucket/auth.ts +64 -0
- package/src/bitbucket/handlers.ts +434 -0
- package/src/bitbucket/index.ts +28 -0
- package/src/bitbucket/templates.ts +134 -0
- package/src/bitbucket/types.ts +65 -0
- package/src/commands/runner.ts +55 -3
- package/src/e2b/env.ts +1 -0
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +2 -2
- package/src/extensions/post-bridge.ts +1 -0
- package/src/github/handlers.ts +6 -6
- package/src/gitlab/handlers.ts +5 -5
- package/src/http/assets.ts +14 -2
- package/src/http/core.ts +12 -0
- package/src/http/index.ts +4 -0
- package/src/http/onboarding.ts +1 -0
- package/src/http/sessions.ts +366 -12
- package/src/http/skills.ts +48 -1
- package/src/http/tasks.ts +1 -1
- package/src/http/users.ts +1 -0
- package/src/http/webhooks.ts +141 -0
- package/src/integrations/chat-sdk/adapters.ts +19 -3
- package/src/integrations/chat-sdk/facts.ts +30 -12
- package/src/integrations/chat-sdk/outbound.ts +38 -1
- package/src/providers/acp-targets.ts +1 -1
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/pi-mono-adapter.ts +5 -53
- package/src/scripts-runtime/types/swarm-extension.d.ts +6 -4
- package/src/slack/handlers.test.ts +158 -0
- package/src/slack/handlers.ts +19 -28
- package/src/tasks/context-key.ts +61 -1
- package/src/tests/bitbucket-auth.test.ts +156 -0
- package/src/tests/bitbucket-handlers.test.ts +425 -0
- package/src/tests/bitbucket-webhook.test.ts +220 -0
- package/src/tests/chat-sdk-inbound.test.ts +39 -0
- package/src/tests/chat-sdk-telegram-markdown.test.ts +92 -0
- package/src/tests/context-key.test.ts +50 -0
- package/src/tests/core-auth.test.ts +5 -0
- package/src/tests/db-task-read-contract.test.ts +7 -0
- package/src/tests/db-task-write-contract.test.ts +6 -0
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/fixtures/bitbucket/pullrequest-comment-created-inline.json +174 -0
- package/src/tests/fixtures/bitbucket/pullrequest-comment-created.json +170 -0
- package/src/tests/fixtures/bitbucket/pullrequest-created-mention.json +132 -0
- package/src/tests/fixtures/bitbucket/pullrequest-created-reviewer.json +140 -0
- package/src/tests/fixtures/bitbucket/pullrequest-fulfilled.json +134 -0
- package/src/tests/fixtures/bitbucket/pullrequest-rejected.json +133 -0
- package/src/tests/generate-mcp-docs.test.ts +101 -0
- package/src/tests/gitlab-handlers.test.ts +3 -3
- package/src/tests/gitlab-vcs-db.test.ts +4 -4
- package/src/tests/integration-disable-flags.test.ts +36 -0
- package/src/tests/mcp-tools.test.ts +1 -1
- package/src/tests/onboarding.test.ts +12 -0
- package/src/tests/pi-codemode.test.ts +8 -2
- package/src/tests/preload-env.test.ts +57 -0
- package/src/tests/preload.ts +21 -0
- package/src/tests/prompt-template-remaining.test.ts +38 -0
- package/src/tests/run-root-coverage.test.ts +39 -0
- package/src/tests/runner-bitbucket-vcs.test.ts +191 -0
- package/src/tests/runner-skills-refresh.test.ts +58 -0
- package/src/tests/secret-scrubber.test.ts +15 -0
- package/src/tests/sessions-move.test.ts +576 -0
- package/src/tests/sessions-tree.test.ts +493 -0
- package/src/tests/skill-files-http.test.ts +50 -0
- package/src/tests/skills-bundle.test.ts +99 -0
- package/src/tests/swarm-config-export-to-env.test.ts +2 -1
- package/src/tests/vcs-bitbucket.test.ts +91 -0
- package/src/tests/vcs-provider-isolation.test.ts +123 -0
- package/src/tests/vcs-provider.test.ts +16 -1
- package/src/tests/vcs-tracking.test.ts +3 -3
- package/src/tests/workflow-foreach-bodies.test.ts +637 -0
- package/src/tests/workflow-foreach-concurrency.test.ts +342 -0
- package/src/tests/workflow-foreach-retry.test.ts +537 -0
- package/src/tests/workflow-foreach.test.ts +44 -23
- package/src/tests/workflow-step-claims.test.ts +6 -6
- package/src/tests/workflow-tla-races.test.ts +2 -2
- package/src/tools/script-delete.ts +1 -1
- package/src/tools/workflows/create-workflow.ts +5 -3
- package/src/types.ts +3 -2
- package/src/utils/config-env-export.ts +1 -0
- package/src/utils/secret-scrubber.ts +4 -0
- package/src/utils/skills-refresh.ts +83 -43
- package/src/vcs/bitbucket.ts +58 -0
- package/src/vcs/index.ts +2 -0
- package/src/vcs/types.ts +2 -2
- package/src/workflows/approval-resolution.ts +4 -1
- package/src/workflows/checkpoint.ts +9 -1
- package/src/workflows/constants.ts +3 -0
- package/src/workflows/definition.ts +48 -17
- package/src/workflows/engine.ts +25 -15
- package/src/workflows/executors/foreach.ts +45 -99
- package/src/workflows/foreach-body.ts +146 -0
- package/src/workflows/foreach-driver.ts +552 -0
- package/src/workflows/foreach-join.ts +206 -7
- package/src/workflows/node-timeout.ts +21 -0
- package/src/workflows/recovery.ts +68 -3
- package/src/workflows/resume.ts +221 -43
- package/src/workflows/retry-poller.ts +39 -24
- package/src/workflows/step-input.ts +59 -0
- package/src/workflows/task-step-routing.ts +114 -56
- package/templates/skills/slack-interaction/content.md +5 -0
- package/templates/skills/work-on-task/SKILL.md +1 -1
- package/templates/skills/work-on-task/content.md +1 -1
- package/dist/cli-9hstj09h.js +0 -2657
- package/dist/cli-yw91rnmw.js +0 -13
- package/dist/handlers-7gw64dxw.js +0 -39
- package/dist/index-bgdyvcnv.js +0 -40
- package/dist/index-gabbcdn2.js +0 -43
- package/dist/index-tfb26zhy.js +0 -41
- package/dist/lead-7e02z839.js +0 -77
- package/dist/worker-5snytpbd.js +0 -77
package/src/http/webhooks.ts
CHANGED
|
@@ -31,6 +31,17 @@ import {
|
|
|
31
31
|
isAzureDevOpsEnabled,
|
|
32
32
|
verifyAzureDevOpsWebhook,
|
|
33
33
|
} from "../azure-devops";
|
|
34
|
+
import {
|
|
35
|
+
pullRequestCommentEventOf as bitbucketCommentEventOf,
|
|
36
|
+
pullRequestEventOf as bitbucketPullRequestEventOf,
|
|
37
|
+
handlePullRequestClosed as handleBitbucketPullRequestClosed,
|
|
38
|
+
handlePullRequestCommentCreated as handleBitbucketPullRequestCommentCreated,
|
|
39
|
+
handlePullRequestCreated as handleBitbucketPullRequestCreated,
|
|
40
|
+
isBitbucketEnabled,
|
|
41
|
+
isDuplicateDelivery as isDuplicateBitbucketDelivery,
|
|
42
|
+
markDeliveryProcessed as markBitbucketDeliveryProcessed,
|
|
43
|
+
verifyBitbucketWebhook,
|
|
44
|
+
} from "../bitbucket";
|
|
34
45
|
import type {
|
|
35
46
|
CheckRunEvent,
|
|
36
47
|
CheckSuiteEvent,
|
|
@@ -180,6 +191,28 @@ const azureDevOpsWebhook = route({
|
|
|
180
191
|
},
|
|
181
192
|
});
|
|
182
193
|
|
|
194
|
+
const bitbucketWebhook = route({
|
|
195
|
+
method: "post",
|
|
196
|
+
path: "/api/bitbucket/webhook",
|
|
197
|
+
pattern: ["api", "bitbucket", "webhook"],
|
|
198
|
+
summary: "Handle Bitbucket Cloud webhook events",
|
|
199
|
+
tags: ["Webhooks"],
|
|
200
|
+
auth: { apiKey: false },
|
|
201
|
+
rbac: {
|
|
202
|
+
ungated:
|
|
203
|
+
"provider webhook: authenticated by the per-webhook HMAC secret, not a swarm principal",
|
|
204
|
+
},
|
|
205
|
+
responses: {
|
|
206
|
+
200: { description: "Event processed", schema: WebhookDispatchResultSchema },
|
|
207
|
+
401: { description: "Invalid signature" },
|
|
208
|
+
413: { description: "Body exceeds 1 MiB" },
|
|
209
|
+
503: { description: "Bitbucket integration not configured" },
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
/** Bitbucket Cloud event payloads are far below this. */
|
|
214
|
+
const BITBUCKET_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
|
|
215
|
+
|
|
183
216
|
const agentmailWebhook = route({
|
|
184
217
|
method: "post",
|
|
185
218
|
path: "/api/agentmail/webhook",
|
|
@@ -710,6 +743,114 @@ export async function handleWebhooks(
|
|
|
710
743
|
return true;
|
|
711
744
|
}
|
|
712
745
|
|
|
746
|
+
// Bitbucket Cloud webhook — HMAC over the raw body, event keyed by X-Event-Key
|
|
747
|
+
if (bitbucketWebhook.match(req.method, pathSegments)) {
|
|
748
|
+
if (!isBitbucketEnabled()) {
|
|
749
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
750
|
+
res.end(JSON.stringify({ error: "Bitbucket integration not configured" }));
|
|
751
|
+
return true;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// The body is read before the HMAC check, so cap it like the Chat SDK
|
|
755
|
+
// route: reject a declared or streamed body over the limit with 413.
|
|
756
|
+
if (enforceContentLengthCap(req, res, BITBUCKET_WEBHOOK_MAX_BODY_BYTES) === BODY_TOO_LARGE) {
|
|
757
|
+
return true;
|
|
758
|
+
}
|
|
759
|
+
const chunks: Buffer[] = [];
|
|
760
|
+
let receivedBytes = 0;
|
|
761
|
+
let tooLarge = false;
|
|
762
|
+
for await (const chunk of req) {
|
|
763
|
+
if (tooLarge) continue;
|
|
764
|
+
receivedBytes += (chunk as Buffer).byteLength;
|
|
765
|
+
if (receivedBytes > BITBUCKET_WEBHOOK_MAX_BODY_BYTES) {
|
|
766
|
+
tooLarge = true;
|
|
767
|
+
chunks.length = 0;
|
|
768
|
+
jsonError(res, `Payload too large (max ${BITBUCKET_WEBHOOK_MAX_BODY_BYTES} bytes)`, 413);
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
chunks.push(chunk as Buffer);
|
|
772
|
+
}
|
|
773
|
+
if (tooLarge) return true;
|
|
774
|
+
const rawBody = Buffer.concat(chunks).toString();
|
|
775
|
+
|
|
776
|
+
const signature = req.headers["x-hub-signature"] as string | undefined;
|
|
777
|
+
if (!verifyBitbucketWebhook(rawBody, signature)) {
|
|
778
|
+
console.log("[Bitbucket] Invalid webhook signature");
|
|
779
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
780
|
+
res.end(JSON.stringify({ error: "Invalid signature" }));
|
|
781
|
+
return true;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
let body: unknown;
|
|
785
|
+
try {
|
|
786
|
+
body = JSON.parse(rawBody);
|
|
787
|
+
} catch {
|
|
788
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
789
|
+
res.end(JSON.stringify({ error: "Invalid JSON body" }));
|
|
790
|
+
return true;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
const eventKey = req.headers["x-event-key"] as string | undefined;
|
|
794
|
+
const requestUuid = req.headers["x-request-uuid"] as string | undefined;
|
|
795
|
+
if (isDuplicateBitbucketDelivery(requestUuid)) {
|
|
796
|
+
console.log(`[Bitbucket] Skipping duplicate delivery ${requestUuid}`);
|
|
797
|
+
bitbucketWebhook.respond(res, 200, { created: false });
|
|
798
|
+
return true;
|
|
799
|
+
}
|
|
800
|
+
console.log(`[Bitbucket] Received ${eventKey} event`);
|
|
801
|
+
|
|
802
|
+
try {
|
|
803
|
+
const result = await runBlockableHandler("Bitbucket", async () => {
|
|
804
|
+
switch (eventKey) {
|
|
805
|
+
case "pullrequest:created":
|
|
806
|
+
return await handleBitbucketPullRequestCreated(body);
|
|
807
|
+
case "pullrequest:comment_created":
|
|
808
|
+
return await handleBitbucketPullRequestCommentCreated(body);
|
|
809
|
+
case "pullrequest:fulfilled":
|
|
810
|
+
return await handleBitbucketPullRequestClosed(body, "merged");
|
|
811
|
+
case "pullrequest:rejected":
|
|
812
|
+
return await handleBitbucketPullRequestClosed(body, "declined");
|
|
813
|
+
default:
|
|
814
|
+
console.log(`[Bitbucket] Ignoring unsupported event type: ${eventKey}`);
|
|
815
|
+
return { created: false };
|
|
816
|
+
}
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
// Emit workflow trigger events for Bitbucket
|
|
820
|
+
const workflowEvent = {
|
|
821
|
+
"pullrequest:created": "created",
|
|
822
|
+
"pullrequest:comment_created": "commented",
|
|
823
|
+
"pullrequest:fulfilled": "merged",
|
|
824
|
+
"pullrequest:rejected": "declined",
|
|
825
|
+
}[eventKey ?? ""];
|
|
826
|
+
const prEvent = workflowEvent ? bitbucketPullRequestEventOf(body) : null;
|
|
827
|
+
if (workflowEvent && prEvent) {
|
|
828
|
+
const pr = prEvent.pullrequest;
|
|
829
|
+
const comment =
|
|
830
|
+
workflowEvent === "commented" ? bitbucketCommentEventOf(body)?.comment : undefined;
|
|
831
|
+
const user = comment?.user ?? prEvent.actor;
|
|
832
|
+
workflowEventBus.emit(`bitbucket.pull_request.${workflowEvent}`, {
|
|
833
|
+
repo: prEvent.repository.full_name,
|
|
834
|
+
number: pr.id,
|
|
835
|
+
...(workflowEvent === "commented"
|
|
836
|
+
? { body: comment?.content.raw }
|
|
837
|
+
: { title: pr.title, body: pr.description ?? undefined }),
|
|
838
|
+
action: workflowEvent,
|
|
839
|
+
user_login: user.account_id ?? user.nickname ?? user.display_name,
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
markBitbucketDeliveryProcessed(requestUuid);
|
|
844
|
+
bitbucketWebhook.respond(res, 200, result);
|
|
845
|
+
} catch (err) {
|
|
846
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
847
|
+
console.error(`[Bitbucket] Error handling ${eventKey} event: ${scrubSecrets(errorMessage)}`);
|
|
848
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
849
|
+
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
850
|
+
}
|
|
851
|
+
return true;
|
|
852
|
+
}
|
|
853
|
+
|
|
713
854
|
// AgentMail webhook — needs raw body for Svix signature verification
|
|
714
855
|
if (agentmailWebhook.match(req.method, pathSegments)) {
|
|
715
856
|
if (!isAgentMailEnabled()) {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import type { TelegramMessage } from "@chat-adapter/telegram";
|
|
1
2
|
import type { Adapter, Logger } from "chat";
|
|
2
3
|
import { type ChatSdkAdapterName, type ResolvedAdapterConfig, readApiUrlOverrides } from "./config";
|
|
4
|
+
import { isTelegramReplyToBot, type TelegramRawMessage } from "./facts";
|
|
3
5
|
import { withInfoFilter } from "./logger";
|
|
4
6
|
import { getChatSdkTestHooks } from "./test-hooks";
|
|
5
7
|
|
|
@@ -61,15 +63,29 @@ export async function loadAdapter(
|
|
|
61
63
|
const base = { ...config, apiUrl, logger };
|
|
62
64
|
switch (name) {
|
|
63
65
|
case "telegram": {
|
|
64
|
-
const {
|
|
66
|
+
const { TelegramAdapter } = await import("@chat-adapter/telegram");
|
|
67
|
+
// The adapter's `mentionOnReply` skips a reply whose target id equals
|
|
68
|
+
// `message_thread_id`, meant for forum topic roots. A plain supergroup
|
|
69
|
+
// reply to a bot message that starts a reply chain has that same shape,
|
|
70
|
+
// so it was never a mention and never reached a handler. Core decides
|
|
71
|
+
// with `is_topic_message`, as the task header facts do.
|
|
72
|
+
class SwarmTelegramAdapter extends TelegramAdapter {
|
|
73
|
+
protected override isBotMentioned(message: TelegramMessage, text: string): boolean {
|
|
74
|
+
return (
|
|
75
|
+
super.isBotMentioned(message, text) ||
|
|
76
|
+
(String(message.from?.id) !== this.botUserId &&
|
|
77
|
+
isTelegramReplyToBot(message as TelegramRawMessage, this.botUserId))
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
65
81
|
// Explicit `false` blocks the TELEGRAM_ALLOW_UNVERIFIED_WEBHOOKS env fallback.
|
|
66
82
|
// Explicit `true` makes a reply to the bot a mention whatever
|
|
67
83
|
// TELEGRAM_MENTION_ON_REPLY says.
|
|
68
|
-
return
|
|
84
|
+
return new SwarmTelegramAdapter({
|
|
69
85
|
...base,
|
|
70
86
|
allowUnverifiedWebhooks: false,
|
|
71
87
|
mentionOnReply: true,
|
|
72
|
-
} as
|
|
88
|
+
} as ConstructorParameters<typeof TelegramAdapter>[0]);
|
|
73
89
|
}
|
|
74
90
|
case "slack": {
|
|
75
91
|
const { createSlackAdapter } = await import("@chat-adapter/slack");
|
|
@@ -30,7 +30,7 @@ export interface ChatMessageFacts {
|
|
|
30
30
|
|
|
31
31
|
const SNIPPET_MAX = 80;
|
|
32
32
|
|
|
33
|
-
interface TelegramRawMessage {
|
|
33
|
+
export interface TelegramRawMessage {
|
|
34
34
|
message_id?: number;
|
|
35
35
|
message_thread_id?: number;
|
|
36
36
|
is_topic_message?: boolean;
|
|
@@ -57,16 +57,37 @@ function personLabel(name: string | undefined, username: string | undefined, fal
|
|
|
57
57
|
return name || fallback;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
/**
|
|
61
|
+
* In a forum topic every plain message replies to the topic's creation
|
|
62
|
+
* message, so that reply is the topic, not an answer to anyone. Only
|
|
63
|
+
* `is_topic_message` marks a topic: a plain supergroup reply also carries
|
|
64
|
+
* `message_thread_id`, the id of its reply chain's first message.
|
|
65
|
+
*/
|
|
66
|
+
function replyIsTopicRoot(raw: TelegramRawMessage): boolean {
|
|
61
67
|
const reply = raw.reply_to_message;
|
|
62
|
-
|
|
63
|
-
// message, so that reply is the topic, not an answer to anyone.
|
|
64
|
-
const replyIsTopicRoot =
|
|
68
|
+
return (
|
|
65
69
|
reply !== undefined &&
|
|
66
70
|
(reply.forum_topic_created !== undefined ||
|
|
67
|
-
(raw.is_topic_message === true && reply.message_id === raw.message_thread_id))
|
|
68
|
-
|
|
69
|
-
|
|
71
|
+
(raw.is_topic_message === true && reply.message_id === raw.message_thread_id))
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The raw Telegram message replies to one of the bot's own messages, not to a forum topic root. */
|
|
76
|
+
export function isTelegramReplyToBot(raw: TelegramRawMessage, botUserId?: string): boolean {
|
|
77
|
+
const fromId = raw.reply_to_message?.from?.id;
|
|
78
|
+
return (
|
|
79
|
+
!replyIsTopicRoot(raw) &&
|
|
80
|
+
fromId !== undefined &&
|
|
81
|
+
botUserId !== undefined &&
|
|
82
|
+
String(fromId) === botUserId
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function telegramFacts(raw: TelegramRawMessage, message: Message, botUserId?: string) {
|
|
87
|
+
const reply = raw.reply_to_message;
|
|
88
|
+
const topicRoot = replyIsTopicRoot(raw);
|
|
89
|
+
const realReply = reply && !topicRoot ? reply : undefined;
|
|
90
|
+
const topicName = topicRoot ? reply?.forum_topic_created?.name : undefined;
|
|
70
91
|
const name = [raw.from?.first_name, raw.from?.last_name].filter(Boolean).join(" ").trim();
|
|
71
92
|
return {
|
|
72
93
|
chatType: raw.chat?.type,
|
|
@@ -79,10 +100,7 @@ function telegramFacts(raw: TelegramRawMessage, message: Message, botUserId?: st
|
|
|
79
100
|
: `#${raw.message_thread_id}`
|
|
80
101
|
: undefined,
|
|
81
102
|
from: personLabel(name || undefined, raw.from?.username, message.author.userId),
|
|
82
|
-
replyToBot:
|
|
83
|
-
realReply?.from?.id !== undefined &&
|
|
84
|
-
botUserId !== undefined &&
|
|
85
|
-
String(realReply.from.id) === botUserId,
|
|
103
|
+
replyToBot: isTelegramReplyToBot(raw, botUserId),
|
|
86
104
|
replyToSnippet: snippet(realReply?.text ?? realReply?.caption),
|
|
87
105
|
} satisfies ChatMessageFacts;
|
|
88
106
|
}
|
|
@@ -84,6 +84,42 @@ interface PostTarget {
|
|
|
84
84
|
}): Promise<{ id: string; threadId?: string }>;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
/** A fence opener or closer: up to 3 spaces, then 3+ backticks or tildes. */
|
|
88
|
+
const FENCE_LINE = /^ {0,3}(`{3,}|~{3,})/;
|
|
89
|
+
/**
|
|
90
|
+
* A `#` run at the start of a line's text (after blockquote, list and task
|
|
91
|
+
* markers) that CommonMark does not read as a heading: 7+ `#`, or `#` followed
|
|
92
|
+
* by a character other than whitespace or `#`.
|
|
93
|
+
*/
|
|
94
|
+
const NON_HEADING_HASH =
|
|
95
|
+
/^( {0,3}(?:>[ \t]?)*[ \t]*(?:(?:[-*+]|\d{1,9}[.)])[ \t]+(?:\[[ xX]\][ \t]+)?)?)(#{7,}|#{1,6}(?=[^\s#]))/;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Telegram renders `{ markdown }` posts with `sendRichMessage`, and Telegram's
|
|
99
|
+
* own Markdown parser reads a line-leading `#2203` or `#tag` as a heading.
|
|
100
|
+
* Escape the first `#` of such a line so only CommonMark ATX headings
|
|
101
|
+
* (`# Title`) stay headings. Fenced code is left as is. The MarkdownV2
|
|
102
|
+
* fallback parses the escape back to a literal `#`.
|
|
103
|
+
*/
|
|
104
|
+
export function escapeTelegramHashLines(markdown: string): string {
|
|
105
|
+
let fence: string | undefined;
|
|
106
|
+
return markdown
|
|
107
|
+
.split("\n")
|
|
108
|
+
.map((line) => {
|
|
109
|
+
const marker = FENCE_LINE.exec(line)?.[1];
|
|
110
|
+
if (fence) {
|
|
111
|
+
if (marker && marker[0] === fence[0] && marker.length >= fence.length) fence = undefined;
|
|
112
|
+
return line;
|
|
113
|
+
}
|
|
114
|
+
if (marker) {
|
|
115
|
+
fence = marker;
|
|
116
|
+
return line;
|
|
117
|
+
}
|
|
118
|
+
return line.replace(NON_HEADING_HASH, "$1\\$2");
|
|
119
|
+
})
|
|
120
|
+
.join("\n");
|
|
121
|
+
}
|
|
122
|
+
|
|
87
123
|
async function postChunks(
|
|
88
124
|
adapter: string,
|
|
89
125
|
target: PostTarget,
|
|
@@ -92,7 +128,8 @@ async function postChunks(
|
|
|
92
128
|
const messageIds: string[] = [];
|
|
93
129
|
let threadId: string | undefined;
|
|
94
130
|
for (const chunk of splitChatMessage(text, chatMessageLimit(adapter))) {
|
|
95
|
-
const
|
|
131
|
+
const markdown = adapter === "telegram" ? escapeTelegramHashLines(chunk) : chunk;
|
|
132
|
+
const sent = await target.post({ markdown });
|
|
96
133
|
messageIds.push(sent.id);
|
|
97
134
|
threadId ??= sent.threadId;
|
|
98
135
|
}
|
|
@@ -359,7 +359,7 @@ const copilotTargetProfile: AcpTargetProfile = {
|
|
|
359
359
|
},
|
|
360
360
|
};
|
|
361
361
|
|
|
362
|
-
/** Qwen Code's `--auth-type` choices (0.
|
|
362
|
+
/** Qwen Code's `--auth-type` choices (0.26.0). `qwen-oauth` needs a browser, so it is left out. */
|
|
363
363
|
const QWEN_AUTH_TYPES = ["openai", "openai-responses", "anthropic", "gemini", "vertex-ai"] as const;
|
|
364
364
|
|
|
365
365
|
// Qwen Code is a Gemini CLI fork: QWEN_SYSTEM_MD replaces its built-in prompt
|
|
@@ -44,7 +44,7 @@ import type {
|
|
|
44
44
|
* default; the image installs exactly this version and the per-task settings
|
|
45
45
|
* set `amp.updates.mode: "disabled"`. Bump it deliberately, with a live run.
|
|
46
46
|
*/
|
|
47
|
-
export const AMP_PACKAGE = "@ampcode/cli@0.0.
|
|
47
|
+
export const AMP_PACKAGE = "@ampcode/cli@0.0.1792670445-ge8d5b3";
|
|
48
48
|
|
|
49
49
|
/** MCP server name. Amp exposes its tools as `agent_swarm` / `store_progress` (dashes become underscores). */
|
|
50
50
|
const AMP_MCP_SERVER_NAME = "agent-swarm";
|
|
@@ -1523,8 +1523,10 @@ const DEFAULT_CODEMODE_LIMITS: CodemodeLimits = {
|
|
|
1523
1523
|
* pi has no option for either: a script runs until it returns unless its own
|
|
1524
1524
|
* `// @options` line sets `timeout_ms`. Both limits abort the signal pi hands
|
|
1525
1525
|
* the sandbox, which interrupts the QuickJS worker and fails the codemode call.
|
|
1526
|
-
*
|
|
1527
|
-
*
|
|
1526
|
+
* pi 1.4 caps live codemode workers per process (two unless
|
|
1527
|
+
* `PI_CODEMODE_MAX_WORKERS` raises it) and queues a script for a free one for
|
|
1528
|
+
* at most 60 seconds. The deadline's abort also ends that wait, before the
|
|
1529
|
+
* script starts.
|
|
1528
1530
|
*
|
|
1529
1531
|
* "on" keeps declared tools declared; "only" would hide the lifecycle tools
|
|
1530
1532
|
* behind scripts. `models` stays off unless `PI_CODEMODE_MODELS` asks for it.
|
|
@@ -1551,43 +1553,6 @@ export function createBoundedCodemodeExtension(
|
|
|
1551
1553
|
);
|
|
1552
1554
|
}
|
|
1553
1555
|
|
|
1554
|
-
const CODEMODE_WORKER_RETRY_MIN_MS = 25;
|
|
1555
|
-
const CODEMODE_WORKER_RETRY_MAX_MS = 250;
|
|
1556
|
-
|
|
1557
|
-
/**
|
|
1558
|
-
* pi 1.3 admits at most two live codemode workers per process, across all
|
|
1559
|
-
* sessions, and counts a stopped worker until its thread exits. A call over
|
|
1560
|
-
* that cap fails before the script runs. The runner hosts every task's pi
|
|
1561
|
-
* session in one process and pi runs a turn's tool calls in parallel, so a
|
|
1562
|
-
* third concurrent script, or one started right after another, can be
|
|
1563
|
-
* refused. Nothing ran (no nested calls, no store writes), so it is safe to
|
|
1564
|
-
* retry until a worker frees up, within the same deadline.
|
|
1565
|
-
*/
|
|
1566
|
-
function isCodemodeWorkerLimitRefusal(result: {
|
|
1567
|
-
content: Array<{ type: string; text?: string }>;
|
|
1568
|
-
details?: unknown;
|
|
1569
|
-
isError?: boolean;
|
|
1570
|
-
}): boolean {
|
|
1571
|
-
if (!result.isError) return false;
|
|
1572
|
-
const calls = (result.details as { calls?: unknown[] } | undefined)?.calls;
|
|
1573
|
-
if (calls && calls.length > 0) return false;
|
|
1574
|
-
return result.content.some((item) =>
|
|
1575
|
-
item.text?.includes("Script sandbox failed: Codemode worker limit reached"),
|
|
1576
|
-
);
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
|
-
function abortableSleep(ms: number, signal: AbortSignal): Promise<void> {
|
|
1580
|
-
return new Promise((resolve) => {
|
|
1581
|
-
const timer = setTimeout(done, ms);
|
|
1582
|
-
function done() {
|
|
1583
|
-
clearTimeout(timer);
|
|
1584
|
-
signal.removeEventListener("abort", done);
|
|
1585
|
-
resolve();
|
|
1586
|
-
}
|
|
1587
|
-
signal.addEventListener("abort", done, { once: true });
|
|
1588
|
-
});
|
|
1589
|
-
}
|
|
1590
|
-
|
|
1591
1556
|
export function boundCodemodeTool(tool: ToolDefinition, limits: CodemodeLimits): ToolDefinition {
|
|
1592
1557
|
return {
|
|
1593
1558
|
...tool,
|
|
@@ -1653,20 +1618,7 @@ export function boundCodemodeTool(tool: ToolDefinition, limits: CodemodeLimits):
|
|
|
1653
1618
|
: ctx;
|
|
1654
1619
|
|
|
1655
1620
|
try {
|
|
1656
|
-
|
|
1657
|
-
for (;;) {
|
|
1658
|
-
const result = await tool.execute(
|
|
1659
|
-
toolCallId,
|
|
1660
|
-
params,
|
|
1661
|
-
script.signal,
|
|
1662
|
-
onUpdate,
|
|
1663
|
-
boundedCtx,
|
|
1664
|
-
);
|
|
1665
|
-
if (!isCodemodeWorkerLimitRefusal(result) || script.signal.aborted) return result;
|
|
1666
|
-
await abortableSleep(delayMs, script.signal);
|
|
1667
|
-
if (script.signal.aborted) return result;
|
|
1668
|
-
delayMs = Math.min(delayMs * 2, CODEMODE_WORKER_RETRY_MAX_MS);
|
|
1669
|
-
}
|
|
1621
|
+
return await tool.execute(toolCallId, params, script.signal, onUpdate, boundedCtx);
|
|
1670
1622
|
} finally {
|
|
1671
1623
|
clearTimeout(timer);
|
|
1672
1624
|
signal?.removeEventListener("abort", onOuterAbort);
|
|
@@ -15,6 +15,7 @@ declare module "swarm-extension" {
|
|
|
15
15
|
| "github"
|
|
16
16
|
| "gitlab"
|
|
17
17
|
| "azure-devops"
|
|
18
|
+
| "bitbucket"
|
|
18
19
|
| "system"
|
|
19
20
|
| "linear"
|
|
20
21
|
| "jira"
|
|
@@ -41,7 +42,7 @@ declare module "swarm-extension" {
|
|
|
41
42
|
slackTriggerMessageTs?: string | undefined;
|
|
42
43
|
slackUserId?: string | undefined;
|
|
43
44
|
overrideSlackContext?: boolean | undefined;
|
|
44
|
-
vcsProvider?: "github" | "gitlab" | "azure-devops" | undefined;
|
|
45
|
+
vcsProvider?: "github" | "gitlab" | "azure-devops" | "bitbucket" | undefined;
|
|
45
46
|
vcsRepo?: string | undefined;
|
|
46
47
|
vcsEventType?: string | undefined;
|
|
47
48
|
vcsNumber?: number | undefined;
|
|
@@ -132,6 +133,7 @@ declare module "swarm-extension" {
|
|
|
132
133
|
| "github"
|
|
133
134
|
| "gitlab"
|
|
134
135
|
| "azure-devops"
|
|
136
|
+
| "bitbucket"
|
|
135
137
|
| "system"
|
|
136
138
|
| "linear"
|
|
137
139
|
| "jira"
|
|
@@ -172,7 +174,7 @@ declare module "swarm-extension" {
|
|
|
172
174
|
slackUserId?: string | undefined;
|
|
173
175
|
slackProgressMessageTs?: string | undefined;
|
|
174
176
|
slackTreeRootMessageTs?: string | undefined;
|
|
175
|
-
vcsProvider?: "github" | "gitlab" | "azure-devops" | undefined;
|
|
177
|
+
vcsProvider?: "github" | "gitlab" | "azure-devops" | "bitbucket" | undefined;
|
|
176
178
|
vcsRepo?: string | undefined;
|
|
177
179
|
vcsEventType?: string | undefined;
|
|
178
180
|
vcsNumber?: number | undefined;
|
|
@@ -625,7 +627,7 @@ declare module "swarm-extension" {
|
|
|
625
627
|
/** The text, or a placeholder naming the type for a non-text message. */
|
|
626
628
|
text: string;
|
|
627
629
|
}
|
|
628
|
-
export type VcsProvider = "github" | "gitlab" | "azure-devops";
|
|
630
|
+
export type VcsProvider = "github" | "gitlab" | "azure-devops" | "bitbucket";
|
|
629
631
|
/**
|
|
630
632
|
* The provider object the event is about, as the provider names it. GitLab
|
|
631
633
|
* calls a pull request a merge request, and a comment a `note`.
|
|
@@ -648,7 +650,7 @@ declare module "swarm-extension" {
|
|
|
648
650
|
provider: VcsProvider;
|
|
649
651
|
kind: VcsEventKind;
|
|
650
652
|
action: string;
|
|
651
|
-
/** `owner/name` on GitHub and GitLab, the repository URL on Azure DevOps. */
|
|
653
|
+
/** `owner/name` on GitHub and GitLab, `workspace/repo_slug` on Bitbucket, the repository URL on Azure DevOps. */
|
|
652
654
|
repo: string;
|
|
653
655
|
/** The PR, merge request, or issue number. Absent for a pipeline outside a merge request. */
|
|
654
656
|
number?: number;
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
checkUserAccess,
|
|
21
21
|
isBotMessage,
|
|
22
22
|
isSwarmThreadRoot,
|
|
23
|
+
type isUserAllowed,
|
|
23
24
|
registerMessageHandler,
|
|
24
25
|
type UserFilterConfig,
|
|
25
26
|
} from "./handlers";
|
|
@@ -189,6 +190,163 @@ describe("checkUserAccess", () => {
|
|
|
189
190
|
});
|
|
190
191
|
});
|
|
191
192
|
|
|
193
|
+
describe("isUserAllowed", () => {
|
|
194
|
+
const usersInfo = mock(async () => ({ ok: true, user: {} }));
|
|
195
|
+
const client = { users: { info: usersInfo } } as never;
|
|
196
|
+
let enrichSpy: ReturnType<typeof spyOn<typeof slackEnrichModule, "enrichSlackUserEmail">>;
|
|
197
|
+
let logSpy: ReturnType<typeof spyOn<typeof console, "log">>;
|
|
198
|
+
|
|
199
|
+
// handlers.ts reads SLACK_ALLOWED_* once at module load, so each case evaluates a fresh
|
|
200
|
+
// copy under its own env and calls the production two-argument isUserAllowed. The copy
|
|
201
|
+
// shares ./enrich with this file, so the enrichSlackUserEmail spy still applies.
|
|
202
|
+
const ENV_KEYS = ["SLACK_ALLOWED_EMAIL_DOMAINS", "SLACK_ALLOWED_USER_IDS"] as const;
|
|
203
|
+
let loadCount = 0;
|
|
204
|
+
async function loadIsUserAllowed(env: {
|
|
205
|
+
SLACK_ALLOWED_EMAIL_DOMAINS?: string;
|
|
206
|
+
SLACK_ALLOWED_USER_IDS?: string;
|
|
207
|
+
}): Promise<typeof isUserAllowed> {
|
|
208
|
+
const saved = ENV_KEYS.map((key) => [key, process.env[key]] as const);
|
|
209
|
+
try {
|
|
210
|
+
for (const key of ENV_KEYS) {
|
|
211
|
+
if (env[key] === undefined) delete process.env[key];
|
|
212
|
+
else process.env[key] = env[key];
|
|
213
|
+
}
|
|
214
|
+
loadCount += 1;
|
|
215
|
+
const fresh = (await import(`./handlers?env=${loadCount}`)) as typeof import("./handlers");
|
|
216
|
+
return fresh.isUserAllowed;
|
|
217
|
+
} finally {
|
|
218
|
+
for (const [key, value] of saved) {
|
|
219
|
+
if (value === undefined) delete process.env[key];
|
|
220
|
+
else process.env[key] = value;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
beforeEach(() => {
|
|
226
|
+
usersInfo.mockClear();
|
|
227
|
+
enrichSpy = spyOn(slackEnrichModule, "enrichSlackUserEmail");
|
|
228
|
+
logSpy = spyOn(console, "log").mockImplementation(() => {});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
afterEach(() => {
|
|
232
|
+
enrichSpy.mockRestore();
|
|
233
|
+
logSpy.mockRestore();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const loggedLines = () => logSpy.mock.calls.map((call) => String(call[0]));
|
|
237
|
+
|
|
238
|
+
describe("filtering off (neither env var set)", () => {
|
|
239
|
+
test("allows everyone without an email lookup", async () => {
|
|
240
|
+
const allowed = await loadIsUserAllowed({});
|
|
241
|
+
expect(await allowed(client, "U_ANY")).toBe(true);
|
|
242
|
+
expect(enrichSpy).not.toHaveBeenCalled();
|
|
243
|
+
expect(usersInfo).not.toHaveBeenCalled();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("treats blank and comma-only values as unset", async () => {
|
|
247
|
+
const allowed = await loadIsUserAllowed({
|
|
248
|
+
SLACK_ALLOWED_EMAIL_DOMAINS: " , ",
|
|
249
|
+
SLACK_ALLOWED_USER_IDS: ",",
|
|
250
|
+
});
|
|
251
|
+
expect(await allowed(client, "U_ANY")).toBe(true);
|
|
252
|
+
expect(enrichSpy).not.toHaveBeenCalled();
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe("user IDs only", () => {
|
|
257
|
+
const env = { SLACK_ALLOWED_USER_IDS: " U_LISTED , U_SECOND ," };
|
|
258
|
+
|
|
259
|
+
test("allows trimmed allowlisted IDs without an email lookup", async () => {
|
|
260
|
+
const allowed = await loadIsUserAllowed(env);
|
|
261
|
+
expect(await allowed(client, "U_LISTED")).toBe(true);
|
|
262
|
+
expect(await allowed(client, "U_SECOND")).toBe(true);
|
|
263
|
+
expect(enrichSpy).not.toHaveBeenCalled();
|
|
264
|
+
expect(usersInfo).not.toHaveBeenCalled();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("denies an unlisted ID without an email lookup", async () => {
|
|
268
|
+
enrichSpy.mockResolvedValue("person@company.com");
|
|
269
|
+
const allowed = await loadIsUserAllowed(env);
|
|
270
|
+
expect(await allowed(client, "U_OTHER")).toBe(false);
|
|
271
|
+
expect(enrichSpy).not.toHaveBeenCalled();
|
|
272
|
+
expect(usersInfo).not.toHaveBeenCalled();
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
describe("email domains only", () => {
|
|
277
|
+
const env = { SLACK_ALLOWED_EMAIL_DOMAINS: " Company.COM , other.org " };
|
|
278
|
+
|
|
279
|
+
test("allows trimmed, case-insensitive domains after one lookup", async () => {
|
|
280
|
+
const allowed = await loadIsUserAllowed(env);
|
|
281
|
+
enrichSpy.mockResolvedValue("person@company.com");
|
|
282
|
+
expect(await allowed(client, "U_A")).toBe(true);
|
|
283
|
+
enrichSpy.mockResolvedValue("person@Other.Org");
|
|
284
|
+
expect(await allowed(client, "U_B")).toBe(true);
|
|
285
|
+
expect(enrichSpy).toHaveBeenCalledTimes(2);
|
|
286
|
+
expect(enrichSpy).toHaveBeenNthCalledWith(1, client, "U_A");
|
|
287
|
+
expect(enrichSpy).toHaveBeenNthCalledWith(2, client, "U_B");
|
|
288
|
+
expect(loggedLines()).toEqual([]);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("denies and logs a domain that is not allowed", async () => {
|
|
292
|
+
enrichSpy.mockResolvedValue("person@evil.com");
|
|
293
|
+
const allowed = await loadIsUserAllowed(env);
|
|
294
|
+
expect(await allowed(client, "U_OTHER")).toBe(false);
|
|
295
|
+
expect(loggedLines()).toEqual([
|
|
296
|
+
'[Slack] User U_OTHER email domain "evil.com" not in allowed list',
|
|
297
|
+
]);
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
describe("user IDs and email domains", () => {
|
|
302
|
+
const env = {
|
|
303
|
+
SLACK_ALLOWED_EMAIL_DOMAINS: "company.com",
|
|
304
|
+
SLACK_ALLOWED_USER_IDS: "U_LISTED",
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
test("allows an allowlisted ID without an email lookup", async () => {
|
|
308
|
+
const allowed = await loadIsUserAllowed(env);
|
|
309
|
+
expect(await allowed(client, "U_LISTED")).toBe(true);
|
|
310
|
+
expect(enrichSpy).not.toHaveBeenCalled();
|
|
311
|
+
expect(usersInfo).not.toHaveBeenCalled();
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test("allows an unlisted ID whose email domain is allowed", async () => {
|
|
315
|
+
enrichSpy.mockResolvedValue("person@Company.com");
|
|
316
|
+
const allowed = await loadIsUserAllowed(env);
|
|
317
|
+
expect(await allowed(client, "U_OTHER")).toBe(true);
|
|
318
|
+
expect(enrichSpy).toHaveBeenCalledTimes(1);
|
|
319
|
+
expect(enrichSpy).toHaveBeenCalledWith(client, "U_OTHER");
|
|
320
|
+
expect(loggedLines()).toEqual([]);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test("denies and logs a domain that is not allowed", async () => {
|
|
324
|
+
enrichSpy.mockResolvedValue("person@other.com");
|
|
325
|
+
const allowed = await loadIsUserAllowed(env);
|
|
326
|
+
expect(await allowed(client, "U_OTHER")).toBe(false);
|
|
327
|
+
expect(loggedLines()).toEqual([
|
|
328
|
+
'[Slack] User U_OTHER email domain "other.com" not in allowed list',
|
|
329
|
+
]);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test("denies and logs a user with no email", async () => {
|
|
333
|
+
enrichSpy.mockResolvedValue(null);
|
|
334
|
+
const allowed = await loadIsUserAllowed(env);
|
|
335
|
+
expect(await allowed(client, "U_OTHER")).toBe(false);
|
|
336
|
+
expect(loggedLines()).toEqual(["[Slack] User U_OTHER has no email, denying access"]);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("denies and logs a user with a malformed email", async () => {
|
|
340
|
+
enrichSpy.mockResolvedValue("no-at-sign");
|
|
341
|
+
const allowed = await loadIsUserAllowed(env);
|
|
342
|
+
expect(await allowed(client, "U_OTHER")).toBe(false);
|
|
343
|
+
expect(loggedLines()).toEqual([
|
|
344
|
+
"[Slack] User U_OTHER has invalid email format, denying access",
|
|
345
|
+
]);
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
192
350
|
// Helper to create a minimal SlackFile for testing
|
|
193
351
|
function makeFile(overrides: Partial<SlackFile> = {}): SlackFile {
|
|
194
352
|
return {
|