@desplega.ai/agent-swarm 2.5.4 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/{acp-adapter-ek38cwsv.js → acp-adapter-ma1js5n7.js} +9 -9
- package/dist/{actions-92mepq03.js → actions-1vhmtvg9.js} +21 -21
- package/dist/{amp-adapter-9gaxg015.js → amp-adapter-sxqqvmdk.js} +4 -4
- package/dist/{app-ctn8gsgx.js → app-mwx1d5a9.js} +3 -3
- package/dist/{assistant-zyr5kz47.js → assistant-yx1p6g82.js} +30 -26
- package/dist/{boot-reembed-w75maga2.js → boot-reembed-5h37a2c2.js} +1 -1
- package/dist/{boot-reembed-dwzfttdr.js → boot-reembed-7h78vmvv.js} +2 -1
- package/dist/{boot-scrub-sweep-e9kq43kf.js → boot-scrub-sweep-8mawd4gw.js} +4 -4
- package/dist/{boot-seeds-x28v3fhb.js → boot-seeds-ca84dhnn.js} +4 -4
- package/dist/{claude-adapter-f0afqnne.js → claude-adapter-wqt4hfkq.js} +9 -9
- package/dist/{claude-managed-adapter-x81a7r65.js → claude-managed-adapter-6fjqtr2z.js} +2 -2
- package/dist/{claude-managed-setup-bqzwjd9x.js → claude-managed-setup-rqv98sea.js} +10 -3
- package/dist/{claude-sdk-session-7b7whkaq.js → claude-sdk-session-f29f3b2x.js} +11 -11
- package/dist/{cli-sxgtf5fw.js → cli-07q03xm2.js} +1 -1
- package/dist/{cli-kxpfx2xr.js → cli-0z1xjzwa.js} +2 -2
- package/dist/{cli-g9vd9ece.js → cli-1hhbzkbb.js} +14 -16
- package/dist/{cli-qqzmjxkf.js → cli-1m9x8dr5.js} +11 -6
- package/dist/{cli-jbnn4vxb.js → cli-2pvzmas1.js} +3 -3
- package/dist/{cli-x77gen2p.js → cli-2y7v133v.js} +7 -7
- package/dist/{cli-dew6x255.js → cli-39aet655.js} +2 -2
- package/dist/{cli-g1cw7xtt.js → cli-39rrw8yp.js} +1 -1
- package/dist/{cli-jb30dzvk.js → cli-48gxjmcz.js} +530 -190
- package/dist/{cli-pzatvw94.js → cli-54sj64fc.js} +2 -2
- package/dist/{cli-bvpgr9ny.js → cli-60sty27p.js} +5 -5
- package/dist/{cli-asq5ydm4.js → cli-6a9qfa8b.js} +411 -69
- package/dist/{cli-277c31dw.js → cli-6mgdba42.js} +225 -3
- package/dist/{cli-19202kew.js → cli-6srv653c.js} +5 -5
- package/dist/{cli-n4c8e3s5.js → cli-708wdy32.js} +1 -1
- package/dist/{cli-pasyvjmm.js → cli-737pyqxr.js} +1 -1
- package/dist/{cli-6x9ma5w5.js → cli-803188pb.js} +1 -1
- package/dist/{cli-hh5we352.js → cli-86ehn4s5.js} +7 -7
- package/dist/{cli-9s05422w.js → cli-8mbc9za9.js} +1 -1
- package/dist/{cli-ff1tm9n9.js → cli-8wmj18cz.js} +1 -1
- package/dist/{cli-1r5ad7j5.js → cli-9gsm2rzp.js} +12 -12
- package/dist/{cli-mz9afxxf.js → cli-9pshx6rd.js} +17 -3
- package/dist/{cli-s682zgqz.js → cli-a6ds2epe.js} +1 -1
- package/dist/{cli-6a1xg1p7.js → cli-a89zdqz9.js} +2 -2
- package/dist/{cli-03vpq45j.js → cli-ay40dnj3.js} +2 -2
- package/dist/{cli-tsma7p8v.js → cli-aypd09nf.js} +2 -2
- package/dist/{cli-ethgy7ha.js → cli-b3yzyc57.js} +2 -2
- package/dist/{cli-bn0rewsy.js → cli-c081bj3b.js} +2 -2
- package/dist/{cli-mmhj8vd7.js → cli-c668vvcf.js} +11 -2
- package/dist/{cli-sbwsxvtd.js → cli-d20qvpr2.js} +2 -2
- package/dist/{cli-c952c7ac.js → cli-e1595bgx.js} +3 -3
- package/dist/{cli-621ce7kw.js → cli-f12mr20z.js} +3 -3
- package/dist/{cli-dqygeeh5.js → cli-f1e0ptjj.js} +10 -1
- package/dist/{cli-xzfq4at3.js → cli-f4dbh7a5.js} +16 -12
- package/dist/{cli-ha9rmwgq.js → cli-fb8m699c.js} +2 -2
- package/dist/{cli-5bnsc0e1.js → cli-gsfk58nw.js} +5 -5
- package/dist/cli-gt4h8cxw.js +895 -0
- package/dist/{cli-nwyw3zhg.js → cli-gxee0bqa.js} +1 -1
- package/dist/{cli-25n3mv5d.js → cli-h5jeg46p.js} +6 -6
- package/dist/{cli-gn5af0x5.js → cli-he9gbdx0.js} +62 -12
- package/dist/{cli-gbfxp48k.js → cli-j0ppywdr.js} +42 -35
- package/dist/{cli-k4tjsdx9.js → cli-j3y0x2n4.js} +277 -199
- package/dist/{cli-7p4jxgtt.js → cli-j9bpmykh.js} +75 -27
- package/dist/{cli-dabehxhp.js → cli-kt907da6.js} +1 -1
- package/dist/{cli-wx2nj19m.js → cli-kv7sbc2c.js} +6 -6
- package/dist/{cli-abs8ndtr.js → cli-m30k9777.js} +26 -26
- package/dist/{cli-cz5nmdxt.js → cli-m5e802r7.js} +2 -2
- package/dist/{cli-0shx9j93.js → cli-pfk92eqz.js} +2 -2
- package/dist/{cli-xtrz6bzs.js → cli-ptfq202x.js} +5 -3
- package/dist/{cli-6n8axjv1.js → cli-qj442fws.js} +2 -2
- package/dist/{cli-c61mbdds.js → cli-rd5m70gj.js} +4 -4
- package/dist/{cli-eftzyh9y.js → cli-s09j9vp5.js} +2 -2
- package/dist/{cli-m4ecdxvz.js → cli-s0mgx214.js} +7 -7
- package/dist/{cli-wghjkr0e.js → cli-sgqm2582.js} +10 -7
- package/dist/{cli-2655k16q.js → cli-sjctjr48.js} +2 -2
- package/dist/{cli-nzfnt2gs.js → cli-tf1n0r0b.js} +1 -1
- package/dist/{cli-x0f2v69c.js → cli-twctnkng.js} +12 -7
- package/dist/{cli-j6rktc91.js → cli-v6shg7na.js} +1 -1
- package/dist/{cli-gx4rm0qx.js → cli-vz78s5wm.js} +1 -1
- package/dist/{cli-gwhjmh8g.js → cli-wqr7cea4.js} +5 -5
- package/dist/{cli-e42vss7b.js → cli-x37r7qwf.js} +16 -8
- package/dist/{cli-4b2z6n0m.js → cli-yd2nreqw.js} +17 -8
- package/dist/{cli-64mh9mf3.js → cli-ymjpkxgn.js} +69 -17
- package/dist/{cli-5kqnemen.js → cli-yvy48785.js} +1 -1
- package/dist/{cli-9j129brq.js → cli-yxbsmx56.js} +6 -6
- package/dist/cli.js +22 -20
- package/dist/{codex-adapter-wch12q99.js → codex-adapter-zk59x2zh.js} +7 -7
- package/dist/{codex-hook-brpnycn5.js → codex-hook-xyjhvxwd.js} +1 -1
- package/dist/{codex-login-rn7c5gxt.js → codex-login-8cg0vhf2.js} +1 -1
- package/dist/{codex-session-runner-41pfcr95.js → codex-session-runner-kh6s4j3n.js} +8 -8
- package/dist/{commands-9849dkj6.js → commands-68et1qv2.js} +2 -2
- package/dist/{controller-ja0sd95p.js → controller-fyrbnzg4.js} +4 -4
- package/dist/{controllers-connect-xp05dj6x.js → controllers-connect-nep9h2xv.js} +23 -23
- package/dist/{cursor-adapter-623qxqew.js → cursor-adapter-yss3pg7t.js} +2 -2
- package/dist/{db-w759qmx0.js → db-28gjyvxj.js} +4 -2
- package/dist/{db-47vj40dy.js → db-8ftwvzba.js} +3 -3
- package/dist/{devin-adapter-4ycy4vrf.js → devin-adapter-byen2cxt.js} +1 -1
- package/dist/{dispatcher-7b8e4z2j.js → dispatcher-hayexmkr.js} +20 -20
- package/dist/{docker-s84hjs91.js → docker-fdzyewpz.js} +2 -2
- package/dist/{dsh-adapter-2ezw3xea.js → dsh-adapter-46wawff9.js} +2 -2
- package/dist/{e2b-9gkdhm3c.js → e2b-222hsajc.js} +2 -2
- package/dist/{e2b-cjtf8k1z.js → e2b-fn3zacvt.js} +2 -2
- package/dist/{easter-egg-3d-e4qv8dxj.js → easter-egg-3d-8d1e0h4m.js} +1 -1
- package/dist/{event-bus-3z78ep5n.js → event-bus-pghg8hqy.js} +1 -1
- package/dist/{execute-0xpz8950.js → execute-cjrfxfnm.js} +1 -1
- package/dist/{external-channel-e2s028da.js → external-channel-xbmxcg09.js} +2 -2
- package/dist/{grok-adapter-ys7gaqee.js → grok-adapter-ag4wks2a.js} +10 -10
- package/dist/handlers-tzmevqb5.js +41 -0
- package/dist/{heartbeat-w3mntghj.js → heartbeat-psfvcgwp.js} +23 -23
- package/dist/{hook-g7kvv6x9.js → hook-8pz04697.js} +6 -6
- package/dist/{hook-3jjywnpb.js → hook-cyq4mryn.js} +8 -8
- package/dist/{http-tkp7d4kb.js → http-haj1nd6k.js} +921 -169
- package/dist/{index-q7m26kvs.js → index-jwt2bj6f.js} +265 -97
- package/dist/{index-3n8m9ea2.js → index-q52f47pe.js} +1 -1
- package/dist/{index-31ceyf42.js → index-syvhnvtw.js} +1 -1
- package/dist/index-y1ajx04a.js +43 -0
- package/dist/{index-1ef1myq2.js → index-z5h22pes.js} +20 -20
- package/dist/{index-xdaahthb.js → index-zyq8ct52.js} +20 -20
- package/dist/{keepalive-v4ta8wgg.js → keepalive-xdvsamvj.js} +4 -4
- package/dist/lead-hqg57kdp.js +79 -0
- package/dist/{maintenance-84n1ex21.js → maintenance-4y2xzh3a.js} +1 -1
- package/dist/{oauth-refresh-sweep-vjnnandw.js → oauth-refresh-sweep-jd4r7zbs.js} +9 -4
- package/dist/{onboard-sfmpe4np.js → onboard-18447q9q.js} +6 -6
- package/dist/{opencode-adapter-jmpst4q4.js → opencode-adapter-26b2frdf.js} +7 -7
- package/dist/{otel-impl-v6cc0zve.js → otel-impl-f9tgr8yw.js} +2 -2
- package/dist/{pi-mono-adapter-fk8fkcf6.js → pi-mono-adapter-qn6qbga1.js} +38 -17
- package/dist/{pricing-refresh-fsk1e1zc.js → pricing-refresh-dw2tgb53.js} +3 -3
- package/dist/{rbac-roles-prmkdnkw.js → rbac-roles-aygm2760.js} +2 -2
- package/dist/{rbac-roles-zxmxqa39.js → rbac-roles-s4nwj2m3.js} +1 -1
- package/dist/render-v2-yfvdfr0x.js +16 -0
- package/dist/{resume-frdtgfa0.js → resume-1bmvjbf3.js} +9 -9
- package/dist/{runtime-bt5pkpm1.js → runtime-f7tpzedy.js} +1 -1
- package/dist/{session-access-fpez4y5q.js → session-access-r5garej6.js} +4 -4
- package/dist/session-source-backfill-424wzv0r.js +110 -0
- package/dist/{setup-mq6m3m8q.js → setup-8tygvxkq.js} +1 -1
- package/dist/{virtual-modules-mnfbh5mr.js → virtual-modules-xsw323b3.js} +2 -2
- package/dist/{work-objects-kfahhjsq.js → work-objects-ggnghpgf.js} +23 -23
- package/dist/worker-sqjd1nqf.js +79 -0
- package/dist/{x-pbpezr0s.js → x-wxrzvzx8.js} +2 -2
- package/openapi.json +1324 -14
- package/package.json +5 -3
- package/src/assets/source-key.ts +95 -0
- package/src/be/automation-preflight.ts +2 -0
- package/src/be/db/api-keys.ts +89 -0
- package/src/be/db/session-labels.ts +28 -0
- package/src/be/db/session-moves.ts +5 -6
- package/src/be/db/session-tree.ts +153 -14
- package/src/be/db/tasks/read.ts +11 -0
- package/src/be/db/tasks/write.ts +6 -1
- package/src/be/db/workflow-runs.ts +219 -6
- package/src/be/db-retention.ts +61 -6
- package/src/be/db.ts +135 -132
- package/src/be/memory/providers/openai-embedding.ts +5 -1
- package/src/be/memory/task-learning.ts +155 -0
- package/src/be/migrations/244_api_key_validation.sql +4 -0
- package/src/be/migrations/245_script_executor.sql +3 -0
- package/src/be/oauth-refresh-sweep.ts +2 -1
- package/src/be/onboarding.ts +4 -0
- package/src/be/scripts/boot-reembed.ts +2 -0
- package/src/be/scripts/db.ts +14 -4
- package/src/be/scripts/embeddings.ts +2 -0
- package/src/be/scripts/run-saved.ts +1 -0
- package/src/be/scripts/typecheck.ts +10 -4
- package/src/be/seed-skills/index.ts +7 -0
- package/src/be/session-source-backfill.ts +163 -0
- package/src/be/swarm-config-guard.ts +1 -0
- package/src/comb/review-batch.ts +28 -1
- package/src/comb/templates.ts +7 -2
- package/src/commands/claude-managed-setup.ts +7 -1
- package/src/commands/credential-refresh.ts +76 -1
- package/src/commands/credential-wait.ts +2 -2
- package/src/commands/onboard/compose-generator.ts +5 -3
- package/src/commands/provider-credentials.ts +212 -171
- package/src/commands/runner.ts +25 -4
- package/src/extensions/apply-task-create.ts +8 -1
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +5 -0
- package/src/heartbeat/heartbeat.ts +14 -1
- package/src/heartbeat/script-runner-probe.ts +47 -0
- package/src/http/all-routes.ts +2 -0
- package/src/http/api-keys.ts +77 -9
- package/src/http/assets.ts +15 -0
- package/src/http/codex-oauth-accounts.ts +191 -0
- package/src/http/comb.ts +15 -2
- package/src/http/config.ts +1 -1
- package/src/http/core.ts +6 -0
- package/src/http/fleet-benchmark.ts +5 -2
- package/src/http/idempotency-key.ts +1 -1
- package/src/http/index.ts +14 -3
- package/src/http/integrations.ts +5 -1
- package/src/http/kv.ts +8 -3
- package/src/http/onboarding.ts +6 -1
- package/src/http/pages-public.ts +38 -5
- package/src/http/pool-keys.ts +354 -0
- package/src/http/scripts.ts +27 -0
- package/src/http/sessions.ts +37 -12
- package/src/http/stats.ts +1 -0
- package/src/http/status.ts +26 -12
- package/src/http/tasks.ts +3 -0
- package/src/http/used-by.ts +7 -2
- package/src/http/workflows.ts +88 -9
- package/src/http/x.ts +5 -0
- package/src/integrations/chat-sdk/inbound.ts +27 -0
- package/src/kv-reserved-namespaces.ts +19 -0
- package/src/prompts/base-prompt.ts +21 -1
- package/src/prompts/session-templates.ts +17 -3
- package/src/providers/acp-targets.ts +1 -1
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/claude-adapter.ts +5 -1
- package/src/providers/claude-managed-adapter.ts +16 -9
- package/src/providers/codex-adapter.ts +71 -11
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/providers/pi-mono-adapter.ts +41 -11
- package/src/scheduler/scheduler.ts +5 -3
- package/src/scripts-runtime/eval-harness.ts +8 -1
- package/src/scripts-runtime/executors/native.ts +22 -16
- package/src/scripts-runtime/executors/quickjs-prelude.ts +103 -1
- package/src/scripts-runtime/executors/quickjs-runner.ts +2 -1
- package/src/scripts-runtime/executors/quickjs.ts +7 -0
- package/src/scripts-runtime/executors/registry.ts +32 -0
- package/src/scripts-runtime/executors/types.ts +7 -0
- package/src/scripts-runtime/host-validated-args.ts +133 -0
- package/src/scripts-runtime/loader.ts +30 -3
- package/src/scripts-runtime/swarm-sdk.ts +16 -3
- package/src/scripts-runtime/types/stdlib.d.ts +10 -2
- package/src/scripts-runtime/types/swarm-extension.d.ts +17 -8
- package/src/scripts-runtime/types/swarm-sdk.d.ts +10 -2
- package/src/slack/assistant.ts +7 -0
- package/src/slack/channel-labels.ts +74 -0
- package/src/slack/handlers.ts +8 -0
- package/src/slack/inbound-files.ts +5 -2
- package/src/slack/thread-buffer.ts +4 -0
- package/src/tasks/sibling-awareness.ts +12 -3
- package/src/telemetry-context.ts +5 -6
- package/src/telemetry-identity.ts +3 -3
- package/src/tests/api-key-status-by-suffix.test.ts +242 -0
- package/src/tests/api-keys-validation.test.ts +170 -0
- package/src/tests/base-prompt.test.ts +70 -2
- package/src/tests/claude-managed-adapter.test.ts +46 -0
- package/src/tests/claude-routes-e2e.test.ts +7 -2
- package/src/tests/codex-credential-pools.test.ts +120 -0
- package/src/tests/codex-gateway-route.test.ts +165 -0
- package/src/tests/codex-oauth-accounts.test.ts +357 -0
- package/src/tests/comb-review-batch.test.ts +63 -2
- package/src/tests/community-linear-delivery-loop.test.ts +2099 -0
- package/src/tests/config-pool-keys.test.ts +533 -0
- package/src/tests/credential-check.test.ts +294 -4
- package/src/tests/credential-refresh.test.ts +172 -1
- package/src/tests/db-retention.test.ts +9 -3
- package/src/tests/db-task-read-contract.test.ts +1 -0
- package/src/tests/db-task-write-contract.test.ts +1 -3
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/docs-index.test.ts +135 -0
- package/src/tests/dsh-adapter.test.ts +1 -1
- package/src/tests/extensions-pre-task.test.ts +53 -1
- package/src/tests/fetch-resolved-env.test.ts +16 -2
- package/src/tests/fixtures/extensions/record-task-create/hooks.ts +9 -0
- package/src/tests/fixtures/extensions/record-task-create/manifest.json +7 -0
- package/src/tests/fleet-benchmark.test.ts +8 -0
- package/src/tests/get-workflow-run-tool.test.ts +98 -0
- package/src/tests/heartbeat-checklist.test.ts +89 -0
- package/src/tests/heartbeat-script-runner-probe.test.ts +83 -0
- package/src/tests/integrations-http.test.ts +32 -2
- package/src/tests/internal-ai/complete-structured.test.ts +37 -1
- package/src/tests/internal-ai/credentials.test.ts +23 -0
- package/src/tests/kv-http.test.ts +32 -0
- package/src/tests/kv-tool.test.ts +25 -0
- package/src/tests/multi-runtime-registration.test.ts +12 -12
- package/src/tests/oauth-refresh-sweep.test.ts +34 -1
- package/src/tests/onboard-compose.test.ts +20 -0
- package/src/tests/onboarding.test.ts +108 -1
- package/src/tests/openai-embedding-credentials.test.ts +35 -0
- package/src/tests/opencode-adapter.test.ts +2 -1
- package/src/tests/openrouter-attribution-call-sites.test.ts +2 -2
- package/src/tests/org-website.test.ts +36 -0
- package/src/tests/pages-frame-ancestors.test.ts +162 -0
- package/src/tests/pi-codemode.test.ts +2 -1
- package/src/tests/pi-mono-adapter.test.ts +54 -0
- package/src/tests/pi-native-mcp.test.ts +13 -1
- package/src/tests/pi-tool-exposure.test.ts +8 -8
- package/src/tests/probe-tool-schema-tokens.test.ts +144 -0
- package/src/tests/prompt-template-session.test.ts +4 -3
- package/src/tests/provider-key-probes.test.ts +181 -0
- package/src/tests/rbac-e2e-helpers.test.ts +88 -1
- package/src/tests/rbac-e2e-helpers.ts +36 -11
- package/src/tests/request-composition-derive.test.ts +329 -0
- package/src/tests/script-connections.test.ts +4 -0
- package/src/tests/script-executor-conformance.test.ts +36 -1
- package/src/tests/script-executor-quickjs-pool.test.ts +50 -0
- package/src/tests/script-executor-quickjs.test.ts +140 -0
- package/src/tests/script-host-validated-args-native.test.ts +196 -0
- package/src/tests/script-per-script-executor.test.ts +415 -0
- package/src/tests/script-runs-retention.test.ts +212 -0
- package/src/tests/scripts-external-api.test.ts +45 -0
- package/src/tests/sdk-allowlist.test.ts +42 -2
- package/src/tests/self-improvement.test.ts +4 -4
- package/src/tests/session-source-backfill.test.ts +429 -0
- package/src/tests/session-source-keys.test.ts +236 -0
- package/src/tests/session-source-labels.test.ts +419 -0
- package/src/tests/sessions-detail-batch.test.ts +2 -0
- package/src/tests/sessions-move.test.ts +131 -0
- package/src/tests/sessions-tree.test.ts +299 -2
- package/src/tests/slack-inbound-files.test.ts +103 -23
- package/src/tests/status.test.ts +101 -18
- package/src/tests/store-progress-learning.test.ts +288 -0
- package/src/tests/tasks-workflow-run-filter.test.ts +146 -0
- package/src/tests/test-net.ts +31 -0
- package/src/tests/transcription.test.ts +8 -0
- package/src/tests/ui-logs-parser.test.ts +180 -0
- package/src/tests/used-by-route.test.ts +11 -0
- package/src/tests/workflow-cascade-cancel.test.ts +172 -0
- package/src/tests/workflow-engine-v2.test.ts +116 -1
- package/src/tests/workflow-http-v2.test.ts +131 -0
- package/src/tests/workflow-mcp-trigger-schema.test.ts +209 -0
- package/src/tests/workflow-run-finished-event.test.ts +263 -0
- package/src/tests/workflow-step-order.test.ts +92 -0
- package/src/tests/workflow-sub-workflow.test.ts +30 -0
- package/src/tests/workflow-swarm-script.test.ts +34 -0
- package/src/tools/get-tasks.ts +8 -0
- package/src/tools/kv/kv-read-auth.ts +4 -2
- package/src/tools/script-common.ts +1 -0
- package/src/tools/script-run.ts +7 -0
- package/src/tools/script-upsert.ts +7 -0
- package/src/tools/store-progress.ts +67 -1
- package/src/tools/task-action.ts +1 -1
- package/src/tools/workflows/get-workflow-run.ts +14 -4
- package/src/tools/workflows/trigger-workflow.ts +104 -10
- package/src/types.ts +36 -1
- package/src/utils/company-profile.ts +51 -1
- package/src/utils/credentials.ts +10 -4
- package/src/utils/internal-ai/complete-structured.ts +5 -0
- package/src/utils/internal-ai/credentials.ts +4 -3
- package/src/utils/provider-key-probes.ts +328 -0
- package/src/utils/transcription.ts +4 -1
- package/src/workflows/engine.ts +18 -16
- package/src/workflows/executors/sub-workflow.ts +8 -1
- package/src/workflows/executors/swarm-script.ts +5 -0
- package/src/workflows/foreach-driver.ts +5 -11
- package/src/workflows/node-timeout.ts +30 -0
- package/templates/ai-toolbox.manifest.json +2 -2
- package/templates/community/linear-delivery-loop/PLAYBOOK.md +323 -0
- package/templates/community/linear-delivery-loop/README.md +38 -0
- package/templates/community/linear-delivery-loop/config.example.json +31 -0
- package/templates/community/linear-delivery-loop/lead-prompt.md +34 -0
- package/templates/community/linear-delivery-loop/schedule.json +10 -0
- package/templates/community/linear-delivery-loop/scripts/linear-delivery-setup.ts +148 -0
- package/templates/community/linear-delivery-loop/scripts/linear-delivery-tick.ts +1781 -0
- package/templates/skills/researching/content.md +2 -0
- package/templates/skills/swarm-docs/config.json +14 -0
- package/templates/skills/swarm-docs/content.md +144 -0
- package/templates/skills/swarm-scripts/SKILL.md +1 -0
- package/templates/skills/swarm-scripts/content.md +1 -0
- package/dist/cli-7421jzsw.js +0 -84
- package/dist/cli-e45e4m6b.js +0 -413
- package/dist/cli-zw88zjba.js +0 -307
- package/dist/handlers-wy1dr6ja.js +0 -41
- package/dist/index-nmm79qgr.js +0 -43
- package/dist/lead-sawnyqxk.js +0 -79
- package/dist/render-v2-pjwxvnkw.js +0 -12
- package/dist/worker-v5e5rxj3.js +0 -79
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
import {
|
|
2
|
+
afterAll,
|
|
3
|
+
afterEach,
|
|
4
|
+
beforeAll,
|
|
5
|
+
beforeEach,
|
|
6
|
+
describe,
|
|
7
|
+
expect,
|
|
8
|
+
spyOn,
|
|
9
|
+
test,
|
|
10
|
+
} from "bun:test";
|
|
11
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
12
|
+
import { Readable } from "node:stream";
|
|
13
|
+
import {
|
|
14
|
+
closeDb,
|
|
15
|
+
createUser,
|
|
16
|
+
getAvailableKeyIndices,
|
|
17
|
+
getDbClient,
|
|
18
|
+
getSwarmConfigs,
|
|
19
|
+
initDb,
|
|
20
|
+
recordKeyValidation,
|
|
21
|
+
upsertSwarmConfig,
|
|
22
|
+
} from "../be/db";
|
|
23
|
+
import { handleConfig } from "../http/config";
|
|
24
|
+
import {
|
|
25
|
+
__resetInjectedEnvTracking,
|
|
26
|
+
flushPendingIntegrationsReload,
|
|
27
|
+
loadGlobalConfigsIntoEnv,
|
|
28
|
+
} from "../http/core";
|
|
29
|
+
import { handlePoolKeys, type PoolKeysResponse } from "../http/pool-keys";
|
|
30
|
+
import { getPathSegments, parseQueryParams } from "../http/utils";
|
|
31
|
+
import { CREDENTIAL_POOL_VARS } from "../utils/credentials";
|
|
32
|
+
import { type HttpRequestAuth, setRequestAuth } from "../utils/request-auth-context";
|
|
33
|
+
|
|
34
|
+
const API_KEY = "example-pool-test-operator";
|
|
35
|
+
const KEY = "ANTHROPIC_API_KEY";
|
|
36
|
+
const first = "example-provider-key-first";
|
|
37
|
+
const second = "example-provider-key-secnd";
|
|
38
|
+
const envKeys = [...CREDENTIAL_POOL_VARS, "OPENROUTER_BASE_URL", "ANTHROPIC_BASE_URL"];
|
|
39
|
+
const originalEnv = Object.fromEntries(envKeys.map((key) => [key, process.env[key]]));
|
|
40
|
+
let providerCalls: Array<{ url: string; init?: RequestInit }>;
|
|
41
|
+
let providerFetch: (input: string, init?: RequestInit) => Promise<Response>;
|
|
42
|
+
let fetchSpy: ReturnType<typeof spyOn<typeof globalThis, "fetch">>;
|
|
43
|
+
|
|
44
|
+
type Principal = boolean | HttpRequestAuth;
|
|
45
|
+
|
|
46
|
+
async function call(
|
|
47
|
+
path: string,
|
|
48
|
+
method: string,
|
|
49
|
+
body?: unknown,
|
|
50
|
+
guest: Principal = false,
|
|
51
|
+
): Promise<Response> {
|
|
52
|
+
const req = Readable.from(
|
|
53
|
+
body === undefined ? [] : [Buffer.from(JSON.stringify(body))],
|
|
54
|
+
) as IncomingMessage;
|
|
55
|
+
Object.assign(req, { method, url: path, headers: { "content-type": "application/json" } });
|
|
56
|
+
setRequestAuth(
|
|
57
|
+
req,
|
|
58
|
+
typeof guest === "object"
|
|
59
|
+
? guest
|
|
60
|
+
: guest
|
|
61
|
+
? { kind: "guest" }
|
|
62
|
+
: { kind: "operator", fingerprint: API_KEY },
|
|
63
|
+
);
|
|
64
|
+
let status = 200;
|
|
65
|
+
let text = "";
|
|
66
|
+
const res = {
|
|
67
|
+
setHeader() {},
|
|
68
|
+
writeHead(code: number) {
|
|
69
|
+
status = code;
|
|
70
|
+
},
|
|
71
|
+
end(chunk?: string) {
|
|
72
|
+
text += chunk ?? "";
|
|
73
|
+
},
|
|
74
|
+
} as unknown as ServerResponse;
|
|
75
|
+
const segments = getPathSegments(path);
|
|
76
|
+
const query = parseQueryParams(path);
|
|
77
|
+
const handled =
|
|
78
|
+
(await handlePoolKeys(req, res, segments, query)) ||
|
|
79
|
+
(await handleConfig(req, res, segments, query));
|
|
80
|
+
expect(handled).toBe(true);
|
|
81
|
+
return new Response(text, { status });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function request(
|
|
85
|
+
method = "GET",
|
|
86
|
+
query = `?key=${KEY}`,
|
|
87
|
+
body?: unknown,
|
|
88
|
+
guest: Principal = false,
|
|
89
|
+
) {
|
|
90
|
+
const response = await call(`/api/config/pool-keys${query}`, method, body, guest);
|
|
91
|
+
const text = await response.text();
|
|
92
|
+
expect(text).not.toContain(first);
|
|
93
|
+
expect(text).not.toContain(second);
|
|
94
|
+
return { status: response.status, body: JSON.parse(text) };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function store(value = first) {
|
|
98
|
+
return await upsertSwarmConfig({
|
|
99
|
+
scope: "global",
|
|
100
|
+
key: KEY,
|
|
101
|
+
value,
|
|
102
|
+
isSecret: true,
|
|
103
|
+
description: "Keep this description",
|
|
104
|
+
exportToEnv: false,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function pool(): Promise<PoolKeysResponse> {
|
|
109
|
+
return (await request()).body;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
beforeAll(() => {
|
|
113
|
+
initDb(":memory:");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
beforeEach(async () => {
|
|
117
|
+
await getDbClient().run("DELETE FROM swarm_config");
|
|
118
|
+
await getDbClient().run("DELETE FROM api_key_status");
|
|
119
|
+
for (const key of envKeys) delete process.env[key];
|
|
120
|
+
__resetInjectedEnvTracking();
|
|
121
|
+
providerCalls = [];
|
|
122
|
+
providerFetch = async () => new Response("{}", { status: 200 });
|
|
123
|
+
fetchSpy = spyOn(globalThis, "fetch").mockImplementation((async (input, init) => {
|
|
124
|
+
const url = String(input);
|
|
125
|
+
providerCalls.push({ url, init });
|
|
126
|
+
return providerFetch(url, init);
|
|
127
|
+
}) as typeof fetch);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
afterEach(async () => {
|
|
131
|
+
await flushPendingIntegrationsReload();
|
|
132
|
+
fetchSpy.mockRestore();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
afterAll(() => {
|
|
136
|
+
closeDb();
|
|
137
|
+
__resetInjectedEnvTracking();
|
|
138
|
+
for (const [key, value] of Object.entries(originalEnv)) {
|
|
139
|
+
if (value === undefined) delete process.env[key];
|
|
140
|
+
else process.env[key] = value;
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("credential pool reads", () => {
|
|
145
|
+
test("lists suffixes with all four statuses and rejection precedence", async () => {
|
|
146
|
+
const limited = "example-provider-key-limit";
|
|
147
|
+
const unchecked = "example-provider-key-newww";
|
|
148
|
+
await store([first, second, limited, unchecked].join(","));
|
|
149
|
+
await recordKeyValidation(KEY, [
|
|
150
|
+
{ keyIndex: 0, keySuffix: first.slice(-5), ok: true },
|
|
151
|
+
{ keyIndex: 1, keySuffix: second.slice(-5), ok: false, httpStatus: 401 },
|
|
152
|
+
{ keyIndex: 2, keySuffix: limited.slice(-5), ok: true },
|
|
153
|
+
]);
|
|
154
|
+
const until = new Date(Date.now() + 60_000).toISOString();
|
|
155
|
+
await getDbClient().run(
|
|
156
|
+
"UPDATE api_key_status SET status = 'rate_limited', rateLimitedUntil = ? WHERE keySuffix IN (?, ?)",
|
|
157
|
+
[until, second.slice(-5), limited.slice(-5)],
|
|
158
|
+
);
|
|
159
|
+
const result = await pool();
|
|
160
|
+
expect(result).toMatchObject({ key: KEY, source: "stored", readOnly: false });
|
|
161
|
+
expect(result.keys.map((key) => key.status)).toEqual([
|
|
162
|
+
"ok",
|
|
163
|
+
"rejected",
|
|
164
|
+
"rate_limited",
|
|
165
|
+
"unchecked",
|
|
166
|
+
]);
|
|
167
|
+
expect(result.keys[1]).toMatchObject({ error: "HTTP 401", rateLimitedUntil: until });
|
|
168
|
+
expect(result.keys[1]!.checkedAt).not.toBeNull();
|
|
169
|
+
expect(result.keys[3]).toMatchObject({ error: null, checkedAt: null, rateLimitedUntil: null });
|
|
170
|
+
expect(JSON.stringify(result)).not.toContain(limited);
|
|
171
|
+
expect(JSON.stringify(result)).not.toContain(unchecked);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("env-only values are read-only and stored values take precedence", async () => {
|
|
175
|
+
process.env[KEY] = first;
|
|
176
|
+
expect(await pool()).toMatchObject({
|
|
177
|
+
source: "env",
|
|
178
|
+
readOnly: true,
|
|
179
|
+
keys: [{ suffix: "first" }],
|
|
180
|
+
});
|
|
181
|
+
await store(second);
|
|
182
|
+
expect(await pool()).toMatchObject({
|
|
183
|
+
source: "stored",
|
|
184
|
+
readOnly: false,
|
|
185
|
+
keys: [{ suffix: "secnd" }],
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("an absent value has no keys", async () => {
|
|
190
|
+
expect(await pool()).toEqual({ key: KEY, source: "none", readOnly: false, keys: [] });
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test.each(["NOT_A_POOL", "CODEX_OAUTH"])("rejects variable %s", async (key) => {
|
|
194
|
+
expect((await request("GET", `?key=${key}`)).status).toBe(400);
|
|
195
|
+
expect((await request("POST", "", { key, value: first })).status).toBe(400);
|
|
196
|
+
expect((await request("DELETE", `?key=${key}&index=0&suffix=first`)).status).toBe(400);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("a caller without secret read permission receives 403", async () => {
|
|
200
|
+
await store();
|
|
201
|
+
expect((await request("GET", `?key=${KEY}`, undefined, true)).status).toBe(403);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("stored validation errors cannot disclose a key", async () => {
|
|
205
|
+
await store();
|
|
206
|
+
await recordKeyValidation(KEY, [
|
|
207
|
+
{ keyIndex: 0, keySuffix: "first", ok: false, httpStatus: 401 },
|
|
208
|
+
]);
|
|
209
|
+
await getDbClient().run("UPDATE api_key_status SET lastValidationError = ?", [
|
|
210
|
+
`Rejected ${first}`,
|
|
211
|
+
]);
|
|
212
|
+
expect((await pool()).keys[0]!.error).toContain("[REDACTED");
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe("credential pool additions", () => {
|
|
217
|
+
test("tests one key, appends it, records a pass, and preserves config metadata", async () => {
|
|
218
|
+
await store();
|
|
219
|
+
await recordKeyValidation(KEY, [
|
|
220
|
+
{ keyIndex: 0, keySuffix: "first", ok: false, httpStatus: 401 },
|
|
221
|
+
]);
|
|
222
|
+
const result = await request("POST", "", { key: KEY, value: second });
|
|
223
|
+
expect(result.status).toBe(200);
|
|
224
|
+
expect(result.body.checks).toEqual([
|
|
225
|
+
{ suffix: "secnd", result: "verified", httpStatus: null, error: null },
|
|
226
|
+
]);
|
|
227
|
+
expect(result.body.keys.map((key: { status: string }) => key.status)).toEqual([
|
|
228
|
+
"rejected",
|
|
229
|
+
"ok",
|
|
230
|
+
]);
|
|
231
|
+
expect((await pool()).keys[1]!.status).toBe("ok");
|
|
232
|
+
expect(providerCalls).toHaveLength(1);
|
|
233
|
+
expect((await getSwarmConfigs({ scope: "global", key: KEY }))[0]).toMatchObject({
|
|
234
|
+
value: `${first},${second}`,
|
|
235
|
+
isSecret: true,
|
|
236
|
+
description: "Keep this description",
|
|
237
|
+
exportToEnv: false,
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("adds two comma-separated keys after trimming and dropping empty parts", async () => {
|
|
242
|
+
const result = await request("POST", "", { key: KEY, value: ` , ${first}, ${second}, ` });
|
|
243
|
+
expect(result.status).toBe(200);
|
|
244
|
+
expect(result.body.keys.map((key: { suffix: string }) => key.suffix)).toEqual([
|
|
245
|
+
"first",
|
|
246
|
+
"secnd",
|
|
247
|
+
]);
|
|
248
|
+
expect(providerCalls).toHaveLength(2);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test.each(["", " , , "])("rejects empty input %s", async (value) => {
|
|
252
|
+
expect(await request("POST", "", { key: KEY, value })).toMatchObject({
|
|
253
|
+
status: 400,
|
|
254
|
+
body: { error: "Enter a key." },
|
|
255
|
+
});
|
|
256
|
+
expect(providerCalls).toHaveLength(0);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test.each([
|
|
260
|
+
"inner space",
|
|
261
|
+
"inner\ttab",
|
|
262
|
+
"inner\nline",
|
|
263
|
+
])("rejects whitespace inside %s", async (value) => {
|
|
264
|
+
expect(await request("POST", "", { key: KEY, value })).toMatchObject({
|
|
265
|
+
status: 400,
|
|
266
|
+
body: { error: "A key cannot contain spaces or line breaks." },
|
|
267
|
+
});
|
|
268
|
+
expect(providerCalls).toHaveLength(0);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("rejects a duplicate already stored", async () => {
|
|
272
|
+
await store();
|
|
273
|
+
expect(await request("POST", "", { key: KEY, value: first })).toMatchObject({
|
|
274
|
+
status: 409,
|
|
275
|
+
body: { error: "The key ending first is already in the pool." },
|
|
276
|
+
});
|
|
277
|
+
expect(providerCalls).toHaveLength(0);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("rejects a duplicate inside the input", async () => {
|
|
281
|
+
expect((await request("POST", "", { key: KEY, value: `${first},${first}` })).status).toBe(409);
|
|
282
|
+
expect(providerCalls).toHaveLength(0);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("a 401 rejects the entire batch and scrubs provider errors", async () => {
|
|
286
|
+
providerFetch = async (_url, init) =>
|
|
287
|
+
new Response(`Rejected ${new Headers(init?.headers).get("x-api-key")}`, { status: 401 });
|
|
288
|
+
const result = await request("POST", "", { key: KEY, value: `${first},${second}` });
|
|
289
|
+
expect(result.status).toBe(422);
|
|
290
|
+
expect(result.body.error).toBe(
|
|
291
|
+
"The key ending first was rejected (HTTP 401). Nothing was saved.",
|
|
292
|
+
);
|
|
293
|
+
expect(result.body.checks[0]).toMatchObject({ result: "rejected", httpStatus: 401 });
|
|
294
|
+
expect(await getSwarmConfigs({ scope: "global", key: KEY })).toEqual([]);
|
|
295
|
+
expect(await getDbClient().query("SELECT * FROM api_key_status")).toEqual([]);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("a provider error cannot expose a long key prefix", async () => {
|
|
299
|
+
const value = "example-long-provider-key-".repeat(12);
|
|
300
|
+
providerFetch = async () => new Response(`Rejected ${value}`, { status: 401 });
|
|
301
|
+
const result = await request("POST", "", { key: KEY, value });
|
|
302
|
+
expect(result.status).toBe(422);
|
|
303
|
+
expect(JSON.stringify(result.body)).not.toContain(value.slice(0, 40));
|
|
304
|
+
expect(result.body.checks[0].error).toContain("[REDACTED");
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test.each([403, 429, 500, 503])("saves after HTTP %s with not_tested", async (status) => {
|
|
308
|
+
providerFetch = async () => new Response("Unavailable", { status });
|
|
309
|
+
const result = await request("POST", "", { key: KEY, value: first });
|
|
310
|
+
expect(result.status).toBe(200);
|
|
311
|
+
expect(result.body.checks[0]).toMatchObject({ result: "not_tested", httpStatus: status });
|
|
312
|
+
expect(result.body.keys[0].status).toBe("unchecked");
|
|
313
|
+
expect((await getSwarmConfigs({ scope: "global", key: KEY }))[0]!.value).toBe(first);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test("saves after a timeout with not_tested", async () => {
|
|
317
|
+
providerFetch = async () => {
|
|
318
|
+
throw new DOMException("Timed out", "TimeoutError");
|
|
319
|
+
};
|
|
320
|
+
const result = await request("POST", "", { key: KEY, value: first });
|
|
321
|
+
expect(result.status).toBe(200);
|
|
322
|
+
expect(result.body.checks[0]).toMatchObject({ result: "not_tested", httpStatus: null });
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test.each([
|
|
326
|
+
"DEVIN_API_KEY",
|
|
327
|
+
"AMP_API_KEY",
|
|
328
|
+
"CURSOR_API_KEY",
|
|
329
|
+
])("does not probe %s", async (key) => {
|
|
330
|
+
const result = await request("POST", "", { key, value: first });
|
|
331
|
+
expect(result.status).toBe(200);
|
|
332
|
+
expect(result.body.checks[0].result).toBe("not_tested");
|
|
333
|
+
expect(providerCalls).toHaveLength(0);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test("env pools cannot be changed", async () => {
|
|
337
|
+
process.env[KEY] = first;
|
|
338
|
+
for (const method of ["POST", "DELETE"]) {
|
|
339
|
+
const result = await request(method, `?key=${KEY}&index=0&suffix=first`, {
|
|
340
|
+
key: KEY,
|
|
341
|
+
value: second,
|
|
342
|
+
});
|
|
343
|
+
expect(result).toMatchObject({
|
|
344
|
+
status: 409,
|
|
345
|
+
body: { error: "These keys come from the deployment env. Change them there." },
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
expect(providerCalls).toHaveLength(0);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test("guests cannot add or remove keys", async () => {
|
|
352
|
+
await store();
|
|
353
|
+
expect((await request("POST", "", { key: KEY, value: second }, true)).status).toBe(403);
|
|
354
|
+
expect(
|
|
355
|
+
(await request("DELETE", `?key=${KEY}&index=0&suffix=first`, undefined, true)).status,
|
|
356
|
+
).toBe(403);
|
|
357
|
+
expect(providerCalls).toHaveLength(0);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test("a config admin who cannot read secrets cannot add or remove keys", async () => {
|
|
361
|
+
await store();
|
|
362
|
+
const user = await createUser({ name: "Config writer without secret read" });
|
|
363
|
+
// A new user starts as admin. Replace that with a role that grants only config writes.
|
|
364
|
+
await getDbClient().run("DELETE FROM principal_roles WHERE principalId = ?", [user.id]);
|
|
365
|
+
await getDbClient().run("INSERT INTO roles (id, name) VALUES ('pool-writer', 'pool-writer')");
|
|
366
|
+
for (const verb of ["config.write.any", "config.delete.any"]) {
|
|
367
|
+
await getDbClient().run(
|
|
368
|
+
"INSERT INTO role_permissions (roleId, verb) VALUES ('pool-writer', ?)",
|
|
369
|
+
[verb],
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
await getDbClient().run(
|
|
373
|
+
"INSERT INTO principal_roles (principalType, principalId, roleId) VALUES ('user', ?, 'pool-writer')",
|
|
374
|
+
[user.id],
|
|
375
|
+
);
|
|
376
|
+
const writer: HttpRequestAuth = { kind: "user", userId: user.id, user };
|
|
377
|
+
const previous = process.env.RBAC_ENABLED;
|
|
378
|
+
process.env.RBAC_ENABLED = "true";
|
|
379
|
+
try {
|
|
380
|
+
expect((await request("POST", "", { key: KEY, value: second }, writer)).status).toBe(403);
|
|
381
|
+
expect(
|
|
382
|
+
(await request("DELETE", `?key=${KEY}&index=0&suffix=first`, undefined, writer)).status,
|
|
383
|
+
).toBe(403);
|
|
384
|
+
} finally {
|
|
385
|
+
if (previous === undefined) delete process.env.RBAC_ENABLED;
|
|
386
|
+
else process.env.RBAC_ENABLED = previous;
|
|
387
|
+
}
|
|
388
|
+
expect(providerCalls).toHaveLength(0);
|
|
389
|
+
expect((await getSwarmConfigs({ scope: "global", key: KEY }))[0]?.value).toBe(first);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test("rechecks duplicates after a probe without holding the write transaction", async () => {
|
|
393
|
+
providerFetch = async () => {
|
|
394
|
+
await store(first);
|
|
395
|
+
return new Response("{}", { status: 200 });
|
|
396
|
+
};
|
|
397
|
+
expect((await request("POST", "", { key: KEY, value: first })).status).toBe(409);
|
|
398
|
+
expect((await getSwarmConfigs({ scope: "global", key: KEY }))[0]!.value).toBe(first);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test("an unrelated unreadable secret does not prevent adding a pool key", async () => {
|
|
402
|
+
await upsertSwarmConfig({
|
|
403
|
+
scope: "global",
|
|
404
|
+
key: "UNRELATED_SECRET",
|
|
405
|
+
value: "unused",
|
|
406
|
+
isSecret: true,
|
|
407
|
+
});
|
|
408
|
+
await getDbClient().run(
|
|
409
|
+
"UPDATE swarm_config SET value = 'invalid-ciphertext', encrypted = 1 WHERE key = 'UNRELATED_SECRET'",
|
|
410
|
+
);
|
|
411
|
+
const result = await request("POST", "", { key: KEY, value: first });
|
|
412
|
+
expect(result.status).toBe(200);
|
|
413
|
+
// Remove the damaged fixture before the normal reload after this test.
|
|
414
|
+
await getDbClient().run("DELETE FROM swarm_config WHERE key = 'UNRELATED_SECRET'");
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
test("uses a stored provider URL and disables redirects", async () => {
|
|
418
|
+
await upsertSwarmConfig({
|
|
419
|
+
scope: "global",
|
|
420
|
+
key: "OPENROUTER_BASE_URL",
|
|
421
|
+
value: "https://gateway.example/v1",
|
|
422
|
+
});
|
|
423
|
+
const result = await request("POST", "", { key: "OPENROUTER_API_KEY", value: first });
|
|
424
|
+
expect(result.status).toBe(200);
|
|
425
|
+
expect(providerCalls).toHaveLength(1);
|
|
426
|
+
expect(providerCalls[0]).toMatchObject({
|
|
427
|
+
url: "https://gateway.example/v1/key",
|
|
428
|
+
init: { redirect: "manual" },
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
test("a null baseUrl checks at the provider default while a gateway URL is stored", async () => {
|
|
433
|
+
await upsertSwarmConfig({
|
|
434
|
+
scope: "global",
|
|
435
|
+
key: "OPENROUTER_BASE_URL",
|
|
436
|
+
value: "https://gateway.example/v1",
|
|
437
|
+
});
|
|
438
|
+
const body = { key: "OPENROUTER_API_KEY", value: first, baseUrl: null };
|
|
439
|
+
expect((await request("POST", "", body)).status).toBe(200);
|
|
440
|
+
expect(providerCalls).toHaveLength(1);
|
|
441
|
+
expect(providerCalls[0]?.url).toStartWith("https://openrouter.ai/");
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
test("an Anthropic key behind a gateway is checked at the gateway", async () => {
|
|
445
|
+
process.env.ANTHROPIC_BASE_URL = "https://gateway.example";
|
|
446
|
+
const result = await request("POST", "", { key: KEY, value: first });
|
|
447
|
+
expect(result.status).toBe(200);
|
|
448
|
+
expect(result.body.checks[0]?.result).toBe("verified");
|
|
449
|
+
expect(providerCalls.map((call) => call.url)).toEqual(["https://gateway.example/v1/models"]);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
test("the old PUT config route still saves a whole pool", async () => {
|
|
453
|
+
const response = await call("/api/config", "PUT", {
|
|
454
|
+
scope: "global",
|
|
455
|
+
key: KEY,
|
|
456
|
+
value: `${first},${second}`,
|
|
457
|
+
isSecret: true,
|
|
458
|
+
});
|
|
459
|
+
expect(response.status).toBe(200);
|
|
460
|
+
expect(await response.text()).not.toContain(first);
|
|
461
|
+
expect((await pool()).keys).toHaveLength(2);
|
|
462
|
+
expect(providerCalls).toHaveLength(0);
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
describe("credential pool removal", () => {
|
|
467
|
+
test("removes the member at the matching index and suffix", async () => {
|
|
468
|
+
await store(`${first},${second}`);
|
|
469
|
+
expect(await request("DELETE", `?key=${KEY}&index=0&suffix=first`)).toMatchObject({
|
|
470
|
+
status: 200,
|
|
471
|
+
body: { source: "stored", keys: [{ index: 0, suffix: "secnd" }] },
|
|
472
|
+
});
|
|
473
|
+
expect((await getSwarmConfigs({ scope: "global", key: KEY }))[0]!.value).toBe(second);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
test("a mismatched index or suffix returns 409 without changing the pool", async () => {
|
|
477
|
+
await store();
|
|
478
|
+
expect((await request("DELETE", `?key=${KEY}&index=1&suffix=first`)).status).toBe(409);
|
|
479
|
+
expect(await request("DELETE", `?key=${KEY}&index=0&suffix=other`)).toMatchObject({
|
|
480
|
+
status: 409,
|
|
481
|
+
body: { error: "The key list changed. Reload it and try again." },
|
|
482
|
+
});
|
|
483
|
+
expect((await getSwarmConfigs({ scope: "global", key: KEY }))[0]!.value).toBe(first);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
test("removing the last member deletes the config row", async () => {
|
|
487
|
+
await store();
|
|
488
|
+
expect(await request("DELETE", `?key=${KEY}&index=0&suffix=first`)).toMatchObject({
|
|
489
|
+
status: 200,
|
|
490
|
+
body: { source: "none", keys: [] },
|
|
491
|
+
});
|
|
492
|
+
expect(await getSwarmConfigs({ scope: "global", key: KEY })).toEqual([]);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
test("the response excludes a deleted key still injected into process.env", async () => {
|
|
496
|
+
await store();
|
|
497
|
+
await loadGlobalConfigsIntoEnv(true);
|
|
498
|
+
expect(process.env[KEY]).toBe(first);
|
|
499
|
+
expect(await request("DELETE", `?key=${KEY}&index=0&suffix=first`)).toMatchObject({
|
|
500
|
+
status: 200,
|
|
501
|
+
body: { source: "none", readOnly: false, keys: [] },
|
|
502
|
+
});
|
|
503
|
+
expect(await pool()).toMatchObject({ source: "none", keys: [] });
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
test("deleting the stored pool restores the deployment env source immediately", async () => {
|
|
507
|
+
process.env[KEY] = second;
|
|
508
|
+
await store();
|
|
509
|
+
await loadGlobalConfigsIntoEnv(true);
|
|
510
|
+
expect(await request("DELETE", `?key=${KEY}&index=0&suffix=first`)).toMatchObject({
|
|
511
|
+
status: 200,
|
|
512
|
+
body: { source: "env", readOnly: true, keys: [{ suffix: "secnd" }] },
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
test.each([
|
|
517
|
+
0, 1,
|
|
518
|
+
])("removing index %i keeps the surviving status at index zero", async (removed) => {
|
|
519
|
+
await store(`${first},${second}`);
|
|
520
|
+
await recordKeyValidation(KEY, [
|
|
521
|
+
{ keyIndex: 0, keySuffix: "first", ok: false, httpStatus: 401 },
|
|
522
|
+
{ keyIndex: 1, keySuffix: "secnd", ok: true },
|
|
523
|
+
]);
|
|
524
|
+
const suffix = removed === 0 ? "first" : "secnd";
|
|
525
|
+
expect((await request("DELETE", `?key=${KEY}&index=${removed}&suffix=${suffix}`)).status).toBe(
|
|
526
|
+
200,
|
|
527
|
+
);
|
|
528
|
+
expect(await getAvailableKeyIndices(KEY, 1)).toMatchObject({
|
|
529
|
+
availableIndices: removed === 0 ? [0] : [],
|
|
530
|
+
rejectedIndices: removed === 0 ? [] : [0],
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
});
|