@desplega.ai/agent-swarm 1.172.0 → 2.0.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 +1 -1
- package/dist/{acp-adapter-fpecx1g1.js → acp-adapter-sj2g0m84.js} +18 -17
- package/dist/{actions-pw87a1a8.js → actions-fegk44gr.js} +65 -35
- package/dist/{amp-adapter-8jem2bc0.js → amp-adapter-ad8ft89x.js} +8 -7
- package/dist/{anthropic-messages-8ee2f22q.js → anthropic-messages-r6czkjv1.js} +7 -7
- package/dist/{app-wmf0m843.js → app-kfvgbvzy.js} +19 -13
- package/dist/{assistant-emp1e108.js → assistant-z7zdy9ja.js} +65 -38
- package/dist/{boot-reembed-1de4f4aj.js → boot-reembed-5c2z0mbe.js} +11 -9
- package/dist/{boot-reembed-4ymn9atn.js → boot-reembed-j8xa181e.js} +11 -9
- package/dist/{boot-scrub-sweep-adhk1s8c.js → boot-scrub-sweep-hnkn3rgt.js} +12 -10
- package/dist/{boot-seeds-8q637aew.js → boot-seeds-kwjq4d2z.js} +15 -13
- package/dist/{claude-adapter-t26cv8a4.js → claude-adapter-9vmvpvgd.js} +20 -19
- package/dist/{claude-managed-adapter-xgxhqrt2.js → claude-managed-adapter-k9qae05j.js} +7 -5
- package/dist/{claude-managed-setup-bnmjarjc.js → claude-managed-setup-q54zx5q2.js} +6 -4
- package/dist/{claude-sdk-session-gsfsh662.js → claude-sdk-session-9jye6cxr.js} +12698 -11995
- package/dist/{cli-k46r9hmk.js → cli-03gedhng.js} +16 -14
- package/dist/{cli-kb4ehzx5.js → cli-1e4b6bzh.js} +7 -7
- package/dist/{cli-d603qf86.js → cli-29jc5q7w.js} +10 -8
- package/dist/cli-2m1ktfzz.js +17795 -0
- package/dist/{cli-8vs9trd4.js → cli-3dj8k4vy.js} +1 -1
- package/dist/{cli-zxm9p3x4.js → cli-3fbmmawm.js} +5 -5
- package/dist/{cli-gmdajfw5.js → cli-4jxm2qtz.js} +4987 -1708
- package/dist/{cli-tadfgd0t.js → cli-5hkm7whv.js} +33 -4
- package/dist/{cli-6jb2rf77.js → cli-5t4f29ez.js} +3 -3
- package/dist/{cli-7a0sde2x.js → cli-6cagzc96.js} +9 -0
- package/dist/cli-6cvfxs8r.js +47 -0
- package/dist/{cli-pj6x9qce.js → cli-6jyj59nb.js} +2 -2
- package/dist/{cli-388egyf6.js → cli-6razj6t4.js} +2 -2
- package/dist/{cli-qsztpfry.js → cli-74ahz61x.js} +2 -2
- package/dist/{cli-4nz8p1p5.js → cli-7x0g4sn1.js} +15474 -16082
- package/dist/{cli-yatxqynm.js → cli-9ha6hs2d.js} +1 -290
- package/dist/{cli-81r1wjyv.js → cli-9nfx44eh.js} +1 -1
- package/dist/cli-9traj3dn.js +638 -0
- package/dist/{cli-smcrrmhy.js → cli-agzebbd5.js} +9 -9
- package/dist/{cli-bf7485pm.js → cli-apx87jne.js} +2 -2
- package/dist/{cli-ztrv9y21.js → cli-bzszjf2p.js} +3689 -1081
- package/dist/{cli-c5km5znr.js → cli-c8r3kzzb.js} +1 -1
- package/dist/{cli-hsem16hb.js → cli-cgtksawx.js} +7 -4
- package/dist/{cli-db3m6zdt.js → cli-d2qr7aj4.js} +1 -1
- package/dist/{cli-1hgat6pa.js → cli-d5f9ed5c.js} +5 -5
- package/dist/cli-e3g5mbkz.js +776 -0
- package/dist/{cli-7jykv7ef.js → cli-e6mpjhed.js} +1 -1
- package/dist/{cli-j6kpj9az.js → cli-ee9gtz70.js} +1553 -68
- package/dist/{cli-e0a9sk00.js → cli-eejsgvm4.js} +4 -4
- package/dist/{cli-tgpfnm6r.js → cli-ef1hg1wv.js} +518 -59
- package/dist/cli-f527ce74.js +781 -0
- package/dist/{cli-ppy4nnnh.js → cli-f7kr7svz.js} +2 -2
- package/dist/cli-hjwmhcg1.js +10622 -0
- package/dist/{cli-2bw3w47j.js → cli-hnhtk6cj.js} +1 -1
- package/dist/{cli-ywm1azg9.js → cli-hnmhtzvh.js} +6 -4
- package/dist/{cli-4fnb6884.js → cli-hvqbtna4.js} +7 -1259
- package/dist/{cli-pstq9888.js → cli-j0pfptdp.js} +220 -103
- package/dist/cli-jta2eyzc.js +619 -0
- package/dist/{cli-esnpehf5.js → cli-k382tshb.js} +12 -13
- package/dist/cli-k3dp5tpf.js +263 -0
- package/dist/{cli-wd0hz6ah.js → cli-kbrt8gqa.js} +9 -91
- package/dist/cli-kceah1eh.js +92 -0
- package/dist/{cli-prrt9fa0.js → cli-ke896fcf.js} +10 -10
- package/dist/{cli-nv4zd0b9.js → cli-kt6zqg0e.js} +6 -6
- package/dist/{cli-g6t9texm.js → cli-m50t4309.js} +5 -18
- package/dist/cli-n2wq4j7m.js +6985 -0
- package/dist/{cli-c2pq2rjb.js → cli-n3hzg89b.js} +46 -15
- package/dist/{cli-tmhsxhb8.js → cli-n5vwg842.js} +23 -6
- package/dist/{cli-5rv1j9ga.js → cli-ne236afy.js} +53 -7
- package/dist/cli-p0sg24zp.js +16 -0
- package/dist/{cli-d161g69q.js → cli-p1x85d2t.js} +50 -2
- package/dist/{cli-7pz3b7x5.js → cli-przpmdgr.js} +25 -0
- package/dist/{cli-g4f5pj26.js → cli-q0ke1yww.js} +111 -25
- package/dist/{cli-e3pmyt39.js → cli-q50942rs.js} +1 -1
- package/dist/{cli-r44t9wqc.js → cli-r106725k.js} +2 -2
- package/dist/{cli-gj81e47t.js → cli-r2pnvj8g.js} +2 -2
- package/dist/{cli-r8smj8b8.js → cli-r62a35ea.js} +19 -3
- package/dist/{cli-cd0hv9nh.js → cli-ra0xzggj.js} +2 -2
- package/dist/cli-rhs9pz33.js +294 -0
- package/dist/{cli-nyhb1r6x.js → cli-s2sp957d.js} +52 -29
- package/dist/{cli-1x7da0de.js → cli-s4setq91.js} +8 -8
- package/dist/{cli-xczpdp9e.js → cli-se8w6wny.js} +6 -6
- package/dist/cli-skxvvpap.js +10140 -0
- package/dist/{cli-014vns2y.js → cli-stfs0k5s.js} +208 -525
- package/dist/{cli-0p3enk35.js → cli-tqaazemc.js} +9 -1
- package/dist/{cli-cf69s3ew.js → cli-tr4sxp68.js} +3 -3
- package/dist/{cli-8hzfb6xz.js → cli-tsde1hdr.js} +15 -15
- package/dist/{cli-cceskcvt.js → cli-txqb9s38.js} +8 -8
- package/dist/{cli-aspcaef3.js → cli-v1yb0rqb.js} +6 -6
- package/dist/{cli-j1pmc07z.js → cli-vatyhshy.js} +268 -40
- package/dist/{cli-04emfe2q.js → cli-vdach5a9.js} +5 -5
- package/dist/{cli-7avy4v8w.js → cli-w1q7h2r2.js} +5 -3
- package/dist/{cli-rcp5aqdw.js → cli-w7m81tb7.js} +1 -1
- package/dist/{cli-hwwvy36w.js → cli-wayw93nt.js} +3 -3
- package/dist/cli-wmgrkscj.js +23254 -0
- package/dist/{cli-gp1rtyvk.js → cli-z5jjpaxw.js} +29 -6
- package/dist/{cli-451ks6aw.js → cli-zkpks3r3.js} +1 -1
- package/dist/cli.js +32 -48
- package/dist/{codex-adapter-m4tgdysy.js → codex-adapter-y81sssk0.js} +16 -15
- package/dist/{codex-login-841kv5fk.js → codex-login-f2rcfpp9.js} +3 -2
- package/dist/{codex-session-runner-ymwzm6ea.js → codex-session-runner-8vx4j6a9.js} +16 -15
- package/dist/{commands-n0hshcvs.js → commands-kcpvpcrr.js} +21 -10
- package/dist/{controller-hq5ggpcp.js → controller-ywrmt6yd.js} +5 -4
- package/dist/{controllers-connect-y07cqcvy.js → controllers-connect-kwpvj3rf.js} +45 -38
- package/dist/{cursor-adapter-q8pgv5qa.js → cursor-adapter-cehv1qns.js} +7 -6
- package/dist/{db-hkqvrdcb.js → db-k8ankj20.js} +13 -11
- package/dist/{db-4w7tmt1g.js → db-ww5d7jq3.js} +29 -9
- package/dist/{devin-adapter-xgtj680j.js → devin-adapter-x1022qb1.js} +3 -2
- package/dist/{dispatcher-5sd69y14.js → dispatcher-e8gq5m68.js} +41 -34
- package/dist/{docker-f503k9p0.js → docker-ff6y1190.js} +3 -2
- package/dist/{dsh-adapter-2h8m4cab.js → dsh-adapter-3epjm3af.js} +7 -6
- package/dist/{e2b-66xra4az.js → e2b-8ygwhg0f.js} +4 -3
- package/dist/{e2b-xwhyj8cw.js → e2b-swhnpw3n.js} +4 -3
- package/dist/{event-bus-vjtv82gm.js → event-bus-6axnngdj.js} +3 -2
- package/dist/external-channel-h3359cwn.js +52 -0
- package/dist/{google-generative-ai-7pavrbv7.js → google-generative-ai-g8db7x0f.js} +5 -3
- package/dist/{google-vertex-3f1jk41f.js → google-vertex-r2692q0r.js} +5 -3
- package/dist/{grok-adapter-8m9yqzbq.js → grok-adapter-0d4x7gkx.js} +19 -18
- package/dist/{handlers-s20xr10r.js → handlers-an1gzv21.js} +44 -37
- package/dist/{heartbeat-3k31q4g2.js → heartbeat-6dakeb61.js} +46 -39
- package/dist/{hook-gdn6a2fn.js → hook-6v6cv6d5.js} +12 -24
- package/dist/{hook-ygwerrdh.js → hook-sa5xye4y.js} +13 -12
- package/dist/{http-jdteeg81.js → http-r7f3eky0.js} +1598 -279
- package/dist/index-52j1aja4.js +3465 -0
- package/dist/{index-e0snym4t.js → index-7ep4gzdk.js} +41 -34
- package/dist/index-9gw58n4k.js +193 -0
- package/dist/index-9j87mcm8.js +56305 -0
- package/dist/index-bsmcresk.js +79424 -0
- package/dist/index-mh85qhec.js +475 -0
- package/dist/{index-vm3bm9t7.js → index-rmpw9a8q.js} +41 -34
- package/dist/{index-6ksewd5y.js → index-s2wxgzr0.js} +1015 -732
- package/dist/{index-tsszt7g9.js → index-v1v6n7hg.js} +46 -39
- package/dist/index-we27kd31.js +12810 -0
- package/dist/{index-ffqzqgwb.js → index-zk96aq1k.js} +11 -9
- package/dist/{keepalive-jekc896t.js → keepalive-be9vwy5h.js} +13 -11
- package/dist/{lead-4k9s9szx.js → lead-0861cwrf.js} +64 -57
- package/dist/{maintenance-shazch4n.js → maintenance-mzzgc8bn.js} +11 -9
- package/dist/node-c9we95xf.js +1 -1
- package/dist/{node-fkajaq96.js → node-mn1zs7an.js} +2 -1
- package/dist/{oauth-refresh-sweep-5gb36kn8.js → oauth-refresh-sweep-cpdnfwdj.js} +12 -10
- package/dist/{onboard-ydrbnvww.js → onboard-fh8qs4r3.js} +11 -3
- package/dist/{opencode-adapter-5zh0he20.js → opencode-adapter-nm42wwj8.js} +19 -14
- package/dist/{otel-impl-6pf5cv86.js → otel-impl-x6c1c9g4.js} +248 -869
- package/dist/{pi-mono-adapter-ef9wwx9m.js → pi-mono-adapter-9n2ya240.js} +245 -107
- package/dist/{pricing-refresh-s52nsa5q.js → pricing-refresh-qb0rv0ap.js} +13 -11
- package/dist/{rbac-roles-aq3msh4e.js → rbac-roles-1gzdwbh1.js} +12 -10
- package/dist/{rbac-roles-q5gb5e8v.js → rbac-roles-3mraey52.js} +13 -11
- package/dist/{render-v2-pd1rg54q.js → render-v2-agpa45zz.js} +19 -13
- package/dist/{resume-gw4a2c6v.js → resume-x0ydy9s9.js} +27 -21
- package/dist/{runtime-qfry88v2.js → runtime-sa2vr16g.js} +2 -2
- package/dist/{setup-jda1ksdh.js → setup-as9ga0d6.js} +2 -2
- package/dist/{virtual-modules-b3ga2m65.js → virtual-modules-9gkxqrap.js} +7 -6
- package/dist/{work-objects-tb7ycye8.js → work-objects-y5g1y7ev.js} +49 -37
- package/dist/{worker-njjccz5d.js → worker-860c3e38.js} +64 -57
- package/dist/{x-qfmygqqv.js → x-m477fr0h.js} +3 -2
- package/openapi.json +951 -356
- package/package.json +12 -13
- package/src/apps/catalog.generated.json +1309 -53
- package/src/apps/page-validator.ts +93 -31
- package/src/azure-devops/api.ts +94 -18
- package/src/azure-devops/auth.ts +32 -0
- package/src/azure-devops/boards-sync.ts +708 -0
- package/src/azure-devops/handlers.ts +52 -8
- package/src/azure-devops/html.ts +26 -0
- package/src/azure-devops/index.ts +15 -0
- package/src/azure-devops/templates.ts +86 -0
- package/src/azure-devops/types.ts +65 -1
- package/src/be/config-refs.ts +60 -0
- package/src/be/db/page-versions.ts +15 -0
- package/src/be/db/runtime.ts +35 -8
- package/src/be/db/skill-name-resolution.ts +36 -0
- package/src/be/db/tasks/read.ts +230 -0
- package/src/be/db/tasks/write.ts +7 -0
- package/src/be/db.ts +74 -13
- package/src/be/migrations/213_script_connections_sql.sql +79 -0
- package/src/be/migrations/215_swarm_config_export_to_env.sql +34 -0
- package/src/be/migrations/216_task_resolved_effort.sql +5 -0
- package/src/be/migrations/217_chat_sdk_state.sql +104 -0
- package/src/be/model-catalog-store.ts +5 -1
- package/src/be/model-tier-resolution.ts +12 -1
- package/src/be/model-validation.ts +5 -2
- package/src/be/models-catalog.ts +26 -1
- package/src/be/modelsdev-cache.json +198 -63
- package/src/be/modelsdev-cache.ts +9 -1
- package/src/be/script-connections.ts +47 -8
- package/src/be/script-credential-broker.ts +39 -2
- package/src/be/scripts/run-saved.ts +1 -0
- package/src/be/scripts/typecheck.ts +10 -8
- package/src/be/seed/runner.ts +13 -1
- package/src/be/seed/state-db.ts +5 -0
- package/src/be/seed/types.ts +6 -0
- package/src/be/seed-pricing.ts +19 -2
- package/src/be/seed-scripts/catalog/ops-catalog-audit.inline.ts +1 -1
- package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +1 -1
- package/src/be/seed-skills/bundled-files.generated.json +10 -18
- package/src/be/seed-skills/index.ts +107 -3
- package/src/be/sql-connections.ts +118 -0
- package/src/be/swarm-config-guard.ts +53 -0
- package/src/cli.tsx +0 -16
- package/src/commands/onboard/non-interactive.ts +4 -2
- package/src/commands/profile-sync.ts +5 -3
- package/src/commands/provider-credentials.ts +52 -6
- package/src/commands/runner.ts +196 -56
- package/src/e2b/env.ts +5 -0
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +45 -0
- package/src/extensions/dispatcher.ts +7 -0
- package/src/extensions/post-bridge.ts +40 -0
- package/src/github/handlers.ts +11 -4
- package/src/heartbeat/templates.ts +8 -5
- package/src/hooks/hook.ts +0 -26
- package/src/http/all-routes.ts +0 -1
- package/src/http/config.ts +36 -17
- package/src/http/core.ts +4 -0
- package/src/http/index.ts +14 -2
- package/src/http/integrations.ts +88 -1
- package/src/http/mcp-oauth.ts +6 -13
- package/src/http/mcp.ts +15 -5
- package/src/http/page-proxy.ts +2 -4
- package/src/http/pages-public.ts +6 -5
- package/src/http/pages.ts +5 -14
- package/src/http/poll.ts +28 -68
- package/src/http/requested-by.ts +54 -0
- package/src/http/route-def.ts +7 -6
- package/src/http/script-connection-proxy.ts +51 -4
- package/src/http/script-connections.ts +5 -2
- package/src/http/scripts.ts +1 -0
- package/src/http/session-cost-recompute.ts +12 -2
- package/src/http/session-data.ts +4 -0
- package/src/http/stats.ts +1 -30
- package/src/http/tasks.ts +61 -5
- package/src/http/utils.ts +14 -0
- package/src/http/webhooks.ts +183 -2
- package/src/http/x.ts +1 -0
- package/src/integrations/chat-sdk/adapters.ts +129 -0
- package/src/integrations/chat-sdk/attachments.ts +232 -0
- package/src/integrations/chat-sdk/background.ts +36 -0
- package/src/integrations/chat-sdk/commands.ts +194 -0
- package/src/integrations/chat-sdk/config.ts +198 -0
- package/src/integrations/chat-sdk/discord-api.ts +63 -0
- package/src/integrations/chat-sdk/discord-gateway.ts +185 -0
- package/src/integrations/chat-sdk/handlers.ts +28 -0
- package/src/integrations/chat-sdk/inbound.ts +426 -0
- package/src/integrations/chat-sdk/limits.ts +48 -0
- package/src/integrations/chat-sdk/logger.ts +81 -0
- package/src/integrations/chat-sdk/outbound.ts +123 -0
- package/src/integrations/chat-sdk/relay.ts +172 -0
- package/src/integrations/chat-sdk/router.ts +169 -0
- package/src/integrations/chat-sdk/runtime.ts +351 -0
- package/src/integrations/chat-sdk/schema.ts +245 -0
- package/src/integrations/chat-sdk/state.ts +243 -0
- package/src/integrations/chat-sdk/status.ts +244 -0
- package/src/integrations/chat-sdk/steering.ts +58 -0
- package/src/integrations/chat-sdk/teams-origin.ts +77 -0
- package/src/integrations/chat-sdk/telegram-api.ts +56 -0
- package/src/integrations/chat-sdk/templates.ts +16 -0
- package/src/integrations/chat-sdk/test-hooks.ts +37 -0
- package/src/integrations/chat-sdk/thread-buffer.ts +100 -0
- package/src/integrations/chat-sdk/typing.ts +65 -0
- package/src/jira/adf.ts +3 -1
- package/src/kv-reserved-namespaces.ts +2 -0
- package/src/oauth/mcp-wrapper.ts +10 -6
- package/src/otel-impl.ts +2 -1
- package/src/pages/version.ts +4 -5
- package/src/prompts/base-prompt.ts +111 -45
- package/src/providers/acp-target-catalog.ts +6 -0
- package/src/providers/acp-targets.ts +56 -3
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/claude-adapter.ts +27 -2
- package/src/providers/claude-flag-settings.ts +133 -0
- package/src/providers/claude-managed-adapter.ts +1 -1
- package/src/providers/claude-sdk-session.ts +7 -0
- package/src/providers/claude-session-events.ts +6 -1
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/providers/opencode-adapter.ts +268 -37
- package/src/providers/opencode-billing.ts +86 -0
- package/src/providers/opencode-swarm-plugin.ts +51 -0
- package/src/providers/pi-mono-adapter.ts +398 -153
- package/src/providers/pricing-sources.md +37 -3
- package/src/providers/reasoning-effort.ts +27 -0
- package/src/providers/types.ts +11 -2
- package/src/rbac/legacy-policy.ts +8 -2
- package/src/rbac/permissions.ts +21 -0
- package/src/scheduler/scheduler.ts +4 -2
- package/src/scripts-runtime/api-client.ts +15 -1
- package/src/scripts-runtime/api-types.ts +9 -1
- package/src/scripts-runtime/ctx.ts +1 -1
- package/src/scripts-runtime/loader.ts +19 -7
- package/src/scripts-runtime/mcp-client.ts +30 -19
- package/src/scripts-runtime/sdk-allowlist.ts +8 -6
- package/src/scripts-runtime/swarm-sdk.ts +0 -11
- package/src/scripts-runtime/types/stdlib.d.ts +27 -15
- package/src/scripts-runtime/types/swarm-extension.d.ts +63 -1
- package/src/scripts-runtime/types/swarm-sdk.d.ts +27 -15
- package/src/server.ts +52 -18
- package/src/slack/actions.ts +29 -1
- package/src/slack/app.ts +14 -2
- package/src/slack/assistant.ts +36 -2
- package/src/slack/channel-join.ts +3 -16
- package/src/slack/commands.ts +9 -1
- package/src/slack/event-dedup.ts +1 -1
- package/src/slack/external-channel-egress.ts +113 -0
- package/src/slack/external-channel.ts +309 -0
- package/src/slack/handlers.ts +61 -0
- package/src/slack/render-v2.ts +40 -47
- package/src/slack/work-objects.ts +5 -0
- package/src/tasks/additive-buffer.ts +4 -2
- package/src/tasks/context-key.ts +94 -2
- package/src/tasks/model-pin.ts +67 -0
- package/src/tasks/worker-follow-up.ts +4 -31
- package/src/telemetry.ts +2 -1
- package/src/tests/acp-adapter.test.ts +42 -0
- package/src/tests/api-drain-worker-e2e.test.ts +1 -6
- package/src/tests/apps-catalog-feedback.test.ts +310 -0
- package/src/tests/apps-catalog-inputs.test.ts +70 -0
- package/src/tests/apps-catalog-table.test.ts +113 -0
- package/src/tests/azure-devops-boards-sync.test.ts +584 -0
- package/src/tests/azure-devops-html.test.ts +13 -0
- package/src/tests/azure-devops-webhook-malformed.test.ts +4 -0
- package/src/tests/base-prompt.test.ts +58 -54
- package/src/tests/chat-sdk-commands.test.ts +191 -0
- package/src/tests/chat-sdk-config.test.ts +334 -0
- package/src/tests/chat-sdk-discord-api.test.ts +68 -0
- package/src/tests/chat-sdk-discord-gateway.test.ts +252 -0
- package/src/tests/chat-sdk-discord-webhook.test.ts +126 -0
- package/src/tests/chat-sdk-fields-sync.test.ts +50 -0
- package/src/tests/chat-sdk-harness.ts +146 -0
- package/src/tests/chat-sdk-inbound.test.ts +363 -0
- package/src/tests/chat-sdk-relay.test.ts +470 -0
- package/src/tests/chat-sdk-reload.test.ts +128 -0
- package/src/tests/chat-sdk-shutdown.test.ts +220 -0
- package/src/tests/chat-sdk-slack.test.ts +268 -0
- package/src/tests/chat-sdk-state.test.ts +163 -0
- package/src/tests/chat-sdk-status-route.test.ts +382 -0
- package/src/tests/chat-sdk-teams.test.ts +277 -0
- package/src/tests/chat-sdk-tools.test.ts +520 -0
- package/src/tests/chat-sdk-typing.test.ts +120 -0
- package/src/tests/chat-sdk-webhook-telegram.test.ts +200 -0
- package/src/tests/claude-adapter-binary.test.ts +144 -0
- package/src/tests/claude-flag-settings.test.ts +114 -0
- package/src/tests/claude-managed-adapter.test.ts +1 -1
- package/src/tests/claude-sdk-lifecycle.test.ts +148 -1
- package/src/tests/claude-sdk-process.fixture.ts +7 -0
- package/src/tests/claude-thinking-tokens.test.ts +57 -0
- package/src/tests/claude-worker-parity.test.ts +2 -7
- package/src/tests/config-env-export-chat-sdk.test.ts +11 -0
- package/src/tests/config-refs.test.ts +66 -0
- package/src/tests/context-key.test.ts +51 -0
- package/src/tests/cors-allowlist.test.ts +25 -0
- package/src/tests/create-page-tool.test.ts +18 -0
- package/src/tests/credential-check.test.ts +139 -0
- package/src/tests/credentials.test.ts +21 -0
- package/src/tests/db-runtime.test.ts +61 -0
- package/src/tests/db-task-read-contract.test.ts +20 -0
- package/src/tests/db-task-write-contract.test.ts +10 -0
- package/src/tests/deferred-task-wake.test.ts +52 -4
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/dsh-adapter.test.ts +1 -1
- package/src/tests/enabled-capabilities.test.ts +20 -0
- package/src/tests/entrypoint-api-readiness.test.ts +14 -8
- package/src/tests/entrypoint-config-env-export.test.ts +5 -2
- package/src/tests/entrypoint-export-to-env.test.ts +78 -0
- package/src/tests/explicit-model-validation.test.ts +105 -1
- package/src/tests/extensions-example-route-chat-channel.test.ts +154 -0
- package/src/tests/extensions-post-bridge.test.ts +51 -0
- package/src/tests/fetch-resolved-env.test.ts +128 -0
- package/src/tests/fixtures/azure-boards/workitem.commented.json +89 -0
- package/src/tests/fixtures/azure-boards/workitem.created.json +81 -0
- package/src/tests/fixtures/azure-boards/workitem.deleted.json +70 -0
- package/src/tests/fixtures/azure-boards/workitem.restored.json +82 -0
- package/src/tests/fixtures/azure-boards/workitem.updated.json +130 -0
- package/src/tests/fixtures/catalog-feedback-definition.json.txt +860 -0
- package/src/tests/fixtures/catalog-inputs-definition.json.txt +910 -0
- package/src/tests/fixtures/catalog-table-definition.json.txt +468 -0
- package/src/tests/fixtures/chat-sdk/telegram-private-text.json +10 -0
- package/src/tests/fixtures/extensions/route-chat-channel-to-agent/hooks.ts +27 -0
- package/src/tests/fixtures/extensions/route-chat-channel-to-agent/manifest.json +9 -0
- package/src/tests/fixtures/openai-compatible-registry-extension.child.ts +146 -0
- package/src/tests/fixtures/runner-identity-refresh.ts +3 -0
- package/src/tests/fixtures/runner-legacy-resume-prompt.ts +122 -0
- package/src/tests/github-handlers.test.ts +35 -0
- package/src/tests/harness-provider-resolution.test.ts +12 -0
- package/src/tests/heartbeat-checklist.test.ts +3 -2
- package/src/tests/hook-runtime-close.test.ts +0 -2
- package/src/tests/http-api-integration.test.ts +0 -25
- package/src/tests/migration-046-budgets.test.ts +17 -16
- package/src/tests/migration-217-chat-sdk-state.test.ts +113 -0
- package/src/tests/model-tier-resolution.test.ts +93 -0
- package/src/tests/models-catalog.test.ts +42 -1
- package/src/tests/node-env-runtime.test.ts +98 -0
- package/src/tests/openai-compatible-providers.test.ts +134 -0
- package/src/tests/openai-compatible-registry-extension.test.ts +42 -0
- package/src/tests/opencode-adapter.test.ts +348 -3
- package/src/tests/opencode-billing.test.ts +28 -0
- package/src/tests/opencode-helmcode-sdk.test.ts +201 -0
- package/src/tests/opencode-shell-env.test.ts +145 -0
- package/src/tests/openrouter-base-url.test.ts +101 -151
- package/src/tests/page-proxy.test.ts +1 -1
- package/src/tests/pages-browser-sdk.test.ts +76 -0
- package/src/tests/pages-feedback.test.ts +1 -1
- package/src/tests/paused-tasks-requested-by.test.ts +102 -0
- package/src/tests/pi-custom-provider-fake-server.test.ts +442 -0
- package/src/tests/pi-mono-adapter.test.ts +385 -5
- package/src/tests/pi-shell-env.test.ts +170 -0
- package/src/tests/prompt-template-remaining.test.ts +35 -0
- package/src/tests/rbac-engine.test.ts +11 -3
- package/src/tests/runner-exit-after-tasks.test.ts +0 -4
- package/src/tests/runner-identity-refresh.test.ts +3 -0
- package/src/tests/runner-legacy-resume-prompt.test.ts +64 -0
- package/src/tests/script-connections-sql.test.ts +675 -0
- package/src/tests/script-connections.test.ts +7 -1
- package/src/tests/scripts-http.test.ts +201 -1
- package/src/tests/scripts-only-gating.test.ts +2 -3
- package/src/tests/scripts-runtime.test.ts +58 -40
- package/src/tests/secret-scrubber.test.ts +86 -0
- package/src/tests/seed-skills-bundled-files.test.ts +45 -53
- package/src/tests/seed-skills-retire.test.ts +182 -0
- package/src/tests/send-task-requester-gate.test.ts +58 -0
- package/src/tests/session-cost-recompute-unit.test.ts +94 -0
- package/src/tests/skill-get-name-resolution.test.ts +137 -0
- package/src/tests/slack-channel-join.test.ts +4 -1
- package/src/tests/slack-external-channel-config.test.ts +65 -0
- package/src/tests/slack-external-channel-egress.test.ts +237 -0
- package/src/tests/slack-external-channel-guard.test.ts +280 -0
- package/src/tests/slack-external-channel-tools.test.ts +173 -0
- package/src/tests/slack-external-channel.test.ts +370 -0
- package/src/tests/slack-render-v2.test.ts +86 -10
- package/src/tests/sonnet-5-5-registry.test.ts +9 -1
- package/src/tests/spec-base-url.test.ts +16 -1
- package/src/tests/swarm-config-export-to-env.test.ts +267 -0
- package/src/tests/swarm-config-reserved-keys.test.ts +16 -0
- package/src/tests/swarm-diff.test.ts +1 -1
- package/src/tests/task-model-effort-rerun.test.ts +164 -0
- package/src/tests/task-session-prompts.test.ts +132 -0
- package/src/tests/task-tool-manifest.test.ts +14 -0
- package/src/tests/task-tools-ctx.test.ts +2 -0
- package/src/tests/tool-annotations.test.ts +6 -6
- package/src/tests/ui-logs-parser.test.ts +43 -0
- package/src/tests/used-by-route.test.ts +6 -0
- package/src/tests/vendored-openapi.test.ts +16 -1
- package/src/tests/vercel-ai-gateway.test.ts +149 -0
- package/src/tests/workflow-swarm-script.test.ts +38 -0
- package/src/tests/workflow-wait-filter.test.ts +5 -3
- package/src/tools/chat-access.ts +41 -0
- package/src/tools/chat-open-dm.ts +59 -0
- package/src/tools/chat-post.ts +74 -0
- package/src/tools/chat-read-thread.ts +100 -0
- package/src/tools/chat-reply.ts +75 -0
- package/src/tools/create-page.ts +3 -13
- package/src/tools/defer-task.ts +17 -0
- package/src/tools/model-catalog/index.ts +2 -1
- package/src/tools/register-chat-channel.ts +139 -0
- package/src/tools/send-task.ts +24 -0
- package/src/tools/skills/skill-get.ts +17 -9
- package/src/tools/slack-post.ts +6 -0
- package/src/tools/slack-read.ts +8 -0
- package/src/tools/slack-reply.ts +6 -0
- package/src/tools/slack-start-thread.ts +6 -0
- package/src/tools/slack-update.ts +6 -0
- package/src/tools/slack-upload-file.ts +6 -0
- package/src/tools/swarm-config/get-config.ts +2 -1
- package/src/tools/swarm-config/list-config.ts +2 -1
- package/src/tools/swarm-config/set-config.ts +16 -3
- package/src/tools/tool-config.ts +8 -6
- package/src/types.ts +12 -2
- package/src/utils/config-env-export.ts +74 -0
- package/src/utils/credentials.test.ts +92 -0
- package/src/utils/credentials.ts +52 -4
- package/src/utils/harness-provider.ts +15 -6
- package/src/utils/node-env.ts +24 -0
- package/src/utils/runtime-model-catalog.ts +11 -1
- package/src/utils/secret-scrubber.ts +71 -2
- package/src/utils/task-tool-manifest.ts +19 -2
- package/src/workflows/executors/swarm-script.ts +1 -0
- package/templates/skills/agent-browser/SKILL.md +1 -1
- package/templates/skills/agent-browser/content.md +1 -1
- package/templates/skills/apps/SKILL.md +749 -0
- package/templates/skills/apps/content.md +60 -22
- package/templates/skills/chat-sdk/config.json +14 -0
- package/templates/skills/chat-sdk/content.md +84 -0
- package/templates/skills/composio-gmail/content.md +7 -0
- package/templates/skills/composio-google-docs/content.md +1 -1
- package/templates/skills/email-domain-health/config.json +14 -0
- package/templates/skills/email-domain-health/content.md +131 -0
- package/templates/skills/kv-storage/content.md +2 -2
- package/templates/skills/pages/content.md +8 -10
- package/templates/skills/qa-loom/config.json +14 -0
- package/templates/skills/qa-loom/content.md +187 -0
- package/templates/skills/qa-loom/files/build-loom.mjs +221 -0
- package/templates/skills/qa-loom/files/run-qa.sh +57 -0
- package/templates/skills/slack-interaction/content.md +4 -0
- package/templates/skills/swarm-extensions/content.md +2 -0
- package/templates/skills/swarm-scripts/SKILL.md +4 -2
- package/templates/skills/swarm-scripts/config.json +1 -1
- package/templates/skills/swarm-scripts/content.md +4 -2
- package/templates/skills/workflow-iterate/content.md +38 -0
- package/dist/artifact-eczsknc3.js +0 -3843
- package/dist/cli-9xsmdpdt.js +0 -56377
- package/dist/cli-bd957dgb.js +0 -153
- package/dist/cli-q51yprfq.js +0 -1104
- package/dist/cli-t1ze64h3.js +0 -811
- package/dist/index-9ch1n761.js +0 -1102
- package/src/artifact-sdk/index.ts +0 -2
- package/src/artifact-sdk/localtunnel.d.ts +0 -20
- package/src/artifact-sdk/port.ts +0 -12
- package/src/artifact-sdk/server.ts +0 -183
- package/src/artifact-sdk/tunnel.ts +0 -19
- package/src/commands/artifact.ts +0 -249
- package/src/http/ecosystem.ts +0 -77
- package/src/tests/artifact-commands.test.ts +0 -92
- package/src/tests/artifact-sdk.test.ts +0 -793
- package/src/tests/register-service-security.test.ts +0 -194
- package/src/tools/list-services.ts +0 -107
- package/src/tools/register-service.ts +0 -177
- package/src/tools/unregister-service.ts +0 -73
- package/src/tools/update-service-status.ts +0 -96
- package/templates/skills/artifacts/config.json +0 -14
- package/templates/skills/artifacts/content.md +0 -344
- package/templates/skills/artifacts/files/examples/approval-flow.ts +0 -34
- package/templates/skills/artifacts/files/examples/hono-dashboard.ts +0 -31
- package/templates/skills/artifacts/files/examples/multi-artifact.ts +0 -20
- package/templates/skills/artifacts/files/examples/static-report.sh +0 -17
- package/dist/{azure-openai-responses-ap1gwxgr.js → azure-openai-responses-ygfreq4t.js} +5 -5
- package/dist/{cli-5rqrj7q0.js → cli-pw7ebjdt.js} +0 -0
- package/dist/{openai-codex-responses-33qhkkdy.js → openai-codex-responses-3bk0grnr.js} +4 -4
- package/dist/{openai-completions-s88xm3ns.js → openai-completions-5d4jpakc.js} +9 -9
- package/dist/{openai-responses-7xd2f2e2.js → openai-responses-krb51f59.js} +5 -5
- package/dist/{pi-messages-xszk613n.js → pi-messages-x9gzynm1.js} +3 -3
- /package/src/{artifact-sdk → pages}/browser-sdk.ts +0 -0
- /package/src/{artifact-sdk → pages}/feedback-overlay.ts +0 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import type { WebClient } from "@slack/web-api";
|
|
2
|
+
import { claimKv, deleteKv, sweepExpiredKv, upsertKv } from "../be/db";
|
|
3
|
+
import { isEnvFlagEnabled } from "../utils/env-flag";
|
|
4
|
+
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
5
|
+
import { slackCode, slackMissingScopeMessage } from "./channel-join";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* External (Slack Connect) channel detection and enforcement.
|
|
9
|
+
*
|
|
10
|
+
* A channel is external when `conversations.info` returns `is_ext_shared` or
|
|
11
|
+
* `is_pending_ext_shared`, or when the event envelope carries
|
|
12
|
+
* `is_ext_shared_channel: true`. `is_shared` / `is_org_shared` alone are
|
|
13
|
+
* internal (Enterprise Grid org-shared channels).
|
|
14
|
+
*
|
|
15
|
+
* `SLACK_BLOCK_EXTERNAL_CHANNELS` is read at call time so a dashboard change
|
|
16
|
+
* applies without a restart.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export type ChannelExternality = "external" | "internal" | "unknown";
|
|
20
|
+
export type ExternalChannelTrigger = "joined" | "shared" | "boot-scan";
|
|
21
|
+
/** Why a channel is blocked: Slack says it is external, or Slack could not confirm it is not. */
|
|
22
|
+
export type ChannelBlockReason = "external" | "unverified";
|
|
23
|
+
|
|
24
|
+
const EXTERNALITY_TTL_MS = 10 * 60 * 1000;
|
|
25
|
+
const EXTERNALITY_CACHE_MAX = 500;
|
|
26
|
+
const ALERT_DEDUPE_MS = 60 * 60 * 1000;
|
|
27
|
+
/** A reservation whose post never settles (crash mid-post) frees the key after this. */
|
|
28
|
+
const ALERT_SENDING_TTL_MS = 2 * 60 * 1000;
|
|
29
|
+
/** Durable alert dedupe in kv_entries, so a restart does not repeat alerts. */
|
|
30
|
+
export const EXTERNAL_ALERT_KV_NAMESPACE = "slack-external-alert";
|
|
31
|
+
|
|
32
|
+
const externalityCache = new Map<string, { value: "external" | "internal"; expiresAt: number }>();
|
|
33
|
+
// Bumped on every invalidation or positive external evidence. A lookup that
|
|
34
|
+
// started under an older generation must not overwrite the newer state.
|
|
35
|
+
const channelGeneration = new Map<string, number>();
|
|
36
|
+
let generationCounter = 0;
|
|
37
|
+
|
|
38
|
+
export const EXTERNAL_CHANNEL_BLOCKED_REPLY =
|
|
39
|
+
"This channel is shared with another organization. The swarm does not work here.";
|
|
40
|
+
|
|
41
|
+
export function externalChannelToolError(
|
|
42
|
+
channelId: string,
|
|
43
|
+
reason: ChannelBlockReason = "external",
|
|
44
|
+
): string {
|
|
45
|
+
if (reason === "unverified") {
|
|
46
|
+
return `Could not verify that channel ${channelId} is internal to this organization. SLACK_BLOCK_EXTERNAL_CHANNELS blocks swarm messages there until Slack confirms it. Try again later.`;
|
|
47
|
+
}
|
|
48
|
+
return `Channel ${channelId} is shared with another organization. SLACK_BLOCK_EXTERNAL_CHANNELS blocks swarm messages there.`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function isBlockExternalEnabled(): boolean {
|
|
52
|
+
return isEnvFlagEnabled("SLACK_BLOCK_EXTERNAL_CHANNELS", false);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Reset in-memory caches between tests. Durable alert state lives in kv_entries. */
|
|
56
|
+
export function resetExternalChannelStateForTesting(): void {
|
|
57
|
+
externalityCache.clear();
|
|
58
|
+
channelGeneration.clear();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Insert into an insertion-ordered Map, evicting the oldest entry at the cap. */
|
|
62
|
+
function setBounded<V>(map: Map<string, V>, key: string, value: V): void {
|
|
63
|
+
map.delete(key);
|
|
64
|
+
if (map.size >= EXTERNALITY_CACHE_MAX) {
|
|
65
|
+
const oldest = map.keys().next().value;
|
|
66
|
+
if (oldest !== undefined) map.delete(oldest);
|
|
67
|
+
}
|
|
68
|
+
map.set(key, value);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function bumpGeneration(channelId: string): void {
|
|
72
|
+
generationCounter += 1;
|
|
73
|
+
setBounded(channelGeneration, channelId, generationCounter);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function invalidateChannelExternality(channelId: string): void {
|
|
77
|
+
externalityCache.delete(channelId);
|
|
78
|
+
bumpGeneration(channelId);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Records positive evidence that a channel is external (for example an event
|
|
83
|
+
* envelope with `is_ext_shared_channel: true`), so later outbound checks block
|
|
84
|
+
* without waiting for the cached internal value to expire.
|
|
85
|
+
*/
|
|
86
|
+
export function recordExternalChannel(channelId: string): void {
|
|
87
|
+
bumpGeneration(channelId);
|
|
88
|
+
setBounded(externalityCache, channelId, {
|
|
89
|
+
value: "external",
|
|
90
|
+
expiresAt: Date.now() + EXTERNALITY_TTL_MS,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Looks up whether a channel is external. Known results are cached for 10
|
|
96
|
+
* minutes; an API error returns "unknown" and is not cached. A result whose
|
|
97
|
+
* channel was invalidated or marked external while the lookup was in flight
|
|
98
|
+
* is not cached: the newer state wins, and an invalidated lookup returns
|
|
99
|
+
* "unknown".
|
|
100
|
+
*/
|
|
101
|
+
export async function getChannelExternality(
|
|
102
|
+
client: WebClient,
|
|
103
|
+
channelId: string,
|
|
104
|
+
): Promise<ChannelExternality> {
|
|
105
|
+
const cached = externalityCache.get(channelId);
|
|
106
|
+
if (cached && cached.expiresAt > Date.now()) return cached.value;
|
|
107
|
+
if (cached) externalityCache.delete(channelId);
|
|
108
|
+
|
|
109
|
+
const generation = channelGeneration.get(channelId);
|
|
110
|
+
let value: "external" | "internal";
|
|
111
|
+
try {
|
|
112
|
+
const resp = await client.conversations.info({ channel: channelId });
|
|
113
|
+
const ch = (resp.channel ?? {}) as {
|
|
114
|
+
is_ext_shared?: boolean;
|
|
115
|
+
is_pending_ext_shared?: boolean;
|
|
116
|
+
};
|
|
117
|
+
value =
|
|
118
|
+
ch.is_ext_shared === true || ch.is_pending_ext_shared === true ? "external" : "internal";
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.warn(
|
|
121
|
+
`[Slack] conversations.info failed for ${channelId}:`,
|
|
122
|
+
scrubSecrets(error instanceof Error ? error.message : String(error)),
|
|
123
|
+
);
|
|
124
|
+
return "unknown";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (channelGeneration.get(channelId) !== generation) {
|
|
128
|
+
const fresher = externalityCache.get(channelId);
|
|
129
|
+
if (fresher && fresher.expiresAt > Date.now()) return fresher.value;
|
|
130
|
+
return value === "external" ? "external" : "unknown";
|
|
131
|
+
}
|
|
132
|
+
setBounded(externalityCache, channelId, { value, expiresAt: Date.now() + EXTERNALITY_TTL_MS });
|
|
133
|
+
return value;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Returns why the swarm must not touch a channel, or null when it may. With the
|
|
138
|
+
* flag on, a channel Slack cannot classify is blocked as "unverified" (fail
|
|
139
|
+
* closed): disclosing swarm output across an unverified boundary is worse than
|
|
140
|
+
* a temporary refusal.
|
|
141
|
+
*/
|
|
142
|
+
export async function getChannelBlockReason(
|
|
143
|
+
client: WebClient,
|
|
144
|
+
channelId: string,
|
|
145
|
+
envelopeFlag?: boolean,
|
|
146
|
+
): Promise<ChannelBlockReason | null> {
|
|
147
|
+
if (!isBlockExternalEnabled()) return null;
|
|
148
|
+
if (envelopeFlag === true) {
|
|
149
|
+
recordExternalChannel(channelId);
|
|
150
|
+
return "external";
|
|
151
|
+
}
|
|
152
|
+
const externality = await getChannelExternality(client, channelId);
|
|
153
|
+
if (externality === "external") return "external";
|
|
154
|
+
if (externality === "unknown") {
|
|
155
|
+
console.warn(
|
|
156
|
+
`[Slack] Could not tell if channel ${channelId} is external; blocking (fail closed)`,
|
|
157
|
+
);
|
|
158
|
+
return "unverified";
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** True when the flag is on and the channel is external or cannot be verified. */
|
|
164
|
+
export async function shouldBlockChannel(
|
|
165
|
+
client: WebClient,
|
|
166
|
+
channelId: string,
|
|
167
|
+
envelopeFlag?: boolean,
|
|
168
|
+
): Promise<boolean> {
|
|
169
|
+
return (await getChannelBlockReason(client, channelId, envelopeFlag)) !== null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function postExternalChannelAlert(
|
|
173
|
+
client: WebClient,
|
|
174
|
+
channelId: string,
|
|
175
|
+
trigger: ExternalChannelTrigger,
|
|
176
|
+
leave: string,
|
|
177
|
+
scopeHint?: string,
|
|
178
|
+
): Promise<void> {
|
|
179
|
+
const alertsChannel = process.env.SLACK_ALERTS_CHANNEL?.trim();
|
|
180
|
+
if (!alertsChannel) {
|
|
181
|
+
console.warn("[Slack] SLACK_ALERTS_CHANNEL not set; skipping external channel alert");
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Durable dedupe: reserve the key briefly, extend it to an hour only after
|
|
186
|
+
// Slack accepts the post, and release it when the post fails.
|
|
187
|
+
const key = `${channelId}:${trigger}`;
|
|
188
|
+
if (!(await reserveAlert(key))) return;
|
|
189
|
+
|
|
190
|
+
const lines = [
|
|
191
|
+
`:no_entry: Swarm bot in external Slack channel <#${channelId}> (${channelId})`,
|
|
192
|
+
`Trigger: ${trigger}`,
|
|
193
|
+
`Leave: ${leave}`,
|
|
194
|
+
...(scopeHint ? [scopeHint] : []),
|
|
195
|
+
"Inbound guard: active. The swarm creates no task from this channel.",
|
|
196
|
+
];
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
await client.chat.postMessage({
|
|
200
|
+
channel: alertsChannel,
|
|
201
|
+
text: scrubSecrets(lines.join("\n")),
|
|
202
|
+
unfurl_links: false,
|
|
203
|
+
unfurl_media: false,
|
|
204
|
+
});
|
|
205
|
+
} catch (error) {
|
|
206
|
+
console.error(
|
|
207
|
+
"[Slack] Failed to send external channel alert:",
|
|
208
|
+
scrubSecrets(error instanceof Error ? error.message : String(error)),
|
|
209
|
+
);
|
|
210
|
+
await settleAlert(key, false);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
await settleAlert(key, true);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** True when this caller may send the alert. A DB failure sends rather than suppresses. */
|
|
217
|
+
async function reserveAlert(key: string): Promise<boolean> {
|
|
218
|
+
try {
|
|
219
|
+
return await claimKv({
|
|
220
|
+
namespace: EXTERNAL_ALERT_KV_NAMESPACE,
|
|
221
|
+
key,
|
|
222
|
+
value: "sending",
|
|
223
|
+
valueType: "string",
|
|
224
|
+
expiresAt: Date.now() + ALERT_SENDING_TTL_MS,
|
|
225
|
+
});
|
|
226
|
+
} catch (error) {
|
|
227
|
+
console.warn("[Slack] External channel alert dedupe unavailable:", error);
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function settleAlert(key: string, sent: boolean): Promise<void> {
|
|
233
|
+
try {
|
|
234
|
+
if (sent) {
|
|
235
|
+
await upsertKv({
|
|
236
|
+
namespace: EXTERNAL_ALERT_KV_NAMESPACE,
|
|
237
|
+
key,
|
|
238
|
+
value: "sent",
|
|
239
|
+
valueType: "string",
|
|
240
|
+
expiresAt: Date.now() + ALERT_DEDUPE_MS,
|
|
241
|
+
});
|
|
242
|
+
} else {
|
|
243
|
+
await deleteKv(EXTERNAL_ALERT_KV_NAMESPACE, key);
|
|
244
|
+
}
|
|
245
|
+
} catch (error) {
|
|
246
|
+
console.warn("[Slack] Failed to record external channel alert state:", error);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Leaves an external channel and posts one alert with the result. */
|
|
251
|
+
export async function leaveExternalChannel(
|
|
252
|
+
client: WebClient,
|
|
253
|
+
channelId: string,
|
|
254
|
+
reason: "joined" | "shared",
|
|
255
|
+
): Promise<void> {
|
|
256
|
+
let leave: string;
|
|
257
|
+
let scopeHint: string | undefined;
|
|
258
|
+
try {
|
|
259
|
+
// A bot that is not a member gets {"ok":false,"not_in_channel":true}; treat it as left.
|
|
260
|
+
await client.conversations.leave({ channel: channelId });
|
|
261
|
+
leave = "left";
|
|
262
|
+
} catch (error) {
|
|
263
|
+
const code = slackCode(error);
|
|
264
|
+
const notInChannel =
|
|
265
|
+
code === "not_in_channel" ||
|
|
266
|
+
(error as { data?: { not_in_channel?: boolean } })?.data?.not_in_channel === true;
|
|
267
|
+
if (notInChannel) {
|
|
268
|
+
leave = "left";
|
|
269
|
+
} else {
|
|
270
|
+
leave = `failed (${code ?? "unknown_error"})`;
|
|
271
|
+
if (code === "missing_scope") scopeHint = slackMissingScopeMessage(error);
|
|
272
|
+
console.error(`[Slack] Failed to leave external channel ${channelId}: ${leave}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (leave === "left") console.log(`[Slack] Left external channel ${channelId} (${reason})`);
|
|
276
|
+
await postExternalChannelAlert(client, channelId, reason, leave, scopeHint);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Boot scan, report only: posts one alert per external channel the bot is in.
|
|
281
|
+
* It never leaves a channel; a human decides.
|
|
282
|
+
*/
|
|
283
|
+
export async function scanExternalChannelsOnBoot(client: WebClient): Promise<void> {
|
|
284
|
+
if (!isBlockExternalEnabled()) return;
|
|
285
|
+
|
|
286
|
+
// kv_entries has no background sweep; drop expired alert keys so the namespace stays bounded.
|
|
287
|
+
await sweepExpiredKv(EXTERNAL_ALERT_KV_NAMESPACE).catch((error) => {
|
|
288
|
+
console.warn("[Slack] Failed to sweep expired external channel alert keys:", error);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
let cursor: string | undefined;
|
|
292
|
+
do {
|
|
293
|
+
const resp = await client.users.conversations({
|
|
294
|
+
types: "public_channel,private_channel",
|
|
295
|
+
exclude_archived: true,
|
|
296
|
+
limit: 200,
|
|
297
|
+
cursor,
|
|
298
|
+
});
|
|
299
|
+
for (const ch of (resp.channels ?? []) as Array<{
|
|
300
|
+
id?: string;
|
|
301
|
+
is_ext_shared?: boolean;
|
|
302
|
+
is_pending_ext_shared?: boolean;
|
|
303
|
+
}>) {
|
|
304
|
+
if (!ch.id || !(ch.is_ext_shared === true || ch.is_pending_ext_shared === true)) continue;
|
|
305
|
+
await postExternalChannelAlert(client, ch.id, "boot-scan", "not attempted (report only)");
|
|
306
|
+
}
|
|
307
|
+
cursor = resp.response_metadata?.next_cursor || undefined;
|
|
308
|
+
} while (cursor);
|
|
309
|
+
}
|
package/src/slack/handlers.ts
CHANGED
|
@@ -44,6 +44,13 @@ import type { AgentMatch } from "./types";
|
|
|
44
44
|
// Side-effect import: registers all Slack event templates in the in-memory registry
|
|
45
45
|
import "./templates";
|
|
46
46
|
import { isEnvFlagEnabled } from "../utils/env-flag";
|
|
47
|
+
import {
|
|
48
|
+
getChannelBlockReason,
|
|
49
|
+
invalidateChannelExternality,
|
|
50
|
+
isBlockExternalEnabled,
|
|
51
|
+
leaveExternalChannel,
|
|
52
|
+
shouldBlockChannel,
|
|
53
|
+
} from "./external-channel";
|
|
47
54
|
import { extractSlackMessageText } from "./message-text";
|
|
48
55
|
import { ensureSlackThreadTree, isSlackRenderV2Enabled } from "./render-v2";
|
|
49
56
|
import { formatSlackSteeringAck, requestSlackThreadSteering } from "./steering";
|
|
@@ -461,6 +468,18 @@ export function registerMessageHandler(app: App): void {
|
|
|
461
468
|
return ignoreSlackInbound("duplicate_message");
|
|
462
469
|
}
|
|
463
470
|
|
|
471
|
+
// Block external (Slack Connect) channels when SLACK_BLOCK_EXTERNAL_CHANNELS is on.
|
|
472
|
+
if (
|
|
473
|
+
await shouldBlockChannel(
|
|
474
|
+
client,
|
|
475
|
+
msg.channel,
|
|
476
|
+
(body as { is_ext_shared_channel?: boolean }).is_ext_shared_channel,
|
|
477
|
+
)
|
|
478
|
+
) {
|
|
479
|
+
console.log(`[Slack] Ignoring message in external channel ${msg.channel}`);
|
|
480
|
+
return ignoreSlackInbound("external_channel");
|
|
481
|
+
}
|
|
482
|
+
|
|
464
483
|
// Check user authorization
|
|
465
484
|
if (!(await isUserAllowed(client, msg.user))) {
|
|
466
485
|
console.log(`[Slack] Ignoring message from unauthorized user ${msg.user}`);
|
|
@@ -1018,4 +1037,46 @@ export function registerMessageHandler(app: App): void {
|
|
|
1018
1037
|
// but we can add specific behavior here if needed
|
|
1019
1038
|
console.log(`[Slack] App mentioned in channel ${event.channel}`);
|
|
1020
1039
|
});
|
|
1040
|
+
|
|
1041
|
+
// External (Slack Connect) channels: leave when the bot joins one, or when a
|
|
1042
|
+
// channel the bot is in becomes shared. All no-ops unless
|
|
1043
|
+
// SLACK_BLOCK_EXTERNAL_CHANNELS is on.
|
|
1044
|
+
app.event("member_joined_channel", async ({ event, client }) => {
|
|
1045
|
+
if (!isBlockExternalEnabled()) return ignoreSlackInbound("external_block_disabled");
|
|
1046
|
+
|
|
1047
|
+
if (!cachedBotUserId) {
|
|
1048
|
+
try {
|
|
1049
|
+
const authResult = await client.auth.test();
|
|
1050
|
+
cachedBotUserId = authResult.user_id as string;
|
|
1051
|
+
cachedBotId = (authResult.bot_id as string | undefined) ?? null;
|
|
1052
|
+
} catch (error) {
|
|
1053
|
+
console.error("[Slack] Failed to cache bot user ID:", error);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
if (!cachedBotUserId || event.user !== cachedBotUserId) {
|
|
1057
|
+
return ignoreSlackInbound("member_joined_not_bot");
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
invalidateChannelExternality(event.channel);
|
|
1061
|
+
// Leave only on positive evidence; an unverified channel stays blocked by the guards.
|
|
1062
|
+
if ((await getChannelBlockReason(client, event.channel)) !== "external") {
|
|
1063
|
+
return ignoreSlackInbound("member_joined_internal");
|
|
1064
|
+
}
|
|
1065
|
+
await leaveExternalChannel(client, event.channel, "joined");
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
app.event("channel_shared", async ({ event, client }) => {
|
|
1069
|
+
if (!isBlockExternalEnabled()) return ignoreSlackInbound("external_block_disabled");
|
|
1070
|
+
|
|
1071
|
+
invalidateChannelExternality(event.channel);
|
|
1072
|
+
if ((await getChannelBlockReason(client, event.channel)) !== "external") {
|
|
1073
|
+
return ignoreSlackInbound("channel_shared_internal");
|
|
1074
|
+
}
|
|
1075
|
+
await leaveExternalChannel(client, event.channel, "shared");
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
app.event("channel_unshared", async ({ event }) => {
|
|
1079
|
+
if (!isBlockExternalEnabled()) return ignoreSlackInbound("external_block_disabled");
|
|
1080
|
+
invalidateChannelExternality(event.channel);
|
|
1081
|
+
});
|
|
1021
1082
|
}
|
package/src/slack/render-v2.ts
CHANGED
|
@@ -961,14 +961,16 @@ export async function childOutcomeContent(
|
|
|
961
961
|
);
|
|
962
962
|
}
|
|
963
963
|
|
|
964
|
-
// A deferred wake continues
|
|
965
|
-
//
|
|
966
|
-
function isSlackContinuation(task: AgentTask): boolean {
|
|
964
|
+
// A lead deferred wake continues the human conversation. Worker wakes report
|
|
965
|
+
// through their lead follow-up instead of posting their own outcome cards.
|
|
966
|
+
async function isSlackContinuation(task: AgentTask): Promise<boolean> {
|
|
967
967
|
return (
|
|
968
968
|
task.source === "schedule" &&
|
|
969
969
|
task.taskType === "deferred" &&
|
|
970
970
|
!!task.slackChannelId &&
|
|
971
|
-
!!task.slackThreadTs
|
|
971
|
+
!!task.slackThreadTs &&
|
|
972
|
+
!!task.agentId &&
|
|
973
|
+
!!(await getAgentById(task.agentId))?.isLead
|
|
972
974
|
);
|
|
973
975
|
}
|
|
974
976
|
|
|
@@ -981,7 +983,7 @@ function isSlackContinuation(task: AgentTask): boolean {
|
|
|
981
983
|
function isChildCardCandidate(task: AgentTask, delegationActivatedAt: string): boolean {
|
|
982
984
|
return (
|
|
983
985
|
task.source !== "slack" &&
|
|
984
|
-
!
|
|
986
|
+
!(task.source === "schedule" && task.taskType === "deferred") &&
|
|
985
987
|
!!task.slackChannelId &&
|
|
986
988
|
!!task.slackThreadTs &&
|
|
987
989
|
(task.status === "completed" || task.status === "failed") &&
|
|
@@ -991,10 +993,10 @@ function isChildCardCandidate(task: AgentTask, delegationActivatedAt: string): b
|
|
|
991
993
|
);
|
|
992
994
|
}
|
|
993
995
|
|
|
994
|
-
function taskNeedsDirectOutcome(task: AgentTask, activatedAt: string): boolean {
|
|
996
|
+
async function taskNeedsDirectOutcome(task: AgentTask, activatedAt: string): Promise<boolean> {
|
|
995
997
|
return (
|
|
996
998
|
((task.source === "slack" && isAskOutcomeStatus(task.status)) ||
|
|
997
|
-
(isSlackContinuation(task) && isOutcomeStatus(task.status))) &&
|
|
999
|
+
((await isSlackContinuation(task)) && isOutcomeStatus(task.status))) &&
|
|
998
1000
|
task.createdAt >= activatedAt
|
|
999
1001
|
);
|
|
1000
1002
|
}
|
|
@@ -1431,48 +1433,34 @@ export async function streamOutcomeCard(
|
|
|
1431
1433
|
});
|
|
1432
1434
|
}
|
|
1433
1435
|
|
|
1434
|
-
/**
|
|
1435
|
-
* The body a resolved deferral card is rewritten to. The ⏳ block promised
|
|
1436
|
-
* the thread an answer; this is that promise being kept in place, instead of
|
|
1437
|
-
* in a second card below it — `isAnsweredByDeferralCard` keeps the wake-up
|
|
1438
|
-
* from posting one, so the answer lands in the thread exactly once. It is
|
|
1439
|
-
* the card the wake-up would have posted, collapsed the same way when the
|
|
1440
|
-
* wake-up already answered by hand with `slack-reply`.
|
|
1441
|
-
*
|
|
1442
|
-
* A wake-up that deferred again does post its own card — the next ⏳ in the
|
|
1443
|
-
* chain, which its own wake-up resolves — so this card only points at it.
|
|
1444
|
-
*/
|
|
1436
|
+
/** Render the latest wait or final result into the chain's original card. */
|
|
1445
1437
|
async function resolvedDeferralContent(
|
|
1446
1438
|
wake: AgentTask,
|
|
1447
1439
|
): Promise<{ text: string; blocks?: unknown[] }> {
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
return { text: presentation, blocks: [{ type: "markdown", text: presentation }, ...captions] };
|
|
1453
|
-
}
|
|
1454
|
-
const card = await getSlackOutcomeMessage(wake.id);
|
|
1455
|
-
const pointer = card?.permalink ? ` — <${card.permalink}|next update>` : ".";
|
|
1456
|
-
return { text: `↪️ Resumed by ${await agentDisplayNameFor(wake)} and deferred again${pointer}` };
|
|
1440
|
+
const content = await outcomeContent(wake, wake.slackReplySent);
|
|
1441
|
+
const presentation = await outcomePresentation(wake, content);
|
|
1442
|
+
const captions = await outcomeCaptionBlocks(wake, content);
|
|
1443
|
+
return { text: presentation, blocks: [{ type: "markdown", text: presentation }, ...captions] };
|
|
1457
1444
|
}
|
|
1458
1445
|
|
|
1459
|
-
/**
|
|
1460
|
-
* True when `task` is a wake-up whose answer goes into its deferral's ⏳ card
|
|
1461
|
-
* (`refreshResolvedDeferralCards`), so it must not post an outcome card of
|
|
1462
|
-
* its own. False — and the wake-up posts its own card as before — when it
|
|
1463
|
-
* deferred again, when the deferral never got a card, and when the in-place
|
|
1464
|
-
* rewrite was abandoned.
|
|
1465
|
-
*/
|
|
1446
|
+
/** A continuation uses the first retained, usable deferral card in its chain. */
|
|
1466
1447
|
async function isAnsweredByDeferralCard(
|
|
1467
1448
|
task: AgentTask,
|
|
1468
1449
|
threadTasks: readonly AgentTask[],
|
|
1469
1450
|
): Promise<boolean> {
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1451
|
+
const seen = new Set<string>();
|
|
1452
|
+
let current = task;
|
|
1453
|
+
while ((await isSlackContinuation(current)) && current.parentTaskId && !seen.has(current.id)) {
|
|
1454
|
+
seen.add(current.id);
|
|
1455
|
+
const deferred = threadTasks.find((candidate) => candidate.id === current.parentTaskId);
|
|
1456
|
+
if (!deferred?.deferredAt) return false;
|
|
1457
|
+
const card = await getSlackOutcomeMessage(deferred.id);
|
|
1458
|
+
if (card) {
|
|
1459
|
+
return !!card.finalizedAt && !isPendingSlackMessage(card) && !card.deferralAbandonedAt;
|
|
1460
|
+
}
|
|
1461
|
+
current = deferred;
|
|
1462
|
+
}
|
|
1463
|
+
return false;
|
|
1476
1464
|
}
|
|
1477
1465
|
|
|
1478
1466
|
/**
|
|
@@ -1490,6 +1478,8 @@ const TRANSIENT_SLACK_ERROR_CODES = new Set([
|
|
|
1490
1478
|
"service_unavailable",
|
|
1491
1479
|
"fatal_error",
|
|
1492
1480
|
"request_timeout",
|
|
1481
|
+
// The external-channel guard could not classify the channel; Slack may answer next time.
|
|
1482
|
+
"external_channel_unverified",
|
|
1493
1483
|
]);
|
|
1494
1484
|
|
|
1495
1485
|
export function slackErrorCode(error: unknown): string | undefined {
|
|
@@ -1539,9 +1529,8 @@ const deferralRefreshAttempts = new Map<string, number>();
|
|
|
1539
1529
|
*
|
|
1540
1530
|
* Runs after the tree loop and independently of it: resolution needs only
|
|
1541
1531
|
* the retained outcome `ts`, so it must not be skipped when the thread has
|
|
1542
|
-
* no other pending render work.
|
|
1543
|
-
*
|
|
1544
|
-
* once (`deferral_resolved_at`).
|
|
1532
|
+
* no other pending render work. Re-defers update the same card; the final
|
|
1533
|
+
* result marks it resolved (`deferral_resolved_at`).
|
|
1545
1534
|
*
|
|
1546
1535
|
* The marker is also burned when Slack refuses the rewrite for good, and when
|
|
1547
1536
|
* an unclassifiable failure has used up its attempts — a card that can never
|
|
@@ -1562,10 +1551,14 @@ export async function refreshResolvedDeferralCards(): Promise<void> {
|
|
|
1562
1551
|
ts: card.ts,
|
|
1563
1552
|
...content,
|
|
1564
1553
|
});
|
|
1565
|
-
|
|
1554
|
+
if (isDeferredTask(wake)) {
|
|
1555
|
+
await updateSlackMessageRecord(card.id, {});
|
|
1556
|
+
} else {
|
|
1557
|
+
await markSlackDeferralResolved(card.id);
|
|
1558
|
+
}
|
|
1566
1559
|
deferralRefreshAttempts.delete(card.id);
|
|
1567
1560
|
// The rewrite is the wake-up's outcome, so it settles the ask's reaction.
|
|
1568
|
-
await finalizeTerminalSlackReactions([wake]);
|
|
1561
|
+
if (!isDeferredTask(wake)) await finalizeTerminalSlackReactions([wake]);
|
|
1569
1562
|
} catch (error) {
|
|
1570
1563
|
const attempts = (deferralRefreshAttempts.get(card.id) ?? 0) + 1;
|
|
1571
1564
|
deferralRefreshAttempts.set(card.id, attempts);
|
|
@@ -1652,7 +1645,7 @@ export async function processSlackRenderV2(): Promise<void> {
|
|
|
1652
1645
|
// not represent immediately active outcome work, so avoid waking an old
|
|
1653
1646
|
// tree solely to verify its permalink on every render tick.
|
|
1654
1647
|
const isDirectCandidate =
|
|
1655
|
-
taskNeedsDirectOutcome(task, activatedAt) &&
|
|
1648
|
+
(await taskNeedsDirectOutcome(task, activatedAt)) &&
|
|
1656
1649
|
isOutcomeStatus(task.status) &&
|
|
1657
1650
|
!(await isAnsweredByDeferralCard(task, tasks));
|
|
1658
1651
|
const isChildCandidate =
|
|
@@ -1751,7 +1744,7 @@ export async function processSlackRenderV2(): Promise<void> {
|
|
|
1751
1744
|
|
|
1752
1745
|
for (const task of tasks) {
|
|
1753
1746
|
if (!isSlackRenderV2Enabled()) return;
|
|
1754
|
-
if (!taskNeedsDirectOutcome(task, activatedAt)) continue;
|
|
1747
|
+
if (!(await taskNeedsDirectOutcome(task, activatedAt))) continue;
|
|
1755
1748
|
const card = await getSlackOutcomeMessage(task.id);
|
|
1756
1749
|
if (card && isSettledSlackMessage(card)) continue;
|
|
1757
1750
|
if (await isAnsweredByDeferralCard(task, tasks)) continue;
|
|
@@ -3,6 +3,7 @@ import type { EntityPresentDetailsArguments, WebClient } from "@slack/web-api";
|
|
|
3
3
|
import { getAgentById, getTaskById } from "../be/db";
|
|
4
4
|
import { isEnvFlagEnabled } from "../utils/env-flag";
|
|
5
5
|
import { getTaskUrl } from "./blocks";
|
|
6
|
+
import { shouldBlockChannel } from "./external-channel";
|
|
6
7
|
import { isUserAllowed } from "./handlers";
|
|
7
8
|
import { slackTaskOutput } from "./task-output";
|
|
8
9
|
|
|
@@ -30,6 +31,10 @@ async function getDetails(event: DetailsEvent, client: WebClient): Promise<Detai
|
|
|
30
31
|
return { error: { status: "restricted" } };
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
if (await shouldBlockChannel(client, event.channel)) {
|
|
35
|
+
return { error: { status: "restricted" } };
|
|
36
|
+
}
|
|
37
|
+
|
|
33
38
|
const task = await getTaskById(ref.id);
|
|
34
39
|
if (!task) return { error: { status: "not_found" } };
|
|
35
40
|
|
|
@@ -22,8 +22,10 @@
|
|
|
22
22
|
* the underlying context becomes irrelevant — e.g. user cancels).
|
|
23
23
|
*
|
|
24
24
|
* Concurrency: single-process, single-event-loop. No cross-instance locking.
|
|
25
|
-
*
|
|
26
|
-
*
|
|
25
|
+
* The buffer is cleared BEFORE the callback fires, so re-enqueues during
|
|
26
|
+
* `onFlush` create a fresh buffer, and its flush can start while the earlier
|
|
27
|
+
* `onFlush` for the same key still runs. A caller that needs per-key order
|
|
28
|
+
* must serialize inside `onFlush`.
|
|
27
29
|
*/
|
|
28
30
|
|
|
29
31
|
export type BufferFlushReason = "timer" | "manual";
|