@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,675 @@
|
|
|
1
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { unlink } from "node:fs/promises";
|
|
4
|
+
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
|
5
|
+
import { Readable } from "node:stream";
|
|
6
|
+
import { SQL } from "bun";
|
|
7
|
+
import { closeDb, createAgent, getDb, getDbClient, initDb, upsertSwarmConfig } from "../be/db";
|
|
8
|
+
import {
|
|
9
|
+
getScriptApiConnectionDescriptors,
|
|
10
|
+
getScriptApiTypes,
|
|
11
|
+
getScriptConnectionById,
|
|
12
|
+
setScriptConnectionEnabled,
|
|
13
|
+
upsertScriptConnection,
|
|
14
|
+
} from "../be/script-connections";
|
|
15
|
+
import { buildScriptCredentialBindings } from "../be/script-credential-broker";
|
|
16
|
+
import { typecheckScript } from "../be/scripts/typecheck";
|
|
17
|
+
import {
|
|
18
|
+
runReadOnlyQuery,
|
|
19
|
+
SQL_MAX_BYTES,
|
|
20
|
+
SQL_MAX_ROWS,
|
|
21
|
+
setSqlExecutorForTesting,
|
|
22
|
+
} from "../be/sql-connections";
|
|
23
|
+
import { handleScriptConnectionProxy } from "../http/script-connection-proxy";
|
|
24
|
+
import { handleScriptConnections } from "../http/script-connections";
|
|
25
|
+
import { getPathSegments, parseQueryParams } from "../http/utils";
|
|
26
|
+
import { createApiRegistryClient } from "../scripts-runtime/api-client";
|
|
27
|
+
import { runScript } from "../scripts-runtime/loader";
|
|
28
|
+
import { SwarmConfig } from "../scripts-runtime/swarm-config";
|
|
29
|
+
import { refreshSecretScrubberCache } from "../utils/secret-scrubber";
|
|
30
|
+
import { SKIP_SANDBOX_SPAWN_TESTS } from "./sandbox-spawn-test-helpers";
|
|
31
|
+
import { listenOnFreePort } from "./test-net";
|
|
32
|
+
|
|
33
|
+
const TEST_DB_PATH = "./test-script-connections-sql.sqlite";
|
|
34
|
+
const API_KEY = "example-test-script-connections-sql-key-1234567890";
|
|
35
|
+
const DSN = "postgres://reader:sql-conn-test-password-123@db.internal.test:5432/analytics";
|
|
36
|
+
const REAL_PG_URL = process.env.SQL_CONNECTION_TEST_URL;
|
|
37
|
+
const spawnTest = test.skipIf(SKIP_SANDBOX_SPAWN_TESTS);
|
|
38
|
+
|
|
39
|
+
let leadId: string;
|
|
40
|
+
let workerId: string;
|
|
41
|
+
let executorCalls: Array<{ dsn: string; query: string; params: unknown[] }> = [];
|
|
42
|
+
const savedEnv = { ...process.env };
|
|
43
|
+
|
|
44
|
+
async function removeDbFiles(path: string): Promise<void> {
|
|
45
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
46
|
+
await unlink(path + suffix).catch((error) => {
|
|
47
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function dispatch(
|
|
53
|
+
path: string,
|
|
54
|
+
init: { method?: string; body?: unknown; agentId?: string } = {},
|
|
55
|
+
): Promise<{ status: number; text: string; json: () => Record<string, unknown> }> {
|
|
56
|
+
const req = Readable.from(
|
|
57
|
+
init.body === undefined ? [] : [Buffer.from(JSON.stringify(init.body))],
|
|
58
|
+
) as IncomingMessage;
|
|
59
|
+
req.method = init.method ?? "GET";
|
|
60
|
+
req.url = path;
|
|
61
|
+
req.headers = {
|
|
62
|
+
"content-type": "application/json",
|
|
63
|
+
...(init.agentId ? { "x-agent-id": init.agentId } : {}),
|
|
64
|
+
};
|
|
65
|
+
let status = 200;
|
|
66
|
+
let text = "";
|
|
67
|
+
const res = {
|
|
68
|
+
headersSent: false,
|
|
69
|
+
writableEnded: false,
|
|
70
|
+
setHeader() {},
|
|
71
|
+
writeHead(code: number) {
|
|
72
|
+
status = code;
|
|
73
|
+
this.headersSent = true;
|
|
74
|
+
return this;
|
|
75
|
+
},
|
|
76
|
+
end(chunk?: unknown) {
|
|
77
|
+
if (chunk !== undefined) text += String(chunk);
|
|
78
|
+
this.writableEnded = true;
|
|
79
|
+
return this;
|
|
80
|
+
},
|
|
81
|
+
} as unknown as ServerResponse;
|
|
82
|
+
const pathSegments = getPathSegments(path);
|
|
83
|
+
const queryParams = parseQueryParams(path);
|
|
84
|
+
const handled =
|
|
85
|
+
(await handleScriptConnectionProxy(req, res, pathSegments, queryParams, init.agentId)) ||
|
|
86
|
+
(await handleScriptConnections(req, res, pathSegments, queryParams, init.agentId));
|
|
87
|
+
if (!handled) status = 404;
|
|
88
|
+
return { status, text, json: () => JSON.parse(text) };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function registerSql(body: Record<string, unknown>, agentId = leadId) {
|
|
92
|
+
return dispatch("/api/script-connections", {
|
|
93
|
+
method: "POST",
|
|
94
|
+
agentId,
|
|
95
|
+
body: { kind: "sql", slug: "analytics", ...body },
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function sqlQuery(connectionId: string, agentId: string, body: Record<string, unknown>) {
|
|
100
|
+
return dispatch(`/api/script-connections/${connectionId}/sql-query`, {
|
|
101
|
+
method: "POST",
|
|
102
|
+
agentId,
|
|
103
|
+
body,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function configRows(key: string) {
|
|
108
|
+
return getDbClient().query<{
|
|
109
|
+
scope: string;
|
|
110
|
+
scopeId: string | null;
|
|
111
|
+
value: string;
|
|
112
|
+
encrypted: number;
|
|
113
|
+
}>("SELECT scope, scopeId, value, encrypted FROM swarm_config WHERE key = ?", [key]);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
beforeAll(async () => {
|
|
117
|
+
await removeDbFiles(TEST_DB_PATH);
|
|
118
|
+
closeDb();
|
|
119
|
+
initDb(TEST_DB_PATH);
|
|
120
|
+
process.env.AGENT_SWARM_API_KEY = API_KEY;
|
|
121
|
+
delete process.env.API_KEY;
|
|
122
|
+
refreshSecretScrubberCache();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
afterAll(async () => {
|
|
126
|
+
setSqlExecutorForTesting(null);
|
|
127
|
+
closeDb();
|
|
128
|
+
await removeDbFiles(TEST_DB_PATH);
|
|
129
|
+
for (const key of Object.keys(process.env)) {
|
|
130
|
+
if (!(key in savedEnv)) delete process.env[key];
|
|
131
|
+
}
|
|
132
|
+
Object.assign(process.env, savedEnv);
|
|
133
|
+
refreshSecretScrubberCache();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
beforeEach(async () => {
|
|
137
|
+
const client = getDbClient();
|
|
138
|
+
await client.run("DELETE FROM script_connections");
|
|
139
|
+
await client.run("DELETE FROM script_credential_bindings");
|
|
140
|
+
await client.run("DELETE FROM swarm_config");
|
|
141
|
+
await client.run("DELETE FROM agents");
|
|
142
|
+
leadId = (await createAgent({ name: "sql-lead", isLead: true, status: "idle" })).id;
|
|
143
|
+
workerId = (await createAgent({ name: "sql-worker", isLead: false, status: "idle" })).id;
|
|
144
|
+
executorCalls = [];
|
|
145
|
+
setSqlExecutorForTesting(async (dsn, query, params, take) => {
|
|
146
|
+
executorCalls.push({ dsn, query, params });
|
|
147
|
+
take([{ x: 1 }]);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe("migration 213", () => {
|
|
152
|
+
test("adds 'sql' to the kind CHECK and requires a DSN key for sql rows", async () => {
|
|
153
|
+
const schema = getDb()
|
|
154
|
+
.query<{ sql: string }, []>(
|
|
155
|
+
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'script_connections'",
|
|
156
|
+
)
|
|
157
|
+
.get();
|
|
158
|
+
expect(schema?.sql).toContain("'graphql', 'sql'");
|
|
159
|
+
await expect(
|
|
160
|
+
getDbClient().run(
|
|
161
|
+
"INSERT INTO script_connections (id, slug, kind) VALUES (?, 'no-key', 'sql')",
|
|
162
|
+
[crypto.randomUUID()],
|
|
163
|
+
),
|
|
164
|
+
).rejects.toThrow(/CHECK constraint failed/);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("rebuild keeps existing rows, auth columns, and managed bindings", async () => {
|
|
168
|
+
const graphql = await upsertScriptConnection({
|
|
169
|
+
slug: "vendor",
|
|
170
|
+
kind: "graphql",
|
|
171
|
+
baseUrl: "https://vendor.example.test/graphql",
|
|
172
|
+
allowedHosts: ["vendor.example.test"],
|
|
173
|
+
auth: { type: "bearer", secret: "vendor-secret-value-123456" },
|
|
174
|
+
});
|
|
175
|
+
expect(graphql.credentialBindingId).not.toBeNull();
|
|
176
|
+
const before = await getDbClient().query<Record<string, unknown>>(
|
|
177
|
+
"SELECT * FROM script_connections ORDER BY id",
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const db = getDb();
|
|
181
|
+
db.run("PRAGMA foreign_keys = OFF");
|
|
182
|
+
try {
|
|
183
|
+
db.exec(
|
|
184
|
+
readFileSync(
|
|
185
|
+
new URL("../be/migrations/213_script_connections_sql.sql", import.meta.url),
|
|
186
|
+
"utf-8",
|
|
187
|
+
),
|
|
188
|
+
);
|
|
189
|
+
} finally {
|
|
190
|
+
db.run("PRAGMA foreign_keys = ON");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const after = await getDbClient().query<Record<string, unknown>>(
|
|
194
|
+
"SELECT * FROM script_connections ORDER BY id",
|
|
195
|
+
);
|
|
196
|
+
expect(after).toEqual(before);
|
|
197
|
+
expect(after[0]?.auth_type).toBe("bearer");
|
|
198
|
+
expect(after[0]?.auth_config_key).toBe("connection.vendor.secret");
|
|
199
|
+
const bindings = await getDbClient().query<{ id: string }>(
|
|
200
|
+
"SELECT id FROM script_credential_bindings WHERE managed_by_connection_id = ?",
|
|
201
|
+
[graphql.id],
|
|
202
|
+
);
|
|
203
|
+
expect(bindings.map((binding) => binding.id)).toEqual([graphql.credentialBindingId!]);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe("sql connection registration", () => {
|
|
208
|
+
test("inline DSN is stored encrypted under connection.<slug>.secret and never echoed", async () => {
|
|
209
|
+
const response = await registerSql({ dsn: { secret: DSN } });
|
|
210
|
+
expect(response.status).toBe(200);
|
|
211
|
+
expect(response.text).not.toContain("postgres://");
|
|
212
|
+
const connection = (response.json().connection ?? {}) as Record<string, unknown>;
|
|
213
|
+
expect(connection).toMatchObject({
|
|
214
|
+
kind: "sql",
|
|
215
|
+
authType: "none",
|
|
216
|
+
authConfigKey: "connection.analytics.secret",
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const rows = await configRows("connection.analytics.secret");
|
|
220
|
+
expect(rows).toHaveLength(1);
|
|
221
|
+
expect(rows[0]).toMatchObject({ scope: "global", encrypted: 1 });
|
|
222
|
+
expect(rows[0]?.value).not.toContain("postgres://");
|
|
223
|
+
|
|
224
|
+
const stored = await getScriptConnectionById(String(connection.id));
|
|
225
|
+
expect(stored?.generatedRuntimeJson).not.toContain("postgres://");
|
|
226
|
+
expect(JSON.parse(stored?.generatedRuntimeJson ?? "{}")).toEqual({
|
|
227
|
+
slug: "analytics",
|
|
228
|
+
kind: "sql",
|
|
229
|
+
connectionId: connection.id,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const detail = await dispatch(`/api/script-connections/${connection.id}`, { agentId: leadId });
|
|
233
|
+
const list = await dispatch("/api/script-connections", { agentId: leadId });
|
|
234
|
+
expect(detail.status).toBe(200);
|
|
235
|
+
expect(list.status).toBe(200);
|
|
236
|
+
expect(detail.text).not.toContain("postgres://");
|
|
237
|
+
expect(list.text).not.toContain("postgres://");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("registration is lead-only", async () => {
|
|
241
|
+
const response = await registerSql({ dsn: { secret: DSN } }, workerId);
|
|
242
|
+
expect(response.status).toBe(403);
|
|
243
|
+
expect(await configRows("connection.analytics.secret")).toHaveLength(0);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("rejects a non-Postgres DSN without echoing it", async () => {
|
|
247
|
+
for (const secret of ["sqlite:///var/lib/swarm/agent-swarm-db.sqlite", "not a url at all"]) {
|
|
248
|
+
const response = await registerSql({ dsn: { secret } });
|
|
249
|
+
expect(response.status).toBe(400);
|
|
250
|
+
expect(response.text).toContain("postgres://");
|
|
251
|
+
expect(response.text).not.toContain(secret);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("rejects inline auth, both or neither DSN fields, and reserved config keys", async () => {
|
|
256
|
+
const auth = await registerSql({
|
|
257
|
+
dsn: { secret: DSN },
|
|
258
|
+
auth: { type: "bearer", secret: "token-value-1234567890" },
|
|
259
|
+
});
|
|
260
|
+
expect(auth.status).toBe(400);
|
|
261
|
+
expect(auth.text).toContain("`auth` is not supported");
|
|
262
|
+
|
|
263
|
+
const both = await registerSql({ dsn: { secret: DSN, configKey: "ANALYTICS_DSN" } });
|
|
264
|
+
expect(both.status).toBe(400);
|
|
265
|
+
const missing = await registerSql({});
|
|
266
|
+
expect(missing.status).toBe(400);
|
|
267
|
+
expect(missing.text).toContain("dsn.secret");
|
|
268
|
+
|
|
269
|
+
const reserved = await registerSql({ dsn: { configKey: "connection.other.secret" } });
|
|
270
|
+
expect(reserved.status).toBe(400);
|
|
271
|
+
expect(reserved.text).toContain("reserved");
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("configKey DSN stores the key as-is", async () => {
|
|
275
|
+
const response = await registerSql({ dsn: { configKey: "ANALYTICS_DSN" } });
|
|
276
|
+
expect(response.status).toBe(200);
|
|
277
|
+
expect(response.json().connection).toMatchObject({ authConfigKey: "ANALYTICS_DSN" });
|
|
278
|
+
expect(await configRows("connection.analytics.secret")).toHaveLength(0);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test("edit without dsn keeps the key; a slug rename with a new DSN drops the old secret", async () => {
|
|
282
|
+
const created = (await registerSql({ dsn: { secret: DSN } })).json().connection as {
|
|
283
|
+
id: string;
|
|
284
|
+
};
|
|
285
|
+
const renamed = await registerSql({ id: created.id, slug: "warehouse", displayName: "WH" });
|
|
286
|
+
expect(renamed.status).toBe(200);
|
|
287
|
+
expect(renamed.json().connection).toMatchObject({
|
|
288
|
+
slug: "warehouse",
|
|
289
|
+
authConfigKey: "connection.analytics.secret",
|
|
290
|
+
});
|
|
291
|
+
expect(await configRows("connection.analytics.secret")).toHaveLength(1);
|
|
292
|
+
|
|
293
|
+
const rekeyed = await registerSql({ id: created.id, slug: "warehouse", dsn: { secret: DSN } });
|
|
294
|
+
expect(rekeyed.status).toBe(200);
|
|
295
|
+
expect(rekeyed.json().connection).toMatchObject({
|
|
296
|
+
authConfigKey: "connection.warehouse.secret",
|
|
297
|
+
});
|
|
298
|
+
expect(await configRows("connection.analytics.secret")).toHaveLength(0);
|
|
299
|
+
expect(await configRows("connection.warehouse.secret")).toHaveLength(1);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("descriptors and types expose ctx.api.<slug>.query and typecheck", async () => {
|
|
303
|
+
const created = (await registerSql({ dsn: { secret: DSN } })).json().connection as {
|
|
304
|
+
id: string;
|
|
305
|
+
};
|
|
306
|
+
expect(getScriptApiConnectionDescriptors({ agentId: workerId })).toContainEqual({
|
|
307
|
+
slug: "analytics",
|
|
308
|
+
kind: "sql",
|
|
309
|
+
connectionId: created.id,
|
|
310
|
+
});
|
|
311
|
+
expect(getScriptApiTypes({ agentId: workerId })).toContain("query<T");
|
|
312
|
+
|
|
313
|
+
const typecheck = await typecheckScript(
|
|
314
|
+
`
|
|
315
|
+
import type { ScriptMain } from "swarm-sdk";
|
|
316
|
+
const main: ScriptMain = async (_args, ctx) => {
|
|
317
|
+
const { rows, rowCount, truncated } = await ctx.api.analytics.query("select $1::int as x", [1]);
|
|
318
|
+
return { rows, rowCount, truncated };
|
|
319
|
+
};
|
|
320
|
+
export default main;
|
|
321
|
+
`,
|
|
322
|
+
{ agentId: workerId },
|
|
323
|
+
);
|
|
324
|
+
expect(typecheck).toEqual({ ok: true });
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
describe("sql-query route", () => {
|
|
329
|
+
async function createSql(extra: Partial<Parameters<typeof upsertScriptConnection>[0]> = {}) {
|
|
330
|
+
return upsertScriptConnection({
|
|
331
|
+
slug: "analytics",
|
|
332
|
+
kind: "sql",
|
|
333
|
+
sqlDsn: { secret: DSN },
|
|
334
|
+
...extra,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
test("a non-lead agent runs a query with the server-resolved DSN", async () => {
|
|
339
|
+
const connection = await createSql();
|
|
340
|
+
const response = await sqlQuery(connection.id, workerId, {
|
|
341
|
+
sql: "select $1::int as x",
|
|
342
|
+
params: [1],
|
|
343
|
+
});
|
|
344
|
+
expect(response.status).toBe(200);
|
|
345
|
+
expect(response.json()).toEqual({ ok: true, rows: [{ x: 1 }], rowCount: 1, truncated: false });
|
|
346
|
+
expect(response.text).not.toContain("postgres://");
|
|
347
|
+
expect(executorCalls).toEqual([{ dsn: DSN, query: "select $1::int as x", params: [1] }]);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test("gates: disabled, agent scope, wrong kind, unknown id", async () => {
|
|
351
|
+
const connection = await createSql();
|
|
352
|
+
await setScriptConnectionEnabled(connection.id, false);
|
|
353
|
+
expect((await sqlQuery(connection.id, workerId, { sql: "select 1" })).status).toBe(403);
|
|
354
|
+
|
|
355
|
+
const owned = await createSql({ slug: "owned", scope: "agent", scopeId: leadId });
|
|
356
|
+
expect((await sqlQuery(owned.id, workerId, { sql: "select 1" })).status).toBe(403);
|
|
357
|
+
|
|
358
|
+
const graphql = await upsertScriptConnection({
|
|
359
|
+
slug: "vendor",
|
|
360
|
+
kind: "graphql",
|
|
361
|
+
baseUrl: "https://vendor.example.test/graphql",
|
|
362
|
+
allowedHosts: ["vendor.example.test"],
|
|
363
|
+
});
|
|
364
|
+
expect((await sqlQuery(graphql.id, workerId, { sql: "select 1" })).status).toBe(400);
|
|
365
|
+
const enabledSql = await createSql({ slug: "enabled" });
|
|
366
|
+
const mcpCall = await dispatch(`/api/script-connections/${enabledSql.id}/mcp-call`, {
|
|
367
|
+
method: "POST",
|
|
368
|
+
agentId: workerId,
|
|
369
|
+
body: { tool: "query" },
|
|
370
|
+
});
|
|
371
|
+
expect(mcpCall.status).toBe(400);
|
|
372
|
+
expect((await sqlQuery(crypto.randomUUID(), workerId, { sql: "select 1" })).status).toBe(404);
|
|
373
|
+
expect(executorCalls).toHaveLength(0);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test("repo-scoped connections resolve the repo-scope secret", async () => {
|
|
377
|
+
const repoId = crypto.randomUUID();
|
|
378
|
+
const connection = await createSql({ scope: "repo", scopeId: repoId });
|
|
379
|
+
expect(await configRows("connection.analytics.secret")).toEqual([
|
|
380
|
+
expect.objectContaining({ scope: "repo", scopeId: repoId }),
|
|
381
|
+
]);
|
|
382
|
+
const response = await sqlQuery(connection.id, workerId, { sql: "select 1" });
|
|
383
|
+
expect(response.json()).toMatchObject({ ok: true });
|
|
384
|
+
expect(executorCalls[0]?.dsn).toBe(DSN);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
test("never falls back to process.env for the DSN key", async () => {
|
|
388
|
+
process.env.ANALYTICS_ENV_DSN = DSN;
|
|
389
|
+
try {
|
|
390
|
+
const connection = await createSql({ sqlDsn: { configKey: "ANALYTICS_ENV_DSN" } });
|
|
391
|
+
const response = await sqlQuery(connection.id, workerId, { sql: "select 1" });
|
|
392
|
+
expect(response.json()).toEqual({
|
|
393
|
+
ok: false,
|
|
394
|
+
error: "SQL connection DSN 'ANALYTICS_ENV_DSN' is not set.",
|
|
395
|
+
});
|
|
396
|
+
expect(executorCalls).toHaveLength(0);
|
|
397
|
+
} finally {
|
|
398
|
+
delete process.env.ANALYTICS_ENV_DSN;
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
test("a stored non-Postgres DSN is refused before any connection", async () => {
|
|
403
|
+
await upsertSwarmConfig({
|
|
404
|
+
scope: "global",
|
|
405
|
+
key: "LOCAL_FILE_DSN",
|
|
406
|
+
value: "sqlite:///tmp/agent-swarm-db.sqlite",
|
|
407
|
+
isSecret: true,
|
|
408
|
+
});
|
|
409
|
+
const connection = await createSql({ sqlDsn: { configKey: "LOCAL_FILE_DSN" } });
|
|
410
|
+
const response = await sqlQuery(connection.id, workerId, { sql: "select 1" });
|
|
411
|
+
expect(response.json()).toMatchObject({ ok: false });
|
|
412
|
+
expect(response.text).not.toContain("sqlite:///");
|
|
413
|
+
expect(executorCalls).toHaveLength(0);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
test("error text is scrubbed of the DSN and its password", async () => {
|
|
417
|
+
const connection = await createSql();
|
|
418
|
+
setSqlExecutorForTesting(async () => {
|
|
419
|
+
throw new Error(`connect failed for ${DSN} (password sql-conn-test-password-123)`);
|
|
420
|
+
});
|
|
421
|
+
const response = await sqlQuery(connection.id, workerId, { sql: "select 1" });
|
|
422
|
+
const body = response.json();
|
|
423
|
+
expect(body.ok).toBe(false);
|
|
424
|
+
expect(String(body.error)).toContain("[REDACTED");
|
|
425
|
+
expect(response.text).not.toContain("sql-conn-test-password-123");
|
|
426
|
+
expect(response.text).not.toContain("postgres://");
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test("caps rows, stops fetching, and reports truncation", async () => {
|
|
430
|
+
const connection = await createSql();
|
|
431
|
+
const accepted: boolean[] = [];
|
|
432
|
+
setSqlExecutorForTesting(async (_dsn, _query, _params, take) => {
|
|
433
|
+
for (let b = 0; b < 20; b++) {
|
|
434
|
+
const more = take(Array.from({ length: 100 }, (_, i) => ({ i: b * 100 + i })));
|
|
435
|
+
accepted.push(more);
|
|
436
|
+
if (!more) return;
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
const body = (await sqlQuery(connection.id, workerId, { sql: "select 1" })).json();
|
|
440
|
+
expect(body).toMatchObject({ ok: true, rowCount: SQL_MAX_ROWS, truncated: true });
|
|
441
|
+
expect((body.rows as unknown[]).length).toBe(SQL_MAX_ROWS);
|
|
442
|
+
expect(accepted).toEqual([...Array(10).fill(true), false]);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
test("caps the serialized size and reports truncation", async () => {
|
|
446
|
+
const connection = await createSql();
|
|
447
|
+
const big = "x".repeat(1024 * 1024);
|
|
448
|
+
setSqlExecutorForTesting(async (_dsn, _query, _params, take) => {
|
|
449
|
+
take(Array.from({ length: 10 }, (_, i) => ({ i, big })));
|
|
450
|
+
});
|
|
451
|
+
const response = await sqlQuery(connection.id, workerId, { sql: "select 1" });
|
|
452
|
+
const body = response.json();
|
|
453
|
+
expect(body).toMatchObject({ ok: true, rowCount: 4, truncated: true });
|
|
454
|
+
expect(response.text.length).toBeLessThan(SQL_MAX_BYTES + 1024);
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
describe("ctx.api sql runtime", () => {
|
|
459
|
+
test("query posts to the sql-query route as the script's agent", async () => {
|
|
460
|
+
const calls: Array<{ url: string; agent: string | null; body: unknown }> = [];
|
|
461
|
+
const savedFetch = globalThis.fetch;
|
|
462
|
+
globalThis.fetch = (async (input, init) => {
|
|
463
|
+
calls.push({
|
|
464
|
+
url: String(input),
|
|
465
|
+
agent: new Headers(init?.headers).get("x-agent-id"),
|
|
466
|
+
body: JSON.parse(String(init?.body)),
|
|
467
|
+
});
|
|
468
|
+
return Response.json({ ok: true, rows: [{ x: 1 }], rowCount: 1, truncated: false });
|
|
469
|
+
}) as typeof globalThis.fetch;
|
|
470
|
+
try {
|
|
471
|
+
const swarmConfig = new SwarmConfig({
|
|
472
|
+
system: {
|
|
473
|
+
apiKey: { value: API_KEY, isSecret: true },
|
|
474
|
+
agentId: { value: "agent-runtime", isSecret: false },
|
|
475
|
+
mcpBaseUrl: { value: "http://swarm.test/", isSecret: false },
|
|
476
|
+
},
|
|
477
|
+
user: {},
|
|
478
|
+
});
|
|
479
|
+
const api = createApiRegistryClient(
|
|
480
|
+
[{ slug: "analytics", kind: "sql", connectionId: "conn-1" }],
|
|
481
|
+
{ swarmConfig },
|
|
482
|
+
);
|
|
483
|
+
expect(await api.analytics!.query!("select $1::int as x", [1])).toEqual({
|
|
484
|
+
rows: [{ x: 1 }],
|
|
485
|
+
rowCount: 1,
|
|
486
|
+
truncated: false,
|
|
487
|
+
});
|
|
488
|
+
expect(calls).toEqual([
|
|
489
|
+
{
|
|
490
|
+
url: "http://swarm.test/api/script-connections/conn-1/sql-query",
|
|
491
|
+
agent: "agent-runtime",
|
|
492
|
+
body: { sql: "select $1::int as x", params: [1] },
|
|
493
|
+
},
|
|
494
|
+
]);
|
|
495
|
+
} finally {
|
|
496
|
+
globalThis.fetch = savedFetch;
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
for (const executor of ["native", "quickjs"]) {
|
|
501
|
+
spawnTest(`script round trip through the ${executor} executor`, async () => {
|
|
502
|
+
const connection = await upsertScriptConnection({
|
|
503
|
+
slug: "analytics",
|
|
504
|
+
kind: "sql",
|
|
505
|
+
sqlDsn: { secret: DSN },
|
|
506
|
+
});
|
|
507
|
+
const server = createServer((req, res) => {
|
|
508
|
+
const url = req.url ?? "/";
|
|
509
|
+
const agentId = req.headers["x-agent-id"] as string | undefined;
|
|
510
|
+
void handleScriptConnectionProxy(
|
|
511
|
+
req,
|
|
512
|
+
res,
|
|
513
|
+
getPathSegments(url),
|
|
514
|
+
parseQueryParams(url),
|
|
515
|
+
agentId,
|
|
516
|
+
).then((handled) => {
|
|
517
|
+
if (!handled) res.writeHead(404).end();
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
const port = await listenOnFreePort(server, "127.0.0.1");
|
|
521
|
+
const savedExecutor = process.env.SCRIPT_EXECUTOR;
|
|
522
|
+
process.env.SCRIPT_EXECUTOR = executor;
|
|
523
|
+
try {
|
|
524
|
+
const apiConnections = getScriptApiConnectionDescriptors({ agentId: workerId });
|
|
525
|
+
const egressSecrets = await buildScriptCredentialBindings({ agentId: workerId });
|
|
526
|
+
expect(JSON.stringify({ apiConnections, egressSecrets })).not.toContain("postgres://");
|
|
527
|
+
const output = await runScript({
|
|
528
|
+
agentId: workerId,
|
|
529
|
+
mcpBaseUrl: `http://127.0.0.1:${port}`,
|
|
530
|
+
apiConnections,
|
|
531
|
+
egressSecrets,
|
|
532
|
+
source: `
|
|
533
|
+
export default async (_args, ctx) => ctx.api.analytics.query("select 1 as x");
|
|
534
|
+
`,
|
|
535
|
+
});
|
|
536
|
+
expect(output.error).toBeUndefined();
|
|
537
|
+
expect(output.result).toEqual({ rows: [{ x: 1 }], rowCount: 1, truncated: false });
|
|
538
|
+
expect(executorCalls).toEqual([{ dsn: DSN, query: "select 1 as x", params: [] }]);
|
|
539
|
+
expect(connection.kind).toBe("sql");
|
|
540
|
+
} finally {
|
|
541
|
+
if (savedExecutor === undefined) delete process.env.SCRIPT_EXECUTOR;
|
|
542
|
+
else process.env.SCRIPT_EXECUTOR = savedExecutor;
|
|
543
|
+
server.close();
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
// Proves the read-only guarantee against a real Postgres. CI sets the URL from a postgres service
|
|
550
|
+
// in merge-gate.yml and ci.yml; locally it is opt-in.
|
|
551
|
+
// SQL_CONNECTION_TEST_URL=postgres://postgres:<pw>@127.0.0.1:55432/postgres
|
|
552
|
+
describe.skipIf(!REAL_PG_URL)("read-only against real Postgres", () => {
|
|
553
|
+
const dsn = REAL_PG_URL ?? "";
|
|
554
|
+
const table = "sql_conn_readonly_probe";
|
|
555
|
+
const reader = "sql_conn_reader_probe";
|
|
556
|
+
let admin: SQL;
|
|
557
|
+
|
|
558
|
+
async function rowCount(): Promise<number> {
|
|
559
|
+
const [row] = await admin.unsafe(`select count(*)::int as n from ${table}`);
|
|
560
|
+
return (row as { n: number }).n;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
async function sqlState(query: string, params: unknown[] = []): Promise<string> {
|
|
564
|
+
setSqlExecutorForTesting(null);
|
|
565
|
+
const err = await runReadOnlyQuery(dsn, query, params).then(
|
|
566
|
+
() => null,
|
|
567
|
+
(e: Error) => e,
|
|
568
|
+
);
|
|
569
|
+
expect(err).not.toBeNull();
|
|
570
|
+
return /SQLSTATE (\w+)/.exec(err?.message ?? "")?.[1] ?? "none";
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
beforeAll(async () => {
|
|
574
|
+
admin = new SQL(dsn);
|
|
575
|
+
await admin.unsafe(`drop table if exists ${table}`);
|
|
576
|
+
await admin.unsafe(`create table ${table} (id serial primary key, v int)`);
|
|
577
|
+
await admin.unsafe(`insert into ${table} (v) values (1), (2)`);
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
afterAll(async () => {
|
|
581
|
+
await admin.unsafe(`drop table if exists ${table}`);
|
|
582
|
+
await admin.unsafe(`drop owned by ${reader}`).catch(() => undefined);
|
|
583
|
+
await admin.unsafe(`drop role if exists ${reader}`);
|
|
584
|
+
await admin.close();
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
beforeEach(() => setSqlExecutorForTesting(null));
|
|
588
|
+
|
|
589
|
+
test("select returns rows", async () => {
|
|
590
|
+
const result = await runReadOnlyQuery(dsn, `select v from ${table} where v = $1`, [2]);
|
|
591
|
+
expect(result).toEqual({ rows: [{ v: 2 }], rowCount: 1, truncated: false });
|
|
592
|
+
expect(await runReadOnlyQuery(dsn, "select 1 as x")).toMatchObject({ rows: [{ x: 1 }] });
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
test("writes are refused by the cursor grammar or the read-only transaction", async () => {
|
|
596
|
+
// 42601: DECLARE ... CURSOR FOR only takes a query. 0A000: no data-modifying WITH.
|
|
597
|
+
// 25006: read_only_sql_transaction, for writes hidden inside a SELECT.
|
|
598
|
+
expect(await sqlState(`insert into ${table} (v) values (3)`)).toBe("42601");
|
|
599
|
+
expect(await sqlState(`insert into ${table} (v) values ($1)`, [3])).toBe("42601");
|
|
600
|
+
expect(await sqlState(`update ${table} set v = 9`)).toBe("42601");
|
|
601
|
+
expect(await sqlState(`delete from ${table}`)).toBe("42601");
|
|
602
|
+
expect(await sqlState("create table sql_conn_should_not_exist (id int)")).toBe("42601");
|
|
603
|
+
expect(await sqlState(`with d as (delete from ${table} returning *) select * from d`)).toBe(
|
|
604
|
+
"0A000",
|
|
605
|
+
);
|
|
606
|
+
expect(await sqlState(`select nextval('${table}_id_seq')`)).toBe("25006");
|
|
607
|
+
expect(await rowCount()).toBe(2);
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
test("the cursor stops at the row cap", async () => {
|
|
611
|
+
expect(
|
|
612
|
+
await runReadOnlyQuery(dsn, "select generate_series(1, $1::int) as i", [5000]),
|
|
613
|
+
).toMatchObject({
|
|
614
|
+
rowCount: SQL_MAX_ROWS,
|
|
615
|
+
truncated: true,
|
|
616
|
+
});
|
|
617
|
+
expect(
|
|
618
|
+
await runReadOnlyQuery(dsn, `select generate_series(1, ${SQL_MAX_ROWS}) as i`),
|
|
619
|
+
).toMatchObject({ rowCount: SQL_MAX_ROWS, truncated: false });
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
test("multi-statement calls are rejected, with or without params", async () => {
|
|
623
|
+
// 42601: "cannot insert multiple commands into a prepared statement".
|
|
624
|
+
expect(await sqlState(`select 1; delete from ${table}`)).toBe("42601");
|
|
625
|
+
expect(await sqlState(`select $1::int; delete from ${table}`, [1])).toBe("42601");
|
|
626
|
+
expect(
|
|
627
|
+
await sqlState(
|
|
628
|
+
`commit; set default_transaction_read_only = off; commit; insert into ${table} (v) values (4)`,
|
|
629
|
+
),
|
|
630
|
+
).toBe("42601");
|
|
631
|
+
expect(await rowCount()).toBe(2);
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
test("session state never carries into the next call", async () => {
|
|
635
|
+
await runReadOnlyQuery(dsn, "set default_transaction_read_only = off").catch(() => undefined);
|
|
636
|
+
await runReadOnlyQuery(dsn, "select set_config('default_transaction_read_only', 'off', false)");
|
|
637
|
+
expect(await sqlState(`select nextval('${table}_id_seq')`)).toBe("25006");
|
|
638
|
+
await runReadOnlyQuery(dsn, "select set_config('transaction_read_only', 'off', false)").catch(
|
|
639
|
+
() => undefined,
|
|
640
|
+
);
|
|
641
|
+
expect(await sqlState(`select nextval('${table}_id_seq')`)).toBe("25006");
|
|
642
|
+
expect(await rowCount()).toBe(2);
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
// The least-privilege role from the script-connections docs page.
|
|
646
|
+
test("a SELECT-only role cannot reach server files or programs", async () => {
|
|
647
|
+
await admin.unsafe(`drop owned by ${reader}`).catch(() => undefined);
|
|
648
|
+
await admin.unsafe(`drop role if exists ${reader}`);
|
|
649
|
+
await admin.unsafe(
|
|
650
|
+
`create role ${reader} login password 'reader-probe-pw' nosuperuser nocreatedb nocreaterole noreplication nobypassrls`,
|
|
651
|
+
);
|
|
652
|
+
await admin.unsafe(`alter role ${reader} set default_transaction_read_only = on`);
|
|
653
|
+
await admin.unsafe(`grant usage on schema public to ${reader}`);
|
|
654
|
+
await admin.unsafe(`grant select on ${table} to ${reader}`);
|
|
655
|
+
const url = new URL(dsn);
|
|
656
|
+
url.username = reader;
|
|
657
|
+
url.password = "reader-probe-pw";
|
|
658
|
+
const readerDsn = url.toString();
|
|
659
|
+
|
|
660
|
+
expect(await runReadOnlyQuery(readerDsn, `select count(*)::int as n from ${table}`)).toEqual({
|
|
661
|
+
rows: [{ n: 2 }],
|
|
662
|
+
rowCount: 1,
|
|
663
|
+
truncated: false,
|
|
664
|
+
});
|
|
665
|
+
const denied = async (query: string) => {
|
|
666
|
+
const err = await runReadOnlyQuery(readerDsn, query).then(
|
|
667
|
+
() => null,
|
|
668
|
+
(e: Error) => e,
|
|
669
|
+
);
|
|
670
|
+
return /SQLSTATE (\w+)/.exec(err?.message ?? "")?.[1];
|
|
671
|
+
};
|
|
672
|
+
expect(await denied("select pg_read_file('/etc/hostname')")).toBe("42501");
|
|
673
|
+
expect(await denied("select pg_ls_dir('.')")).toBe("42501");
|
|
674
|
+
});
|
|
675
|
+
});
|
|
@@ -624,6 +624,10 @@ describe("script connections", () => {
|
|
|
624
624
|
markMigrationApplied(database, "206_runtime_pools.sql");
|
|
625
625
|
// 207 rebuilds session_tokens, which this migration-112-only fixture does not create.
|
|
626
626
|
markMigrationApplied(database, "207_runtime_session_tokens.sql");
|
|
627
|
+
// 213 rebuilds script_connections with the post-117 columns this fixture skips.
|
|
628
|
+
markMigrationApplied(database, "213_script_connections_sql.sql");
|
|
629
|
+
// 215 alters swarm_config, which this migration-112-only fixture does not create.
|
|
630
|
+
markMigrationApplied(database, "215_swarm_config_export_to_env.sql");
|
|
627
631
|
|
|
628
632
|
const id = crypto.randomUUID();
|
|
629
633
|
const now = new Date().toISOString();
|
|
@@ -757,7 +761,9 @@ describe("script connections", () => {
|
|
|
757
761
|
new Date().toISOString(),
|
|
758
762
|
createHash("sha256").update(sql117).digest("hex"),
|
|
759
763
|
);
|
|
760
|
-
|
|
764
|
+
// 213 rebuilds script_connections with the post-117 columns; skip it with 117.
|
|
765
|
+
markMigrationApplied(database, "213_script_connections_sql.sql");
|
|
766
|
+
runMigrations(database); // applies every migration except 117 and 213
|
|
761
767
|
|
|
762
768
|
const now = new Date().toISOString();
|
|
763
769
|
// A user-created STANDALONE binding: source='user', arbitrary config_key
|