@desplega.ai/agent-swarm 2.4.4 → 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-thq04adh.js → acp-adapter-11gp67nr.js} +8 -6
- package/dist/{actions-j6ywh6x5.js → actions-8p2kze8j.js} +24 -22
- package/dist/{amp-adapter-k8b5xmj8.js → amp-adapter-z4qvhacj.js} +2 -2
- package/dist/{anthropic-messages-g6pqmqk4.js → anthropic-messages-txs190dd.js} +17 -20
- package/dist/{app-rb7vc02s.js → app-0qp0a2gk.js} +3 -3
- package/dist/{assistant-ftta1zjf.js → assistant-jedd3ktm.js} +27 -25
- package/dist/{azure-openai-responses-9dbmj346.js → azure-openai-responses-9kfwhmn7.js} +18 -13
- package/dist/{boot-reembed-xns7h5h9.js → boot-reembed-4xwq5tg0.js} +1 -1
- package/dist/{boot-reembed-srp6v6af.js → boot-reembed-n4wq5dd2.js} +1 -1
- package/dist/{boot-scrub-sweep-3qqrh2mz.js → boot-scrub-sweep-69h90xcq.js} +4 -4
- package/dist/{boot-seeds-9ks8hce7.js → boot-seeds-3vn0m9sg.js} +4 -4
- package/dist/{claude-adapter-k5ksyres.js → claude-adapter-221mvad5.js} +8 -6
- package/dist/{claude-managed-adapter-h2y889ge.js → claude-managed-adapter-gt6pa35d.js} +2 -2
- package/dist/{claude-managed-setup-n5a5eqks.js → claude-managed-setup-c3hp36ph.js} +4 -4
- package/dist/{claude-sdk-session-2j3j4kwn.js → claude-sdk-session-8thjsk5e.js} +9729 -9628
- package/dist/{cli-b9fcxxev.js → cli-007cnhm1.js} +50 -4
- package/dist/{cli-j4rqdf2n.js → cli-03vpq45j.js} +2 -2
- package/dist/{cli-amyhm3gj.js → cli-0y6f0ckq.js} +1 -1
- package/dist/{cli-4dg55wsk.js → cli-0z3vjrpm.js} +1 -1
- package/dist/{cli-pccfmtb5.js → cli-179xcyky.js} +11 -11
- package/dist/{cli-3r8samv3.js → cli-1f0gp9vz.js} +3 -2
- package/dist/{cli-hj3debgw.js → cli-1pr672hz.js} +3 -3
- package/dist/{cli-0bqteszy.js → cli-2nf3b08t.js} +328 -25
- package/dist/{cli-t7cyef5g.js → cli-36j4jnk3.js} +4 -4
- package/dist/{cli-er83cjbn.js → cli-3qv8naq8.js} +527 -231
- package/dist/cli-3zwye9sn.js +1193 -0
- package/dist/{cli-60cgh1rv.js → cli-459fk97x.js} +7 -7
- package/dist/{cli-xj0re5tm.js → cli-4w24f7z3.js} +1 -1
- package/dist/{cli-v3yr2gbg.js → cli-5kzk3472.js} +2198 -968
- package/dist/{cli-h7gc0b4r.js → cli-5m84zd2k.js} +55 -18
- package/dist/{cli-w6nadkh4.js → cli-5rvg1ddk.js} +5 -5
- package/dist/{cli-dvkmp9bt.js → cli-6ctwrf1z.js} +20 -10
- package/dist/{cli-9qtjfd60.js → cli-6rg2jn2y.js} +3 -2
- package/dist/{cli-2v045xyf.js → cli-7bwtc6yx.js} +3 -3
- package/dist/{cli-3cgngyd8.js → cli-7g40g8hq.js} +35 -17
- package/dist/{cli-q6r7t299.js → cli-92mqenkc.js} +2379 -154
- package/dist/{cli-cynvc9fj.js → cli-9ce2xw98.js} +8 -8
- package/dist/{cli-gqvh8d8a.js → cli-bgdtv62r.js} +36 -22
- 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-p20r8w42.js → cli-fcc5qe3f.js} +763 -268
- package/dist/{cli-1ephgwnq.js → cli-fes51ya2.js} +3 -3
- package/dist/{cli-1d70crsy.js → cli-ftmq4nvk.js} +2 -2
- package/dist/{cli-qkhc5aqy.js → cli-fy1jyyym.js} +13 -13
- package/dist/{cli-s95dknxh.js → cli-g1cw7xtt.js} +1 -1
- package/dist/{cli-qje9k2nh.js → cli-g9ypd5en.js} +12 -2
- package/dist/{cli-74c1wpm0.js → cli-gpv60278.js} +1 -1
- package/dist/{cli-pemywhfq.js → cli-ha9rmwgq.js} +2 -1
- package/dist/{cli-bnz0r3kc.js → cli-j1eghqfm.js} +5 -5
- package/dist/{cli-2zqpkvtt.js → cli-jpxsrxcb.js} +1 -1
- package/dist/{cli-ejs3m1sx.js → cli-kxpfx2xr.js} +1 -1
- package/dist/{cli-37yb7307.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-a0kcty51.js → cli-n6kmqngw.js} +2 -2
- package/dist/{cli-te6b5wvg.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-zrwpctp4.js → cli-r25qpaxt.js} +161 -63
- package/dist/{cli-nc35pxb7.js → cli-rh5smt8t.js} +19 -4
- package/dist/{cli-qwkcwjdn.js → cli-rycdaj0a.js} +1 -1
- package/dist/{cli-esxerzg7.js → cli-s4sntwt4.js} +2 -2
- package/dist/{cli-b8k7epj1.js → cli-segs7hx9.js} +8 -8
- package/dist/{cli-36ckw0wx.js → cli-stxdrqs2.js} +2 -2
- package/dist/{cli-vdm20wej.js → cli-sy8f9705.js} +4 -4
- package/dist/{cli-1m17dc9d.js → cli-ta8bd7th.js} +2 -2
- package/dist/{cli-2y91wnbh.js → cli-we0ra057.js} +222 -10
- package/dist/{cli-55qmbp6z.js → cli-x21dqn18.js} +5 -5
- package/dist/{cli-dqb4n629.js → cli-x6vf4sad.js} +11 -11
- package/dist/{cli-d2mb5s8a.js → cli-x77gen2p.js} +680 -440
- package/dist/{cli-vq97g1jq.js → cli-xawhpgc9.js} +95 -195
- package/dist/{cli-82d3nkc3.js → cli-xd4eqtr2.js} +2 -2
- package/dist/{cli-9fhkkpj5.js → cli-xt768r5m.js} +2 -2
- package/dist/{cli-zrk6jxv8.js → cli-y4x8ww6s.js} +2 -2
- package/dist/{cli-xc27fwzg.js → cli-y9jvxfhk.js} +3 -3
- package/dist/{cli-44159k28.js → cli-z63a269p.js} +2 -2
- package/dist/{cli-sm8rses9.js → cli-zfdp01hn.js} +8 -5
- package/dist/cli.js +31 -29
- package/dist/{cloudflare-workers-ai-system-one-ch2wxfcp.js → cloudflare-workers-ai-system-one-43v9qkjk.js} +7 -2
- package/dist/{codex-adapter-968xwfy7.js → codex-adapter-tcg1q3jk.js} +6 -4
- package/dist/{codex-login-m819c41r.js → codex-login-rn7c5gxt.js} +1 -1
- package/dist/{codex-session-runner-ht4z99fa.js → codex-session-runner-xfx4wd9x.js} +7 -5
- package/dist/{commands-pcymaeg9.js → commands-ew83nj70.js} +2 -2
- package/dist/{controller-fpjyk46r.js → controller-gnq1k1mf.js} +4 -4
- package/dist/{controllers-connect-mwzx1fh6.js → controllers-connect-tw1dan9r.js} +26 -24
- package/dist/{cursor-adapter-cp4s2e2f.js → cursor-adapter-yq5xgfcg.js} +1 -1
- package/dist/{db-xsjg9jqq.js → db-7gbc53fd.js} +14 -2
- package/dist/{db-sgd12bt9.js → db-t86xrnes.js} +3 -3
- package/dist/{devin-adapter-zgqadvnm.js → devin-adapter-4ycy4vrf.js} +1 -1
- package/dist/{dispatcher-x0jfw2c9.js → dispatcher-64bdyhps.js} +23 -21
- package/dist/{docker-fkgtr75t.js → docker-nfy2gc8t.js} +2 -2
- package/dist/{dsh-adapter-jrq7ma7h.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-8njj4h3b.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-kkr97v2f.js → grok-adapter-v82srhrx.js} +9 -7
- package/dist/handlers-c9ajzhbn.js +41 -0
- package/dist/{heartbeat-wd03zhn0.js → heartbeat-2vqs1319.js} +26 -24
- package/dist/{hook-xzj2ts2p.js → hook-01nk13wx.js} +7 -5
- package/dist/{hook-f0vghj42.js → hook-jzpheegg.js} +9 -7
- package/dist/{http-4hwxv62p.js → http-fc1k6ec8.js} +859 -252
- 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-c4ykd0f1.js → index-dr94zbnh.js} +13348 -11611
- package/dist/index-q3t40ryv.js +46 -0
- package/dist/{index-ww2vznkp.js → index-sepja052.js} +1 -1
- package/dist/{index-97q4xn03.js → index-zdycv3ja.js} +135 -79
- package/dist/{index-g216q6jr.js → index-zsyx51ap.js} +1 -4
- package/dist/{keepalive-hrh7xnr9.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-e8wnwnqx.js → maintenance-hjhzdt3k.js} +1 -1
- package/dist/{mistral-conversations-1medfwv7.js → mistral-conversations-sd0xf1tn.js} +13 -13
- package/dist/{node-3v3f08f7.js → node-jzjj7r9x.js} +10 -9
- package/dist/{oauth-refresh-sweep-zm0yj4sj.js → oauth-refresh-sweep-y3n1knmd.js} +3 -3
- package/dist/{onboard-dtwcq603.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-w3fbhpn0.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-j38dehfs.js → pricing-refresh-nevhat8h.js} +3 -3
- package/dist/{rbac-roles-ae8yttq4.js → rbac-roles-4y6ngvzq.js} +1 -1
- package/dist/{rbac-roles-ejxx9b35.js → rbac-roles-exbg8xb6.js} +2 -2
- package/dist/{registry-rxw10xzg.js → registry-ajhexe44.js} +1 -1
- package/dist/{render-v2-n16n0phe.js → render-v2-cfsh13z3.js} +3 -3
- package/dist/{resume-z0fgjc38.js → resume-5s2t0dbs.js} +10 -8
- package/dist/{runtime-xe3aqy4x.js → runtime-bt5pkpm1.js} +1 -1
- package/dist/{session-access-cv3mq6mc.js → session-access-dnbyp3cn.js} +6 -4
- package/dist/{setup-mafhj3yt.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-8b86mgjp.js → work-objects-7mg6216g.js} +26 -24
- package/dist/worker-nwb789mg.js +79 -0
- package/dist/{x-y45mthwn.js → x-pbpezr0s.js} +2 -2
- package/openapi.json +793 -6
- package/package.json +11 -11
- package/src/apps/catalog.generated.json +206 -4
- package/src/apps/page-validator.ts +13 -1
- package/src/assets/key.ts +17 -0
- package/src/azure-devops/handlers.ts +1 -1
- package/src/be/chunking.ts +42 -9
- 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/draft-text.ts +15 -0
- package/src/be/db/tasks/read.ts +54 -5
- package/src/be/db/workflow-runs.ts +96 -0
- package/src/be/db.ts +85 -237
- package/src/be/memory/index-content.ts +36 -17
- package/src/be/memory/providers/openai-embedding.ts +115 -61
- package/src/be/memory/providers/sqlite-store.ts +69 -23
- package/src/be/memory/reranker.ts +31 -1
- package/src/be/memory/search-candidates.ts +47 -0
- package/src/be/memory/types.ts +14 -0
- package/src/be/onboarding.ts +3 -0
- package/src/be/scripts/typecheck.ts +2 -0
- package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +106 -54
- package/src/be/seed-scripts/index.ts +2 -2
- package/src/be/skill-sync.ts +43 -2
- package/src/be/swarm-config-guard.ts +25 -1
- package/src/be/task-attachment-store.ts +30 -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/catalog.generated.json +4 -4
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +9 -2
- package/src/extensions/post-bridge.ts +1 -0
- package/src/fs/provider.ts +13 -1
- package/src/github/handlers.ts +6 -6
- package/src/gitlab/handlers.ts +5 -5
- package/src/heartbeat/heartbeat.ts +31 -11
- package/src/http/assets.ts +14 -2
- package/src/http/core.ts +12 -0
- package/src/http/fs.ts +54 -34
- package/src/http/index.ts +4 -0
- package/src/http/mcp-body.ts +37 -0
- package/src/http/mcp-user.ts +3 -5
- package/src/http/mcp.ts +6 -7
- package/src/http/memory.ts +26 -15
- package/src/http/onboarding.ts +1 -0
- package/src/http/page-proxy.ts +61 -5
- package/src/http/pages-public.ts +27 -44
- package/src/http/pages.ts +4 -4
- package/src/http/script-runs.ts +2 -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 +13 -1
- package/src/http/webhooks.ts +141 -0
- package/src/integrations/chat-sdk/adapters.ts +24 -4
- package/src/integrations/chat-sdk/attachments.ts +90 -12
- package/src/integrations/chat-sdk/facts.ts +138 -0
- package/src/integrations/chat-sdk/inbound.ts +156 -8
- package/src/integrations/chat-sdk/limits.ts +24 -0
- package/src/integrations/chat-sdk/outbound-files.ts +186 -0
- package/src/integrations/chat-sdk/outbound.ts +272 -5
- package/src/integrations/chat-sdk/relay.ts +4 -1
- package/src/integrations/chat-sdk/router.ts +28 -2
- package/src/integrations/chat-sdk/steering.ts +9 -0
- package/src/integrations/chat-sdk/templates.ts +14 -0
- package/src/integrations/chat-sdk/thread-buffer.ts +4 -0
- package/src/mcp-limits.ts +20 -0
- package/src/pages/browser-sdk.ts +8 -3
- package/src/pages/feedback-overlay.ts +1 -1
- package/src/providers/acp-targets.ts +1 -1
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/codex-adapter.ts +106 -119
- package/src/providers/codex-agents-md.ts +35 -229
- package/src/providers/cursor-adapter.ts +1 -1
- package/src/providers/pi-mono-adapter.ts +5 -53
- package/src/realtime/transport.ts +1 -2
- package/src/script-workflows/workflow-ctx.ts +1 -0
- package/src/scripts-runtime/types/stdlib.d.ts +2 -0
- package/src/scripts-runtime/types/swarm-extension.d.ts +14 -4
- package/src/scripts-runtime/types/swarm-sdk.d.ts +2 -0
- package/src/server-user.ts +2 -0
- package/src/server.ts +2 -0
- package/src/slack/assistant.ts +2 -2
- package/src/slack/files.ts +2 -0
- package/src/slack/handlers.test.ts +158 -0
- package/src/slack/handlers.ts +23 -32
- package/src/slack/inbound-files.ts +39 -6
- package/src/slack/thread-buffer.ts +2 -1
- package/src/tasks/context-key.ts +61 -1
- package/src/tasks/task-terminal-effects.ts +21 -17
- package/src/tests/apps-card-grid.test.ts +197 -0
- package/src/tests/apps-catalog-additions.test.ts +120 -0
- package/src/tests/apps-tree.test.ts +129 -0
- 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-attachments-reply.test.ts +52 -0
- package/src/tests/chat-sdk-config.test.ts +11 -0
- package/src/tests/chat-sdk-facts.test.ts +23 -0
- package/src/tests/chat-sdk-inbound.test.ts +597 -4
- package/src/tests/chat-sdk-outbound-files.test.ts +652 -0
- package/src/tests/chat-sdk-telegram-markdown.test.ts +92 -0
- package/src/tests/codex-adapter.test.ts +167 -242
- package/src/tests/config-api-only-keys.test.ts +9 -0
- package/src/tests/context-key.test.ts +50 -0
- package/src/tests/core-auth.test.ts +5 -0
- package/src/tests/cors-allowlist.test.ts +1 -1
- 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/extensions-example-slack-reply-gate.test.ts +262 -91
- 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/heartbeat-checklist.test.ts +33 -0
- package/src/tests/heartbeat-jev-gate-credentials.test.ts +44 -0
- package/src/tests/heartbeat-jev-gate.test.ts +242 -1
- package/src/tests/integration-disable-flags.test.ts +36 -0
- package/src/tests/kv-page-proxy.test.ts +91 -5
- package/src/tests/launch-password-rejection.test.ts +2 -2
- package/src/tests/mcp-input-caps.test.ts +294 -0
- package/src/tests/mcp-tools.test.ts +1 -1
- package/src/tests/memory-chunked-search-starvation.test.ts +200 -0
- package/src/tests/memory-embedding-input-cap.test.ts +114 -0
- package/src/tests/memory.test.ts +14 -0
- package/src/tests/onboarding.test.ts +12 -0
- package/src/tests/page-guest-session.test.ts +19 -8
- package/src/tests/page-proxy-authed.test.ts +1 -1
- package/src/tests/page-proxy.test.ts +17 -10
- package/src/tests/page-session.test.ts +102 -1
- package/src/tests/pages-authed-mode.test.ts +82 -12
- package/src/tests/pages-password-mode.test.ts +5 -5
- 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/realtime-transport.test.ts +13 -7
- 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/script-runs-http.test.ts +84 -0
- package/src/tests/scripts-http.test.ts +10 -1
- 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 +23 -3
- package/src/tests/task-audio-transcription.test.ts +299 -0
- package/src/tests/task-completion-memory-chunking.test.ts +434 -0
- package/src/tests/task-terminal-secret-scrub.test.ts +6 -1
- package/src/tests/transcription.test.ts +299 -0
- 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-system-one-decision-providers.test.ts +98 -1
- package/src/tests/workflow-system-one-decision.test.ts +2 -1
- package/src/tests/workflow-tla-races.test.ts +2 -2
- package/src/tools/chat-access.ts +10 -0
- package/src/tools/chat-post.ts +31 -11
- package/src/tools/chat-reply.ts +32 -10
- package/src/tools/memory-search.ts +3 -1
- package/src/tools/script-delete.ts +1 -1
- package/src/tools/system-one-decide.ts +17 -4
- package/src/tools/workflows/create-workflow.ts +5 -3
- package/src/types.ts +9 -2
- package/src/utils/config-env-export.ts +4 -1
- package/src/utils/page-session.ts +133 -20
- package/src/utils/secret-scrubber.ts +4 -0
- package/src/utils/skills-refresh.ts +83 -43
- package/src/utils/transcription.ts +246 -0
- 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/executors/system-one-decision.ts +28 -7
- package/src/workflows/executors/system-one-providers.ts +39 -4
- 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/extensions/slack-reply-gate/README.md +24 -22
- package/templates/extensions/slack-reply-gate/hooks.ts +146 -95
- package/templates/extensions/slack-reply-gate/manifest.json +2 -2
- package/templates/skills/apps/SKILL.md +28 -6
- package/templates/skills/apps/content.md +28 -6
- package/templates/skills/chat-sdk/content.md +6 -2
- package/templates/skills/code-quality/content.md +2 -1
- package/templates/skills/heartbeat-runbook/content.md +5 -5
- package/templates/skills/pages/content.md +6 -4
- package/templates/skills/slack-interaction/content.md +5 -0
- package/templates/skills/swarm-scripts/SKILL.md +2 -2
- package/templates/skills/swarm-scripts/content.md +2 -2
- package/templates/skills/work-on-task/SKILL.md +1 -1
- package/templates/skills/work-on-task/content.md +1 -1
- package/dist/cli-51y09c6p.js +0 -2657
- package/dist/cli-yw91rnmw.js +0 -13
- package/dist/handlers-0pvee9nw.js +0 -39
- package/dist/index-gdh8jrdm.js +0 -43
- package/dist/index-hg0x2cdv.js +0 -40
- package/dist/index-thm0y6a8.js +0 -41
- package/dist/lead-ee8vazz2.js +0 -77
- package/dist/worker-16s75hm5.js +0 -77
|
@@ -159,8 +159,7 @@ async function authenticate(req: IncomingMessage): Promise<AuthenticatedConnecti
|
|
|
159
159
|
throw new Error("Invalid page origin");
|
|
160
160
|
const page = await getPage(pageId);
|
|
161
161
|
if (!page) throw new Error("Page not found");
|
|
162
|
-
const
|
|
163
|
-
const session = cookie?.pageId === pageId ? cookie : null;
|
|
162
|
+
const session = await extractAndVerifyCookie(req, pageId);
|
|
164
163
|
if (!session && page.authMode !== "public") throw new Error("Page session required");
|
|
165
164
|
if (session?.uid && (await findUserById(session.uid))?.status !== "active")
|
|
166
165
|
throw new Error("Viewer is inactive");
|
|
@@ -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;
|
|
@@ -67,6 +68,7 @@ declare module "swarm-extension" {
|
|
|
67
68
|
sourceTaskId?: string | undefined;
|
|
68
69
|
outputSchema?: Record<string, unknown> | undefined;
|
|
69
70
|
inheritParentOutputSchema?: boolean | undefined;
|
|
71
|
+
inheritParentRequester?: boolean | undefined;
|
|
70
72
|
inheritParentRoutingAffinity?: boolean | undefined;
|
|
71
73
|
inheritParentFollowUpConfig?: boolean | undefined;
|
|
72
74
|
followUpConfig?:
|
|
@@ -131,6 +133,7 @@ declare module "swarm-extension" {
|
|
|
131
133
|
| "github"
|
|
132
134
|
| "gitlab"
|
|
133
135
|
| "azure-devops"
|
|
136
|
+
| "bitbucket"
|
|
134
137
|
| "system"
|
|
135
138
|
| "linear"
|
|
136
139
|
| "jira"
|
|
@@ -171,7 +174,7 @@ declare module "swarm-extension" {
|
|
|
171
174
|
slackUserId?: string | undefined;
|
|
172
175
|
slackProgressMessageTs?: string | undefined;
|
|
173
176
|
slackTreeRootMessageTs?: string | undefined;
|
|
174
|
-
vcsProvider?: "github" | "gitlab" | "azure-devops" | undefined;
|
|
177
|
+
vcsProvider?: "github" | "gitlab" | "azure-devops" | "bitbucket" | undefined;
|
|
175
178
|
vcsRepo?: string | undefined;
|
|
176
179
|
vcsEventType?: string | undefined;
|
|
177
180
|
vcsNumber?: number | undefined;
|
|
@@ -485,8 +488,15 @@ declare module "swarm-extension" {
|
|
|
485
488
|
userName?: string;
|
|
486
489
|
/** The buffered messages, joined. */
|
|
487
490
|
text: string;
|
|
491
|
+
/** True for an @mention and for a reply to one of the bot's messages. */
|
|
488
492
|
botMentioned: boolean;
|
|
489
493
|
isDirectMessage: boolean;
|
|
494
|
+
/** Platform chat type, such as Telegram `private`, `group`, `supergroup` or `channel`. */
|
|
495
|
+
chatType?: string;
|
|
496
|
+
/** Set for a Telegram supergroup with topics. */
|
|
497
|
+
isForum?: boolean;
|
|
498
|
+
/** A buffered message replies to one of the bot's own messages. */
|
|
499
|
+
replyToBot: boolean;
|
|
490
500
|
/** Set when earlier swarm tasks exist in this thread. */
|
|
491
501
|
threadContext?: {
|
|
492
502
|
adapter: string;
|
|
@@ -617,7 +627,7 @@ declare module "swarm-extension" {
|
|
|
617
627
|
/** The text, or a placeholder naming the type for a non-text message. */
|
|
618
628
|
text: string;
|
|
619
629
|
}
|
|
620
|
-
export type VcsProvider = "github" | "gitlab" | "azure-devops";
|
|
630
|
+
export type VcsProvider = "github" | "gitlab" | "azure-devops" | "bitbucket";
|
|
621
631
|
/**
|
|
622
632
|
* The provider object the event is about, as the provider names it. GitLab
|
|
623
633
|
* calls a pull request a merge request, and a comment a `note`.
|
|
@@ -640,7 +650,7 @@ declare module "swarm-extension" {
|
|
|
640
650
|
provider: VcsProvider;
|
|
641
651
|
kind: VcsEventKind;
|
|
642
652
|
action: string;
|
|
643
|
-
/** `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. */
|
|
644
654
|
repo: string;
|
|
645
655
|
/** The PR, merge request, or issue number. Absent for a pipeline outside a merge request. */
|
|
646
656
|
number?: number;
|
package/src/server-user.ts
CHANGED
|
@@ -4,6 +4,7 @@ import * as z from "zod";
|
|
|
4
4
|
import pkg from "../package.json";
|
|
5
5
|
import { enqueueAdmissionRow } from "./be/rbac-audit";
|
|
6
6
|
import { getUserGrant } from "./be/rbac-roles";
|
|
7
|
+
import { MCP_MAX_TOOL_INPUT_ELEMENTS } from "./mcp-limits";
|
|
7
8
|
import {
|
|
8
9
|
type AdmissionRbac,
|
|
9
10
|
decideToolAdmission,
|
|
@@ -107,6 +108,7 @@ export function createUserServer(user: User): McpServer {
|
|
|
107
108
|
capabilities: {
|
|
108
109
|
logging: {},
|
|
109
110
|
},
|
|
111
|
+
maxToolInputElements: MCP_MAX_TOOL_INPUT_ELEMENTS,
|
|
110
112
|
},
|
|
111
113
|
);
|
|
112
114
|
|
package/src/server.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { isSteeringEnabled } from "./be/steering";
|
|
|
7
7
|
import { registerGithubTaskReactions } from "./github/task-reactions";
|
|
8
8
|
import { loadGlobalConfigsIntoEnv } from "./http/core";
|
|
9
9
|
import { isChatSdkEnabled } from "./integrations/chat-sdk/config";
|
|
10
|
+
import { MCP_MAX_TOOL_INPUT_ELEMENTS } from "./mcp-limits";
|
|
10
11
|
import { resolveTemplate } from "./prompts/resolver";
|
|
11
12
|
import { isRbacEnabled } from "./rbac";
|
|
12
13
|
import { getSlackConfiguration } from "./slack/config";
|
|
@@ -450,6 +451,7 @@ export async function createServer(
|
|
|
450
451
|
capabilities: {
|
|
451
452
|
logging: {},
|
|
452
453
|
},
|
|
454
|
+
maxToolInputElements: MCP_MAX_TOOL_INPUT_ELEMENTS,
|
|
453
455
|
},
|
|
454
456
|
);
|
|
455
457
|
|
package/src/slack/assistant.ts
CHANGED
|
@@ -204,13 +204,13 @@ export function createAssistant(): Assistant {
|
|
|
204
204
|
// Fetch shared files before the task text is final: a file that can't be
|
|
205
205
|
// downloaded is flagged in it.
|
|
206
206
|
if (files?.length) await safeSetStatus("Downloading attachments...");
|
|
207
|
-
await using inbound = await fetchSlackFiles(client, files);
|
|
207
|
+
await using inbound = await fetchSlackFiles(client, files, { transcribe: true });
|
|
208
208
|
// Any in-body `<@U…>` mention the requester typed is rewritten via
|
|
209
209
|
// the identity primitive before it reaches agent-visible task text —
|
|
210
210
|
// never a raw Slack ID. Bot-mention routing checks above use the
|
|
211
211
|
// raw `messageText`, not this rendered copy.
|
|
212
212
|
const renderedMessageText = await rewriteSlackMentions(
|
|
213
|
-
buildEffectiveText(messageText, inbound.files, inbound.failed),
|
|
213
|
+
buildEffectiveText(messageText, inbound.files, inbound.failed, inbound.notes),
|
|
214
214
|
cachedBotUserId ?? undefined,
|
|
215
215
|
);
|
|
216
216
|
|
package/src/slack/files.ts
CHANGED
|
@@ -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 {
|
package/src/slack/handlers.ts
CHANGED
|
@@ -79,8 +79,6 @@ const allowedUserIds = (process.env.SLACK_ALLOWED_USER_IDS || "")
|
|
|
79
79
|
.map((id) => id.trim())
|
|
80
80
|
.filter(Boolean);
|
|
81
81
|
|
|
82
|
-
const filteringEnabled = allowedEmailDomains.length > 0 || allowedUserIds.length > 0;
|
|
83
|
-
|
|
84
82
|
/**
|
|
85
83
|
* Configuration for user filtering.
|
|
86
84
|
*/
|
|
@@ -89,6 +87,8 @@ export interface UserFilterConfig {
|
|
|
89
87
|
allowedUserIds: string[];
|
|
90
88
|
}
|
|
91
89
|
|
|
90
|
+
const envUserFilterConfig: UserFilterConfig = { allowedEmailDomains, allowedUserIds };
|
|
91
|
+
|
|
92
92
|
/**
|
|
93
93
|
* Core logic for checking if a user is allowed based on email and/or user ID.
|
|
94
94
|
* Exported for testing.
|
|
@@ -137,40 +137,31 @@ export function checkUserAccess(
|
|
|
137
137
|
/**
|
|
138
138
|
* Check if a user is allowed to interact with the swarm.
|
|
139
139
|
* Returns true if filtering is disabled, user is in whitelist, or user's email domain is allowed.
|
|
140
|
+
* The allow/deny decision is `checkUserAccess`; this wrapper only adds the lazy email lookup
|
|
141
|
+
* and the deny logs. The config comes from the SLACK_ALLOWED_* env vars, read at module load.
|
|
140
142
|
*/
|
|
141
143
|
export async function isUserAllowed(client: WebClient, userId: string): Promise<boolean> {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (allowedUserIds.includes(userId)) {
|
|
149
|
-
return true;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// No email domains configured and not in user whitelist
|
|
153
|
-
if (allowedEmailDomains.length === 0) {
|
|
154
|
-
return false;
|
|
144
|
+
const config = envUserFilterConfig;
|
|
145
|
+
// Decide without an email first: filtering off and allowlisted IDs pass, and with no
|
|
146
|
+
// email domains configured an unlisted ID is denied. Only domain checks need a lookup.
|
|
147
|
+
const allowedWithoutEmail = checkUserAccess(userId, null, config);
|
|
148
|
+
if (allowedWithoutEmail || config.allowedEmailDomains.length === 0) {
|
|
149
|
+
return allowedWithoutEmail;
|
|
155
150
|
}
|
|
156
151
|
|
|
157
152
|
// Check email domain — uses kv-backed enrichment (24h TTL, only success cached).
|
|
158
153
|
const email = await enrichSlackUserEmail(client, userId);
|
|
154
|
+
const allowed = checkUserAccess(userId, email, config);
|
|
159
155
|
|
|
160
|
-
if (!email) {
|
|
161
|
-
console.log(`[Slack] User ${userId} has no email, denying access`);
|
|
162
|
-
return false;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const domain = email.split("@")[1]?.toLowerCase();
|
|
166
|
-
if (!domain) {
|
|
167
|
-
console.log(`[Slack] User ${userId} has invalid email format, denying access`);
|
|
168
|
-
return false;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const allowed = allowedEmailDomains.includes(domain);
|
|
172
156
|
if (!allowed) {
|
|
173
|
-
|
|
157
|
+
const domain = email?.split("@")[1]?.toLowerCase();
|
|
158
|
+
if (!email) {
|
|
159
|
+
console.log(`[Slack] User ${userId} has no email, denying access`);
|
|
160
|
+
} else if (!domain) {
|
|
161
|
+
console.log(`[Slack] User ${userId} has invalid email format, denying access`);
|
|
162
|
+
} else {
|
|
163
|
+
console.log(`[Slack] User ${userId} email domain "${domain}" not in allowed list`);
|
|
164
|
+
}
|
|
174
165
|
}
|
|
175
166
|
return allowed;
|
|
176
167
|
}
|
|
@@ -702,8 +693,8 @@ export function registerMessageHandler(app: App): void {
|
|
|
702
693
|
return ignoreSlackInbound("rate_limited");
|
|
703
694
|
}
|
|
704
695
|
|
|
705
|
-
await using inbound = await fetchSlackFiles(client, msg.files);
|
|
706
|
-
const taskText = buildEffectiveText(msg.text, inbound.files, inbound.failed);
|
|
696
|
+
await using inbound = await fetchSlackFiles(client, msg.files, { transcribe: true });
|
|
697
|
+
const taskText = buildEffectiveText(msg.text, inbound.files, inbound.failed, inbound.notes);
|
|
707
698
|
const taskDescription = isImplicitMention
|
|
708
699
|
? taskText
|
|
709
700
|
: extractTaskFromMessage(taskText, botUserId);
|
|
@@ -778,9 +769,9 @@ export function registerMessageHandler(app: App): void {
|
|
|
778
769
|
}
|
|
779
770
|
|
|
780
771
|
// Extract task description (the text plus a `[File: …]` line per attachment)
|
|
781
|
-
await using inbound = await fetchSlackFiles(client, msg.files);
|
|
772
|
+
await using inbound = await fetchSlackFiles(client, msg.files, { transcribe: true });
|
|
782
773
|
const taskDescription = extractTaskFromMessage(
|
|
783
|
-
buildEffectiveText(msg.text, inbound.files, inbound.failed),
|
|
774
|
+
buildEffectiveText(msg.text, inbound.files, inbound.failed, inbound.notes),
|
|
784
775
|
botUserId,
|
|
785
776
|
);
|
|
786
777
|
if (!taskDescription) {
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
* could not be attached — never dropped silently. A download failure is
|
|
14
14
|
* also flagged on that line; a storage failure happens after the text is
|
|
15
15
|
* written, so it shows up only in the reply and in the missing attachment.
|
|
16
|
+
* Audio files (voice clips) a user shares get their transcript on the next
|
|
17
|
+
* line when transcription is configured, and keep the audio attached.
|
|
16
18
|
*/
|
|
17
19
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
18
20
|
import { tmpdir } from "node:os";
|
|
@@ -27,6 +29,7 @@ import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
|
|
|
27
29
|
import type { AgentTask, CreateTaskOptions, TaskAttachment } from "../types";
|
|
28
30
|
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
29
31
|
import { taskAttachmentFetchCommand } from "../utils/task-attachment-links";
|
|
32
|
+
import { transcribeAudioNotes } from "../utils/transcription";
|
|
30
33
|
import { getFileInfo, type SlackFile } from "./files";
|
|
31
34
|
import { noteSlackInboundSideEffect } from "./inbound-dispatch";
|
|
32
35
|
// Side-effect import: registers all Slack event templates in the in-memory registry
|
|
@@ -73,6 +76,8 @@ export type InboundSlackFiles = {
|
|
|
73
76
|
files: SlackFile[];
|
|
74
77
|
fetched: FetchedSlackFile[];
|
|
75
78
|
failed: SlackFileFailure[];
|
|
79
|
+
/** Task-text note per audio file id (a transcript), filled when fetched with `transcribe`. */
|
|
80
|
+
notes?: Map<string, string>;
|
|
76
81
|
/** Deletes the downloaded copies; hold the batch with `await using`. */
|
|
77
82
|
[Symbol.asyncDispose](): Promise<void>;
|
|
78
83
|
};
|
|
@@ -96,9 +101,14 @@ export function formatFileSize(bytes: number): string {
|
|
|
96
101
|
* Build a text representation of file attachments for inclusion in messages.
|
|
97
102
|
* Each file is formatted as: [File: filename.ext (mimetype, size) id=FILE_ID],
|
|
98
103
|
* followed by `(not attached: <reason>)` when the file never made it into the
|
|
99
|
-
* task's attachments
|
|
104
|
+
* task's attachments, and by the file's note (a voice-clip transcript) on the
|
|
105
|
+
* next line.
|
|
100
106
|
*/
|
|
101
|
-
export function buildAttachmentText(
|
|
107
|
+
export function buildAttachmentText(
|
|
108
|
+
files: SlackFile[],
|
|
109
|
+
failed: SlackFileFailure[] = [],
|
|
110
|
+
notes: Map<string, string> = new Map(),
|
|
111
|
+
): string {
|
|
102
112
|
const reasons = new Map(failed.map((f) => [f.file.id, f.reason]));
|
|
103
113
|
return files
|
|
104
114
|
.map((f) => {
|
|
@@ -106,7 +116,8 @@ export function buildAttachmentText(files: SlackFile[], failed: SlackFileFailure
|
|
|
106
116
|
const size = typeof f.size === "number" ? formatFileSize(f.size) : "unknown size";
|
|
107
117
|
const line = `[File: ${f.name ?? f.id} (${f.mimetype ?? "unknown type"}, ${size}) id=${f.id}]`;
|
|
108
118
|
const reason = reasons.get(f.id);
|
|
109
|
-
|
|
119
|
+
const note = notes.get(f.id);
|
|
120
|
+
return [reason ? `${line} (not attached: ${reason})` : line, note].filter(Boolean).join("\n");
|
|
110
121
|
})
|
|
111
122
|
.join("\n");
|
|
112
123
|
}
|
|
@@ -121,15 +132,16 @@ export function buildEffectiveText(
|
|
|
121
132
|
text: string | undefined,
|
|
122
133
|
files?: SlackFile[],
|
|
123
134
|
failed?: SlackFileFailure[],
|
|
135
|
+
notes?: Map<string, string>,
|
|
124
136
|
): string {
|
|
125
137
|
const hasText = !!text?.trim();
|
|
126
138
|
const hasFiles = files && files.length > 0;
|
|
127
139
|
|
|
128
140
|
if (hasText && hasFiles) {
|
|
129
|
-
return `${text}\n\n${buildAttachmentText(files, failed)}`;
|
|
141
|
+
return `${text}\n\n${buildAttachmentText(files, failed, notes)}`;
|
|
130
142
|
}
|
|
131
143
|
if (hasFiles) {
|
|
132
|
-
return buildAttachmentText(files, failed);
|
|
144
|
+
return buildAttachmentText(files, failed, notes);
|
|
133
145
|
}
|
|
134
146
|
return text || "";
|
|
135
147
|
}
|
|
@@ -147,11 +159,14 @@ export function bufferedFileFailures(files: SlackFile[] | undefined): SlackFileF
|
|
|
147
159
|
* Download every file on a Slack message with the bot token, each to its own
|
|
148
160
|
* temp file. Never throws: a file that can't be fetched lands in `failed` with
|
|
149
161
|
* a reason the user and the agent can read. Hold the result with `await using`
|
|
150
|
-
* so the temp files go away with it.
|
|
162
|
+
* so the temp files go away with it. With `transcribe`, every downloaded audio
|
|
163
|
+
* file also gets a `notes` entry: its transcript, the failure, or a line saying
|
|
164
|
+
* no provider is configured.
|
|
151
165
|
*/
|
|
152
166
|
export async function fetchSlackFiles(
|
|
153
167
|
client: WebClient,
|
|
154
168
|
files: SlackFile[] | undefined,
|
|
169
|
+
options: { transcribe?: boolean } = {},
|
|
155
170
|
): Promise<InboundSlackFiles> {
|
|
156
171
|
let dir: string | undefined;
|
|
157
172
|
const result: InboundSlackFiles = {
|
|
@@ -182,9 +197,27 @@ export async function fetchSlackFiles(
|
|
|
182
197
|
result.fetched.push({ file, ...outcome });
|
|
183
198
|
}
|
|
184
199
|
}
|
|
200
|
+
if (options.transcribe) {
|
|
201
|
+
result.notes = await transcribeAudioNotes(
|
|
202
|
+
result.fetched
|
|
203
|
+
.filter(({ file }) => isSlackAudio(file))
|
|
204
|
+
.map(({ file, path }) => ({
|
|
205
|
+
key: file.id,
|
|
206
|
+
name: file.name ?? file.id,
|
|
207
|
+
data: Bun.file(path),
|
|
208
|
+
mimeType: file.mimetype,
|
|
209
|
+
})),
|
|
210
|
+
"[Slack]",
|
|
211
|
+
);
|
|
212
|
+
}
|
|
185
213
|
return result;
|
|
186
214
|
}
|
|
187
215
|
|
|
216
|
+
/** An audio file, or a voice clip recorded in Slack (its MIME type can read `video/…`). */
|
|
217
|
+
function isSlackAudio(file: SlackFile): boolean {
|
|
218
|
+
return file.subtype === "slack_audio" || Boolean(file.mimetype?.startsWith("audio/"));
|
|
219
|
+
}
|
|
220
|
+
|
|
188
221
|
/** Does the start of an HTML response read like Slack's sign-in page? */
|
|
189
222
|
function looksLikeSignIn(head: Uint8Array[]): boolean {
|
|
190
223
|
const joined = new Uint8Array(head.reduce((total, part) => total + part.byteLength, 0));
|
|
@@ -177,7 +177,7 @@ async function slackFlush(
|
|
|
177
177
|
const app = getSlackApp();
|
|
178
178
|
const files = items.flatMap((item) => item.files ?? []);
|
|
179
179
|
await using inbound = app
|
|
180
|
-
? await fetchSlackFiles(app.client, files)
|
|
180
|
+
? await fetchSlackFiles(app.client, files, { transcribe: true })
|
|
181
181
|
: {
|
|
182
182
|
files,
|
|
183
183
|
fetched: [],
|
|
@@ -196,6 +196,7 @@ async function slackFlush(
|
|
|
196
196
|
item.text,
|
|
197
197
|
item.files?.map((file) => resolvedFiles.get(file.id) ?? file),
|
|
198
198
|
inbound.failed,
|
|
199
|
+
inbound.notes,
|
|
199
200
|
),
|
|
200
201
|
)
|
|
201
202
|
.join("\n---\n"),
|
package/src/tasks/context-key.ts
CHANGED
|
@@ -21,6 +21,10 @@
|
|
|
21
21
|
* task:trackers:gitlab:{projectId}:{mr|issue}:{iid}
|
|
22
22
|
* task:trackers:azure-devops:{repositoryId}:pr:{pullRequestId}
|
|
23
23
|
* task:trackers:azure-devops:{organization}:workitem:{workItemId}
|
|
24
|
+
* task:trackers:bitbucket:{repoUuid}:pr:{pullRequestId}
|
|
25
|
+
* (repo UUID with Bitbucket's `{}` braces
|
|
26
|
+
* stripped and lowercased, so renames keep
|
|
27
|
+
* task threads)
|
|
24
28
|
* task:trackers:linear:{issueIdentifier} (e.g. DES-42 — case preserved)
|
|
25
29
|
* task:trackers:jira:{issueIdentifier} (e.g. PROJ-123 — case preserved)
|
|
26
30
|
* task:schedule:{scheduleId}
|
|
@@ -50,7 +54,13 @@ export type ContextKeyFamily =
|
|
|
50
54
|
| "schedule"
|
|
51
55
|
| "workflow";
|
|
52
56
|
|
|
53
|
-
export type TrackerProvider =
|
|
57
|
+
export type TrackerProvider =
|
|
58
|
+
| "github"
|
|
59
|
+
| "gitlab"
|
|
60
|
+
| "azure-devops"
|
|
61
|
+
| "bitbucket"
|
|
62
|
+
| "linear"
|
|
63
|
+
| "jira";
|
|
54
64
|
|
|
55
65
|
export type ParsedContextKey =
|
|
56
66
|
| { family: "slack"; parts: { channelId: string; threadTs: string } }
|
|
@@ -76,6 +86,11 @@ export type ParsedContextKey =
|
|
|
76
86
|
subFamily: "azure-devops";
|
|
77
87
|
parts: { organization: string; kind: "workitem"; workItemId: number };
|
|
78
88
|
}
|
|
89
|
+
| {
|
|
90
|
+
family: "trackers";
|
|
91
|
+
subFamily: "bitbucket";
|
|
92
|
+
parts: { repoUuid: string; kind: "pr"; pullRequestId: number };
|
|
93
|
+
}
|
|
79
94
|
| {
|
|
80
95
|
family: "trackers";
|
|
81
96
|
subFamily: "linear";
|
|
@@ -224,6 +239,34 @@ export function azureDevOpsWorkItemContextKey(input: {
|
|
|
224
239
|
return ["task", "trackers", "azure-devops", organization, "workitem", workItemId].join(SEPARATOR);
|
|
225
240
|
}
|
|
226
241
|
|
|
242
|
+
/**
|
|
243
|
+
* Bitbucket Cloud wraps repository UUIDs in braces (`{1a2b...}`). Strip them
|
|
244
|
+
* and lowercase so the key is stable regardless of which payload field the
|
|
245
|
+
* UUID came from.
|
|
246
|
+
*/
|
|
247
|
+
export function bitbucketContextKey({
|
|
248
|
+
repoUuid,
|
|
249
|
+
pullRequestId,
|
|
250
|
+
}: {
|
|
251
|
+
repoUuid: string;
|
|
252
|
+
pullRequestId: number | string;
|
|
253
|
+
}): string {
|
|
254
|
+
const rawUuid = typeof repoUuid === "string" ? repoUuid.trim().replace(/^\{|\}$/g, "") : repoUuid;
|
|
255
|
+
const uuid = assertSafePart(rawUuid, "repoUuid").toLowerCase();
|
|
256
|
+
if (/[{}]/.test(uuid)) {
|
|
257
|
+
throw new Error(
|
|
258
|
+
`context-key: bitbucket "repoUuid" has unbalanced braces (got ${JSON.stringify(repoUuid)})`,
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
const id = assertSafePart(pullRequestId, "pullRequestId");
|
|
262
|
+
if (!/^\d+$/.test(id)) {
|
|
263
|
+
throw new Error(
|
|
264
|
+
`context-key: bitbucket "pullRequestId" must be a positive integer (got ${JSON.stringify(id)})`,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
return ["task", "trackers", "bitbucket", uuid, "pr", id].join(SEPARATOR);
|
|
268
|
+
}
|
|
269
|
+
|
|
227
270
|
export function linearContextKey(input: { issueIdentifier: string }): string {
|
|
228
271
|
const issueIdentifier = assertSafePart(input.issueIdentifier, "issueIdentifier");
|
|
229
272
|
return ["task", "trackers", "linear", issueIdentifier].join(SEPARATOR);
|
|
@@ -401,6 +444,23 @@ export function parseContextKey(key: string): ParsedContextKey {
|
|
|
401
444
|
parts: { repositoryId: parts[3] as string, kind: "pr", pullRequestId },
|
|
402
445
|
};
|
|
403
446
|
}
|
|
447
|
+
if (subFamily === "bitbucket") {
|
|
448
|
+
if (parts.length !== 6 || parts[4] !== "pr" || !parts[3]) {
|
|
449
|
+
throw new Error(`context-key: malformed bitbucket key: ${JSON.stringify(key)}`);
|
|
450
|
+
}
|
|
451
|
+
const idStr = parts[5] as string;
|
|
452
|
+
const pullRequestId = Number.parseInt(idStr, 10);
|
|
453
|
+
if (!/^\d+$/.test(idStr) || !Number.isFinite(pullRequestId)) {
|
|
454
|
+
throw new Error(
|
|
455
|
+
`context-key: malformed bitbucket pullRequestId "${idStr}": ${JSON.stringify(key)}`,
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
return {
|
|
459
|
+
family: "trackers",
|
|
460
|
+
subFamily: "bitbucket",
|
|
461
|
+
parts: { repoUuid: parts[3] as string, kind: "pr", pullRequestId },
|
|
462
|
+
};
|
|
463
|
+
}
|
|
404
464
|
if (subFamily === "linear") {
|
|
405
465
|
if (parts.length !== 4) {
|
|
406
466
|
throw new Error(`context-key: malformed linear key: ${JSON.stringify(key)}`);
|