@desplega.ai/agent-swarm 2.6.0 → 2.7.1
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 +2 -0
- package/dist/{acp-adapter-dgfakfpw.js → acp-adapter-8dn5tq56.js} +6 -6
- package/dist/{actions-fbqma8aa.js → actions-3r6gw7dy.js} +18 -18
- package/dist/{amp-adapter-en0jy6hs.js → amp-adapter-wjtq413p.js} +3 -3
- package/dist/{anthropic-messages-txs190dd.js → anthropic-messages-n4cby1rz.js} +4 -4
- package/dist/{app-k8a5cgpp.js → app-d53yhykk.js} +3 -3
- package/dist/{assistant-a16433gr.js → assistant-ksrawj3k.js} +20 -20
- package/dist/{boot-reembed-6c1sjfnr.js → boot-reembed-fxfw17m2.js} +11 -3
- package/dist/{boot-reembed-f4nn561n.js → boot-reembed-k5y5xbj0.js} +15 -8
- package/dist/{boot-scrub-sweep-d4w9gajf.js → boot-scrub-sweep-se41j9fd.js} +3 -3
- package/dist/{boot-seeds-9bmkspyw.js → boot-seeds-r3kwwv8b.js} +4 -4
- package/dist/{claude-adapter-nys1kjxa.js → claude-adapter-ahmyf231.js} +8 -8
- package/dist/{claude-managed-adapter-55207973.js → claude-managed-adapter-5knamegc.js} +3 -3
- package/dist/{claude-managed-setup-3cm35gey.js → claude-managed-setup-mdc7dy6b.js} +3 -3
- package/dist/{claude-sdk-session-nw5yd8cp.js → claude-sdk-session-n5wen9zk.js} +8729 -8600
- package/dist/{cli-683y0b3a.js → cli-0dqtv2jg.js} +2 -2
- package/dist/{cli-a34pr3w1.js → cli-0tww7dnz.js} +2 -2
- package/dist/{cli-w2656ka1.js → cli-0w4e4v58.js} +7 -7
- package/dist/{cli-5sckpqcx.js → cli-2sjm3t4r.js} +1 -1
- package/dist/{cli-gf2850q7.js → cli-3cywb44q.js} +18 -1
- package/dist/{cli-8wkb09q5.js → cli-3k9jej0c.js} +2 -2
- package/dist/{cli-3aqd4jaa.js → cli-3v2cyqb8.js} +3 -3
- package/dist/{cli-emhjcj2y.js → cli-5qt9vzmq.js} +89 -20
- package/dist/{cli-abkm71vr.js → cli-5rp1w4cr.js} +1 -1
- package/dist/{cli-gf95q3bm.js → cli-6vwjsetx.js} +4 -4
- package/dist/{cli-k21gx5ht.js → cli-6w2yrrc0.js} +10 -10
- package/dist/{cli-m70bajzr.js → cli-75tdydgv.js} +1 -1
- package/dist/{cli-5rj83bam.js → cli-8feswf2v.js} +47 -36
- package/dist/{cli-sxgtf5fw.js → cli-8wd3mbyj.js} +1 -1
- package/dist/{cli-ca6banwz.js → cli-9csfthbw.js} +10 -8
- package/dist/{cli-dqygeeh5.js → cli-b41p764a.js} +31 -3
- package/dist/{cli-m5a9m0ak.js → cli-c1t533hh.js} +3 -3
- package/dist/{cli-2sjhpsns.js → cli-c28jxpcv.js} +1 -1
- package/dist/{cli-bqztfyve.js → cli-cb50g9cd.js} +1 -1
- package/dist/{cli-ag2yrnt5.js → cli-d8gaphtr.js} +2 -2
- package/dist/{cli-9abdgn5t.js → cli-dgtfysne.js} +3 -3
- package/dist/{cli-nq677csy.js → cli-dscnhn18.js} +1 -1
- package/dist/{cli-3fwqy71j.js → cli-dv11pgpv.js} +39 -32
- package/dist/{cli-v49fkghp.js → cli-ekptgk2m.js} +6 -6
- package/dist/{cli-c2habvsf.js → cli-f3mzy662.js} +17 -17
- package/dist/{cli-zqne1qcj.js → cli-fk7k8s9x.js} +2 -2
- package/dist/{cli-1tem6ns9.js → cli-gt4h8cxw.js} +3 -2
- package/dist/{cli-vbhgkw9h.js → cli-h3e7bhvn.js} +3 -3
- package/dist/{cli-ffvsj2qw.js → cli-hkxqdmnb.js} +3 -3
- package/dist/{cli-a2rr0fmg.js → cli-hpkvgdad.js} +7 -4
- package/dist/{cli-art7c1zh.js → cli-hq2v4rr5.js} +37 -9
- package/dist/{cli-sqma6vw1.js → cli-k1zb7raw.js} +5 -5
- package/dist/{cli-4rvr7zfx.js → cli-k4awgvzm.js} +5 -0
- package/dist/{cli-3ndmpxxv.js → cli-k7d3hvya.js} +2 -2
- package/dist/{cli-3gfvmzht.js → cli-kbtdftbw.js} +74 -26
- package/dist/{cli-hfeb68z4.js → cli-kj69b9tw.js} +34 -6
- package/dist/{cli-az25fn5f.js → cli-kk9jy4nm.js} +2 -2
- package/dist/{cli-39aet655.js → cli-m52t4cta.js} +1 -1
- package/dist/{cli-cz37jqss.js → cli-mdadewqp.js} +5 -5
- package/dist/{cli-esakngh8.js → cli-nf6k60sn.js} +1 -1
- package/dist/{cli-j6rktc91.js → cli-nzdf09js.js} +1 -1
- package/dist/{cli-rr7zx19j.js → cli-prcyzecg.js} +12 -12
- package/dist/{cli-4hxrf0p4.js → cli-pzafyqtk.js} +393 -62
- package/dist/{cli-g8ka5j4j.js → cli-qvaa1gjp.js} +173 -52
- package/dist/{cli-d2dpf95n.js → cli-rg4b2g6j.js} +1 -1
- package/dist/{cli-kf4jree9.js → cli-rm66vqdz.js} +1 -1
- package/dist/{cli-4rf0f5vg.js → cli-s504c3w7.js} +5 -3
- package/dist/{cli-nn6vx2mq.js → cli-swkx9jm2.js} +7 -5
- package/dist/{cli-g20jjwt4.js → cli-thsaq4pd.js} +16 -16
- package/dist/{cli-aj8djyqt.js → cli-twjnyvs8.js} +2 -2
- package/dist/{cli-03gg9pd1.js → cli-typdpe8g.js} +4 -4
- package/dist/{cli-mtnj4fwa.js → cli-vg8pwmsd.js} +6 -6
- package/dist/{cli-gnsb1zxa.js → cli-vhpckzvp.js} +1 -1
- package/dist/{cli-7enjwya9.js → cli-vz78s5wm.js} +1 -1
- package/dist/{cli-dv97kdzv.js → cli-wdxgsdz5.js} +2 -2
- package/dist/{cli-7a6p4e65.js → cli-wtjg6je3.js} +1 -1
- package/dist/{cli-6y51ryva.js → cli-x3cngb1h.js} +44 -12
- package/dist/{cli-n7asmce6.js → cli-xbz6mcg4.js} +3 -3
- package/dist/{cli-24j1jeeb.js → cli-y4tdxsqb.js} +3 -3
- package/dist/{cli-mvjz5an3.js → cli-yqrsp3ny.js} +7 -7
- package/dist/cli.js +31 -21
- package/dist/{codex-adapter-d95q4qsn.js → codex-adapter-7wepzq15.js} +6 -6
- package/dist/{codex-hook-brpnycn5.js → codex-hook-pbdh4xhx.js} +2 -2
- package/dist/{codex-session-runner-gdwd8h0c.js → codex-session-runner-02sb49e4.js} +6 -6
- package/dist/{commands-jqmgk54b.js → commands-3rcspxct.js} +2 -2
- package/dist/{controller-z3jq589f.js → controller-8ar4avd6.js} +3 -3
- package/dist/{controllers-connect-r2za39ef.js → controllers-connect-ajvh9h0v.js} +19 -19
- package/dist/{cursor-adapter-4mr4jgkw.js → cursor-adapter-gd0sa7bh.js} +1 -1
- package/dist/{db-w36wwb5g.js → db-dv9j7ts5.js} +2 -2
- package/dist/{db-ppv2tze7.js → db-z26vgdvj.js} +3 -3
- package/dist/{dispatcher-pv5h77vf.js → dispatcher-8m0htjq8.js} +17 -17
- package/dist/{dsh-adapter-d4pdpb3r.js → dsh-adapter-y8gyk130.js} +1 -1
- package/dist/{e2b-fn3zacvt.js → e2b-74vg9b7e.js} +1 -1
- package/dist/{e2b-222hsajc.js → e2b-jyay8wzn.js} +1 -1
- package/dist/{easter-egg-3d-4sp4dvmc.js → easter-egg-3d-28f880xx.js} +1 -1
- package/dist/{execute-jev839y1.js → execute-9seb79vh.js} +1 -1
- package/dist/{external-channel-y6nkzhnn.js → external-channel-smz7jyth.js} +2 -2
- package/dist/{getMachineId-bsd-brn0xym3.js → getMachineId-bsd-whfre6gr.js} +3 -3
- package/dist/{getMachineId-darwin-t339sfxm.js → getMachineId-darwin-5zjynb19.js} +3 -3
- package/dist/{getMachineId-linux-5eq95d8b.js → getMachineId-linux-8mksrrsq.js} +3 -3
- package/dist/{getMachineId-unsupported-qavrtc7j.js → getMachineId-unsupported-fxtqsdt2.js} +3 -3
- package/dist/{getMachineId-win-q7fe1wx7.js → getMachineId-win-nzfmz857.js} +3 -3
- package/dist/{grok-adapter-t08b9ntm.js → grok-adapter-z0nnvtbn.js} +7 -7
- package/dist/handlers-pspryh6g.js +41 -0
- package/dist/{heartbeat-vn31xwdk.js → heartbeat-f928aqyp.js} +20 -20
- package/dist/{hook-cpjs8q51.js → hook-d7c605r4.js} +7 -7
- package/dist/{hook-6d0zbvvt.js → hook-q9hv5xf9.js} +5 -5
- package/dist/{http-9c6medda.js → http-q6j4k989.js} +1000 -251
- package/dist/{index-k0yt1tvq.js → index-5nb266jr.js} +17 -17
- package/dist/{index-ysd45kgc.js → index-8fxt5eah.js} +10 -10
- package/dist/{index-zgvqzjsw.js → index-cekzwhez.js} +5 -5
- package/dist/{index-38t35h0n.js → index-ch0c1xff.js} +17 -17
- package/dist/{index-pkma1yf8.js → index-ebnjhxh1.js} +1 -1
- package/dist/{index-yqgbfgaj.js → index-ed8dy8rn.js} +20 -20
- package/dist/{index-qhjf2htm.js → index-ejqdkkqx.js} +262 -96
- package/dist/{index-dr94zbnh.js → index-farm1zsn.js} +32661 -29443
- package/dist/{index-g6k9tctj.js → index-yn1yxg85.js} +1 -1
- package/dist/{keepalive-em0117bp.js → keepalive-zzc3hks1.js} +3 -3
- package/dist/lead-c0pjcaw4.js +79 -0
- package/dist/{maintenance-kbme3qy9.js → maintenance-kqsxzpzx.js} +1 -1
- package/dist/{oauth-refresh-sweep-xysj4yse.js → oauth-refresh-sweep-7qt0g55z.js} +8 -3
- package/dist/{onboard-p6q0vz97.js → onboard-k7x13fqg.js} +121 -8
- package/dist/{opencode-adapter-esv3k9yn.js → opencode-adapter-tqvrtter.js} +5 -5
- package/dist/{otel-impl-54qhqh1z.js → otel-impl-sqkf6k3b.js} +97 -97
- package/dist/{pi-mono-adapter-mymbwhj0.js → pi-mono-adapter-m2qnafxm.js} +89 -14
- package/dist/{pricing-refresh-yb75wv33.js → pricing-refresh-wkpwhgvs.js} +3 -3
- package/dist/{rbac-roles-dexhc9x8.js → rbac-roles-dpn0c0wj.js} +7 -4
- package/dist/{rbac-roles-1vp2mw4p.js → rbac-roles-wk7xd6a6.js} +2 -2
- package/dist/{render-v2-tjay5hhf.js → render-v2-ex2m4g2k.js} +3 -3
- package/dist/{resume-8ayf37g5.js → resume-725g9b0b.js} +7 -7
- package/dist/{runtime-hqe5cqcd.js → runtime-kajas15g.js} +1 -1
- package/dist/{session-access-fdhp987s.js → session-access-0j2ezjje.js} +8 -4
- package/dist/{session-source-backfill-qgtc86z1.js → session-source-backfill-62202awe.js} +2 -2
- package/dist/{setup-4rrmqcq3.js → setup-9sj11ax6.js} +1 -1
- package/dist/{virtual-modules-52km00d6.js → virtual-modules-c6fcq6zr.js} +2 -2
- package/dist/{work-objects-p28fgd70.js → work-objects-0y07495y.js} +20 -20
- package/dist/worker-d4q9sqbc.js +79 -0
- package/openapi.json +1536 -407
- package/package.json +7 -5
- package/src/be/automation-preflight.ts +2 -0
- package/src/be/connector-codes.ts +65 -22
- package/src/be/db/api-keys.ts +9 -0
- package/src/be/db/workflow-runs.ts +36 -5
- package/src/be/db.ts +80 -22
- package/src/be/memory/boot-reembed.ts +4 -1
- package/src/be/memory/constants.ts +6 -0
- package/src/be/memory/embedding-recovery.ts +48 -0
- package/src/be/memory/index-content.ts +5 -2
- package/src/be/memory/providers/openai-embedding.ts +65 -14
- package/src/be/memory/task-learning.ts +257 -0
- package/src/be/memory/types.ts +13 -2
- package/src/be/migrations/245_script_executor.sql +3 -0
- package/src/be/migrations/246_connector_code_kind.sql +11 -0
- package/src/be/oauth-refresh-sweep.ts +2 -1
- package/src/be/onboarding.ts +125 -15
- package/src/be/rbac-roles.ts +4 -1
- package/src/be/scripts/boot-reembed.ts +2 -0
- package/src/be/scripts/db.ts +14 -4
- package/src/be/scripts/embeddings.ts +4 -2
- package/src/be/scripts/run-saved.ts +1 -0
- package/src/be/scripts/typecheck.ts +9 -3
- package/src/be/seed/agent-fs-provision.ts +75 -14
- package/src/be/seed-skills/index.ts +7 -0
- package/src/be/swarm-config-guard.ts +3 -0
- package/src/cli.tsx +9 -0
- package/src/comb/config.ts +6 -1
- package/src/commands/onboard/agent-fs-version.ts +5 -0
- package/src/commands/onboard/compose-generator.ts +117 -4
- package/src/commands/onboard/env-generator.ts +9 -0
- package/src/commands/onboard/tei-image.ts +12 -0
- package/src/commands/onboard/types.ts +7 -0
- package/src/commands/onboard.tsx +11 -1
- package/src/commands/provider-credentials.ts +2 -1
- package/src/commands/runner.ts +14 -1
- package/src/heartbeat/heartbeat.ts +14 -1
- package/src/heartbeat/script-runner-probe.ts +47 -0
- package/src/http/agentfs-proxy.ts +369 -0
- package/src/http/all-routes.ts +1 -0
- package/src/http/api-keys.ts +24 -9
- package/src/http/codex-oauth-device.ts +8 -1
- package/src/http/fs.ts +64 -10
- package/src/http/idempotency-key.ts +1 -1
- package/src/http/inbox-state.ts +27 -1
- package/src/http/index.ts +7 -4
- package/src/http/memory.ts +7 -4
- package/src/http/onboarding.ts +139 -11
- package/src/http/pages-public.ts +38 -5
- package/src/http/scripts.ts +27 -0
- package/src/http/session-access.ts +24 -0
- package/src/http/sessions.ts +10 -6
- package/src/http/tasks.ts +85 -11
- package/src/http/telemetry-ui.ts +1 -1
- package/src/http/user-roles.ts +4 -1
- package/src/http/users.ts +272 -3
- package/src/http/utils.ts +22 -3
- package/src/http/workflows.ts +10 -0
- package/src/http/x.ts +5 -0
- package/src/prompts/base-prompt.ts +45 -10
- package/src/prompts/session-templates.ts +43 -5
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/codex-adapter.ts +6 -1
- package/src/providers/cursor-adapter.ts +1 -1
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/providers/pi-mono-adapter.ts +99 -14
- package/src/rbac/legacy-policy.ts +1 -0
- package/src/rbac/permissions.ts +4 -0
- package/src/scheduler/scheduler.ts +2 -1
- 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 +9 -2
- package/src/scripts-runtime/types/stdlib.d.ts +9 -1
- package/src/scripts-runtime/types/swarm-sdk.d.ts +9 -1
- package/src/tests/agent-fs-provision-seeder.test.ts +261 -92
- package/src/tests/agentfs-proxy.test.ts +1106 -0
- package/src/tests/api-key-status-by-suffix.test.ts +242 -0
- package/src/tests/base-prompt.test.ts +139 -4
- package/src/tests/codex-adapter.test.ts +46 -8
- package/src/tests/codex-oauth-device.test.ts +12 -1
- package/src/tests/comb-config-guard.test.ts +20 -0
- package/src/tests/comb-links.test.ts +79 -1
- package/src/tests/comb-status.test.ts +44 -0
- package/src/tests/community-linear-delivery-loop.test.ts +2099 -0
- package/src/tests/credential-check.test.ts +4 -2
- 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/fetch-resolved-env.test.ts +16 -2
- package/src/tests/fixtures/onboard/baseline-states.ts +68 -0
- package/src/tests/fixtures/onboard/content.compose.yml.txt +190 -0
- package/src/tests/fixtures/onboard/content.env.txt +29 -0
- package/src/tests/fixtures/onboard/dev.compose.yml.txt +155 -0
- package/src/tests/fixtures/onboard/dev.env.txt +28 -0
- package/src/tests/fixtures/onboard/full-all.compose.yml.txt +582 -0
- package/src/tests/fixtures/onboard/full-all.env.txt +59 -0
- package/src/tests/fixtures/onboard/full.compose.yml.txt +435 -0
- package/src/tests/fixtures/onboard/full.env.txt +36 -0
- package/src/tests/fixtures/onboard/research.compose.yml.txt +155 -0
- package/src/tests/fixtures/onboard/research.env.txt +28 -0
- package/src/tests/fixtures/onboard/solo.compose.yml.txt +85 -0
- package/src/tests/fixtures/onboard/solo.env.txt +26 -0
- package/src/tests/heartbeat-checklist.test.ts +89 -0
- package/src/tests/heartbeat-script-runner-probe.test.ts +83 -0
- package/src/tests/http-log-scrubbing.test.ts +34 -1
- package/src/tests/http-task-write-ownership.test.ts +5 -1
- package/src/tests/invites-http.test.ts +464 -0
- package/src/tests/memory-embedding-input-cap.test.ts +122 -1
- package/src/tests/memory-embedding-recovery.test.ts +137 -0
- package/src/tests/oauth-refresh-sweep.test.ts +34 -1
- package/src/tests/onboard-compose.test.ts +162 -0
- package/src/tests/onboard-env.test.ts +24 -0
- package/src/tests/onboard-non-interactive.test.ts +29 -0
- package/src/tests/onboard-tei-image.test.ts +36 -0
- package/src/tests/onboarding.test.ts +451 -4
- package/src/tests/openai-embedding-provider.test.ts +105 -2
- package/src/tests/opencode-adapter.test.ts +2 -1
- 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-mono-session-cost-on-abort.test.ts +270 -0
- package/src/tests/pi-native-mcp.test.ts +13 -1
- package/src/tests/pi-tool-exposure.test.ts +8 -8
- package/src/tests/preload-env.test.ts +4 -0
- package/src/tests/preload.ts +4 -0
- package/src/tests/probe-tool-schema-tokens.test.ts +144 -0
- package/src/tests/prompt-template-session.test.ts +20 -3
- package/src/tests/rbac-admission-e2e.test.ts +22 -6
- package/src/tests/rbac-admission.test.ts +342 -4
- package/src/tests/rbac-engine.test.ts +1 -0
- package/src/tests/rbac-roles.test.ts +9 -1
- package/src/tests/request-composition-derive.test.ts +329 -0
- package/src/tests/script-connections.test.ts +2 -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/scripts-external-api.test.ts +45 -0
- package/src/tests/sdk-allowlist.test.ts +3 -2
- package/src/tests/self-improvement.test.ts +4 -4
- package/src/tests/send-task-requester-gate.test.ts +64 -3
- package/src/tests/sessions-move.test.ts +39 -1
- package/src/tests/store-progress-learning.test.ts +381 -0
- package/src/tests/ui-logs-parser.test.ts +180 -0
- package/src/tests/whoami-route.test.ts +41 -0
- package/src/tests/workflow-engine-v2.test.ts +116 -1
- 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-swarm-script.test.ts +34 -0
- package/src/tools/inject-learning.ts +1 -1
- package/src/tools/memory-edit.ts +1 -1
- package/src/tools/memory-search.ts +1 -1
- 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 +71 -1
- package/src/tools/utils.ts +12 -1
- package/src/tools/workflows/trigger-workflow.ts +104 -10
- package/src/types.ts +12 -1
- package/src/utils/constants.ts +39 -0
- package/src/utils/credentials.ts +5 -2
- package/src/utils/task-attachment-links.ts +15 -3
- package/src/workflows/engine.ts +18 -16
- 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/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/swarm-docs/config.json +14 -0
- package/templates/skills/swarm-docs/content.md +144 -0
- package/dist/handlers-eqsnmv95.js +0 -41
- package/dist/lead-23e7z59t.js +0 -79
- package/dist/worker-e3w7vye1.js +0 -79
- package/dist/{azure-openai-responses-9kfwhmn7.js → azure-openai-responses-43asa23s.js} +0 -0
- package/dist/{node-vxx896fa.js → node-t28cc56b.js} +0 -0
- package/dist/{openai-codex-responses-79s8xnbh.js → openai-codex-responses-jhr3sjtm.js} +3 -3
- package/dist/{openai-completions-tdbyts0x.js → openai-completions-gs6yn37k.js} +3 -3
- /package/dist/{openai-responses-dm3t5vqp.js → openai-responses-jqm9vwwk.js} +0 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Key status is matched by key suffix when the caller sends its pool
|
|
3
|
+
* (`GET /api/keys/available?keySuffixes=`), so two agent-scoped pools with
|
|
4
|
+
* different keys at the same index do not bench each other (issue #2425).
|
|
5
|
+
*/
|
|
6
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
7
|
+
import { unlink } from "node:fs/promises";
|
|
8
|
+
import { createServer as createHttpServer, type Server } from "node:http";
|
|
9
|
+
import {
|
|
10
|
+
closeDb,
|
|
11
|
+
getAvailableKeyIndices,
|
|
12
|
+
initDb,
|
|
13
|
+
markKeyRateLimited,
|
|
14
|
+
recordKeyRateLimitWindows,
|
|
15
|
+
} from "../be/db";
|
|
16
|
+
import { recordKeyValidation } from "../be/db/api-keys";
|
|
17
|
+
import { handleApiKeys } from "../http/api-keys";
|
|
18
|
+
import { resolveCredentialPools } from "../utils/credentials";
|
|
19
|
+
import { listenOnFreePort } from "./test-net";
|
|
20
|
+
|
|
21
|
+
const TEST_DB = `./test-api-key-status-by-suffix-${Date.now()}.sqlite`;
|
|
22
|
+
const FUTURE = () => new Date(Date.now() + 3_600_000).toISOString();
|
|
23
|
+
|
|
24
|
+
// Agent A and agent B each pool two keys; index 1 differs between them.
|
|
25
|
+
const POOL_A = ["example-key-shared", "example-key-aaaa1"];
|
|
26
|
+
const POOL_B = ["example-key-shared", "example-key-bbbb2"];
|
|
27
|
+
const suffixes = (pool: string[]) => pool.map((key) => key.slice(-5));
|
|
28
|
+
|
|
29
|
+
describe("key status by suffix", () => {
|
|
30
|
+
let server: Server;
|
|
31
|
+
let baseUrl = "";
|
|
32
|
+
|
|
33
|
+
const getAvailable = async (query: string) => {
|
|
34
|
+
const res = await fetch(`${baseUrl}/api/keys/available?${query}`);
|
|
35
|
+
return { status: res.status, body: (await res.json()) as Record<string, unknown> };
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
beforeAll(async () => {
|
|
39
|
+
process.env.DB_PATH = TEST_DB;
|
|
40
|
+
initDb(TEST_DB);
|
|
41
|
+
server = createHttpServer(async (req, res) => {
|
|
42
|
+
const url = new URL(req.url || "/", "http://localhost");
|
|
43
|
+
const pathSegments = url.pathname.split("/").filter(Boolean);
|
|
44
|
+
const handled = await handleApiKeys(req, res, pathSegments, url.searchParams);
|
|
45
|
+
if (!handled) {
|
|
46
|
+
res.writeHead(404);
|
|
47
|
+
res.end();
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
const port = await listenOnFreePort(server);
|
|
51
|
+
baseUrl = `http://127.0.0.1:${port}`;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
afterAll(async () => {
|
|
55
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
56
|
+
closeDb();
|
|
57
|
+
await unlink(TEST_DB).catch(() => {});
|
|
58
|
+
await unlink(`${TEST_DB}-wal`).catch(() => {});
|
|
59
|
+
await unlink(`${TEST_DB}-shm`).catch(() => {});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("a rate limit on agent A's key at index 1 does not bench agent B's key at index 1", async () => {
|
|
63
|
+
// Workers report at global scope, by suffix and index.
|
|
64
|
+
await markKeyRateLimited("OPENAI_API_KEY", "aaaa1", 1, FUTURE());
|
|
65
|
+
|
|
66
|
+
const a = await getAvailableKeyIndices("OPENAI_API_KEY", 2, "global", null, undefined, [
|
|
67
|
+
...suffixes(POOL_A),
|
|
68
|
+
]);
|
|
69
|
+
const b = await getAvailableKeyIndices("OPENAI_API_KEY", 2, "global", null, undefined, [
|
|
70
|
+
...suffixes(POOL_B),
|
|
71
|
+
]);
|
|
72
|
+
expect(a.availableIndices).toEqual([0]);
|
|
73
|
+
expect(b.availableIndices).toEqual([0, 1]);
|
|
74
|
+
|
|
75
|
+
// Control: the index read (no suffixes) still benches index 1 for everyone.
|
|
76
|
+
const legacy = await getAvailableKeyIndices("OPENAI_API_KEY", 2);
|
|
77
|
+
expect(legacy.availableIndices).toEqual([0]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("a rejection on agent A's key does not bench agent B's key at the same index", async () => {
|
|
81
|
+
await recordKeyValidation(
|
|
82
|
+
"GEMINI_API_KEY",
|
|
83
|
+
[{ keyIndex: 1, keySuffix: "aaaa1", ok: false, httpStatus: 401 }],
|
|
84
|
+
"global",
|
|
85
|
+
null,
|
|
86
|
+
{ retireAbsent: false },
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const a = await getAvailable(
|
|
90
|
+
`keyType=GEMINI_API_KEY&totalKeys=2&keySuffixes=${suffixes(POOL_A).join(",")}`,
|
|
91
|
+
);
|
|
92
|
+
const b = await getAvailable(
|
|
93
|
+
`keyType=GEMINI_API_KEY&totalKeys=2&keySuffixes=${suffixes(POOL_B).join(",")}`,
|
|
94
|
+
);
|
|
95
|
+
expect(a.body).toMatchObject({ availableIndices: [0], rejectedIndices: [1] });
|
|
96
|
+
expect(b.body).toMatchObject({ availableIndices: [0, 1], rejectedIndices: [] });
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("one key in two scopes has one state, at each pool's own position", async () => {
|
|
100
|
+
// The shared key sits at index 0 for A and B, and at index 2 in a third pool.
|
|
101
|
+
await markKeyRateLimited("ANTHROPIC_API_KEY", "hared", 0, FUTURE());
|
|
102
|
+
// A row of the same key in an agent scope (for example a dashboard check) also counts.
|
|
103
|
+
await recordKeyValidation(
|
|
104
|
+
"ANTHROPIC_API_KEY",
|
|
105
|
+
[{ keyIndex: 0, keySuffix: "hared", ok: false, httpStatus: 401 }],
|
|
106
|
+
"agent",
|
|
107
|
+
"agent-b",
|
|
108
|
+
{ retireAbsent: false },
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const b = await getAvailableKeyIndices("ANTHROPIC_API_KEY", 2, "global", null, undefined, [
|
|
112
|
+
...suffixes(POOL_B),
|
|
113
|
+
]);
|
|
114
|
+
expect(b.availableIndices).toEqual([1]);
|
|
115
|
+
expect(b.rejectedIndices).toEqual([0]);
|
|
116
|
+
|
|
117
|
+
const reordered = ["example-key-ccccc", "example-key-ddddd", "example-key-shared"];
|
|
118
|
+
const c = await getAvailableKeyIndices("ANTHROPIC_API_KEY", 3, "global", null, undefined, [
|
|
119
|
+
...suffixes(reordered),
|
|
120
|
+
]);
|
|
121
|
+
expect(c.availableIndices).toEqual([0, 1]);
|
|
122
|
+
expect(c.rejectedIndices).toEqual([2]);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("model window blocks map to the caller's positions", async () => {
|
|
126
|
+
const resetsAt = Math.floor(Date.now() / 1000) + 3600;
|
|
127
|
+
await recordKeyRateLimitWindows("CLAUDE_CODE_OAUTH_TOKEN", "aaaa1", 1, {
|
|
128
|
+
seven_day_opus: { status: "rejected", resetsAt, lastSeenAt: new Date().toISOString() },
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const a = await getAvailableKeyIndices(
|
|
132
|
+
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
133
|
+
2,
|
|
134
|
+
"global",
|
|
135
|
+
null,
|
|
136
|
+
"opus",
|
|
137
|
+
suffixes(POOL_A),
|
|
138
|
+
);
|
|
139
|
+
const b = await getAvailableKeyIndices(
|
|
140
|
+
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
141
|
+
2,
|
|
142
|
+
"global",
|
|
143
|
+
null,
|
|
144
|
+
"opus",
|
|
145
|
+
suffixes(POOL_B),
|
|
146
|
+
);
|
|
147
|
+
expect(a.availableIndices).toEqual([0]);
|
|
148
|
+
expect(a.modelBlockedIndices).toEqual([1]);
|
|
149
|
+
expect(b.availableIndices).toEqual([0, 1]);
|
|
150
|
+
expect(b.modelBlockedIndices).toEqual([]);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("two pool keys with the same suffix keep the index read: one bench does not bench both", async () => {
|
|
154
|
+
// a-same1 and b-same1 share one status row; a rate limit at index 0 must leave index 1.
|
|
155
|
+
await markKeyRateLimited("MISTRAL_API_KEY", "same1", 0, FUTURE());
|
|
156
|
+
|
|
157
|
+
const res = await getAvailable("keyType=MISTRAL_API_KEY&totalKeys=2&keySuffixes=same1,same1");
|
|
158
|
+
expect(res.body).toMatchObject({ availableIndices: [1] });
|
|
159
|
+
// Control: the index read gives the same answer.
|
|
160
|
+
const legacy = await getAvailableKeyIndices("MISTRAL_API_KEY", 2);
|
|
161
|
+
expect(legacy.availableIndices).toEqual([1]);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("validation success and a rate-limit clear at global scope reach the key's agent-scoped rows", async () => {
|
|
165
|
+
const post = (path: string, body: unknown) =>
|
|
166
|
+
fetch(`${baseUrl}/api/keys/${path}`, {
|
|
167
|
+
method: "POST",
|
|
168
|
+
headers: { "Content-Type": "application/json" },
|
|
169
|
+
body: JSON.stringify(body),
|
|
170
|
+
});
|
|
171
|
+
const available = () =>
|
|
172
|
+
getAvailable(
|
|
173
|
+
`keyType=DEEPSEEK_API_KEY&totalKeys=2&keySuffixes=${suffixes(POOL_A).join(",")}`,
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
// 401 for aaaa1 at agent scope, then OK at global scope.
|
|
177
|
+
const keys = (ok: boolean) => [
|
|
178
|
+
{ keyIndex: 1, keySuffix: "aaaa1", ok, ...(ok ? {} : { httpStatus: 401 }) },
|
|
179
|
+
];
|
|
180
|
+
expect(
|
|
181
|
+
(
|
|
182
|
+
await post("report-validation", {
|
|
183
|
+
keyType: "DEEPSEEK_API_KEY",
|
|
184
|
+
keys: keys(false),
|
|
185
|
+
scope: "agent",
|
|
186
|
+
scopeId: "agent-a",
|
|
187
|
+
})
|
|
188
|
+
).status,
|
|
189
|
+
).toBe(200);
|
|
190
|
+
expect((await available()).body).toMatchObject({ rejectedIndices: [1] });
|
|
191
|
+
expect(
|
|
192
|
+
(await post("report-validation", { keyType: "DEEPSEEK_API_KEY", keys: keys(true) })).status,
|
|
193
|
+
).toBe(200);
|
|
194
|
+
expect((await available()).body).toMatchObject({
|
|
195
|
+
availableIndices: [0, 1],
|
|
196
|
+
rejectedIndices: [],
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// A rate limit on the agent-scoped row, cleared by the worker's global-scope recheck.
|
|
200
|
+
await markKeyRateLimited("DEEPSEEK_API_KEY", "aaaa1", 1, FUTURE(), "agent", "agent-a");
|
|
201
|
+
expect((await available()).body).toMatchObject({ availableIndices: [0] });
|
|
202
|
+
const clear = await post("clear-rate-limit", {
|
|
203
|
+
keyType: "DEEPSEEK_API_KEY",
|
|
204
|
+
keySuffix: "aaaa1",
|
|
205
|
+
});
|
|
206
|
+
expect(((await clear.json()) as { cleared: boolean }).cleared).toBe(true);
|
|
207
|
+
expect((await available()).body).toMatchObject({ availableIndices: [0, 1] });
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("the route rejects a suffix list that does not match totalKeys", async () => {
|
|
211
|
+
const res = await getAvailable("keyType=OPENAI_API_KEY&totalKeys=3&keySuffixes=hared,aaaa1");
|
|
212
|
+
expect(res.status).toBe(400);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("worker key selection skips its own benched key and keeps the other pool's", async () => {
|
|
216
|
+
await markKeyRateLimited("OPENROUTER_API_KEY", "aaaa1", 1, FUTURE());
|
|
217
|
+
await markKeyRateLimited("OPENROUTER_API_KEY", "hared", 0, FUTURE());
|
|
218
|
+
|
|
219
|
+
for (let i = 0; i < 10; i++) {
|
|
220
|
+
const envA: Record<string, string | undefined> = { OPENROUTER_API_KEY: POOL_A.join(",") };
|
|
221
|
+
const [selA] = await resolveCredentialPools(envA, {
|
|
222
|
+
apiUrl: baseUrl,
|
|
223
|
+
apiKey: "test",
|
|
224
|
+
provider: "opencode",
|
|
225
|
+
model: "google/gemini-3-flash-preview",
|
|
226
|
+
});
|
|
227
|
+
// Both of A's keys are benched: best-effort fallback.
|
|
228
|
+
expect(selA!.isRateLimitFallback).toBe(true);
|
|
229
|
+
|
|
230
|
+
const envB: Record<string, string | undefined> = { OPENROUTER_API_KEY: POOL_B.join(",") };
|
|
231
|
+
const [selB] = await resolveCredentialPools(envB, {
|
|
232
|
+
apiUrl: baseUrl,
|
|
233
|
+
apiKey: "test",
|
|
234
|
+
provider: "opencode",
|
|
235
|
+
model: "google/gemini-3-flash-preview",
|
|
236
|
+
});
|
|
237
|
+
expect(selB!.isRateLimitFallback).toBe(false);
|
|
238
|
+
expect(selB!.index).toBe(1);
|
|
239
|
+
expect(envB.OPENROUTER_API_KEY).toBe("example-key-bbbb2");
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
});
|
|
@@ -34,6 +34,10 @@ const ENV_KEYS = [
|
|
|
34
34
|
"SLACK_SIGNING_SECRET",
|
|
35
35
|
"STEERING_ENABLED",
|
|
36
36
|
"AGENT_FS_API_URL",
|
|
37
|
+
"AGENT_FS_LINK_TARGET",
|
|
38
|
+
"AGENT_FS_LIVE_URL",
|
|
39
|
+
"APP_URL",
|
|
40
|
+
"DASHBOARD_URL",
|
|
37
41
|
"SCRIPTS_ONLY_MCP",
|
|
38
42
|
] as const;
|
|
39
43
|
|
|
@@ -495,6 +499,47 @@ describe("getBasePrompt: outputs section", () => {
|
|
|
495
499
|
expect(result).not.toContain(NO_AGENT_FS_LINE);
|
|
496
500
|
});
|
|
497
501
|
|
|
502
|
+
test("AGENT_FS_LINK_TARGET=dashboard with APP_URL links people to <APP_URL>/file/~/", async () => {
|
|
503
|
+
process.env.AGENT_FS_API_URL = "http://localhost:8787";
|
|
504
|
+
process.env.AGENT_FS_LINK_TARGET = "dashboard";
|
|
505
|
+
process.env.APP_URL = "http://localhost:5274/";
|
|
506
|
+
const result = await getBasePrompt({ ...minimalArgs, traits: localTraits });
|
|
507
|
+
expect(result).toContain(
|
|
508
|
+
"Give people the live app link `http://localhost:5274/file/~/<org>/<drive>/<path>`.",
|
|
509
|
+
);
|
|
510
|
+
expect(result).not.toContain("AGENT_FS_LIVE_URL}/file/~/");
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
test("AGENT_FS_LINK_TARGET=dashboard without APP_URL falls back to the live base", async () => {
|
|
514
|
+
process.env.AGENT_FS_API_URL = "http://localhost:8787";
|
|
515
|
+
process.env.AGENT_FS_LINK_TARGET = "dashboard";
|
|
516
|
+
process.env.AGENT_FS_LIVE_URL = "https://live.example.test/";
|
|
517
|
+
const result = await getBasePrompt({ ...minimalArgs, traits: localTraits });
|
|
518
|
+
expect(result).toContain(
|
|
519
|
+
"Give people the live app link `https://live.example.test/file/~/<org>/<drive>/<path>`.",
|
|
520
|
+
);
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
test("AGENT_FS_LINK_TARGET=live uses the default live host", async () => {
|
|
524
|
+
process.env.AGENT_FS_API_URL = "http://localhost:8787";
|
|
525
|
+
process.env.AGENT_FS_LINK_TARGET = "live";
|
|
526
|
+
process.env.APP_URL = "http://localhost:5274";
|
|
527
|
+
const result = await getBasePrompt({ ...minimalArgs, traits: localTraits });
|
|
528
|
+
expect(result).toContain(
|
|
529
|
+
"Give people the live app link `https://live.agent-fs.dev/file/~/<org>/<drive>/<path>`.",
|
|
530
|
+
);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
test("an invalid AGENT_FS_LINK_TARGET keeps the exact old sentence", async () => {
|
|
534
|
+
process.env.AGENT_FS_API_URL = "http://localhost:8787";
|
|
535
|
+
process.env.AGENT_FS_LINK_TARGET = "Dashboard2";
|
|
536
|
+
process.env.APP_URL = "http://localhost:5274";
|
|
537
|
+
const result = await getBasePrompt({ ...minimalArgs, traits: localTraits });
|
|
538
|
+
expect(result).toContain(
|
|
539
|
+
`Give people the live app link \`\${AGENT_FS_LIVE_URL}/file/~/<org>/<drive>/<path>\`. Make an \`agent-fs share-create\` link only when the requester asks for one or the reader has no agent-fs login.`,
|
|
540
|
+
);
|
|
541
|
+
});
|
|
542
|
+
|
|
498
543
|
test("uses the no_agent_fs variant when AGENT_FS_API_URL is unset", async () => {
|
|
499
544
|
const result = await getBasePrompt({ ...minimalArgs, traits: localTraits });
|
|
500
545
|
expect(result).toContain("## Outputs");
|
|
@@ -787,12 +832,78 @@ describe("getBasePrompt: tools and skills section", () => {
|
|
|
787
832
|
);
|
|
788
833
|
});
|
|
789
834
|
|
|
835
|
+
test("pi with deferral points installed MCP servers at tool search", async () => {
|
|
836
|
+
const result = await getBasePrompt({
|
|
837
|
+
...minimalArgs,
|
|
838
|
+
provider: "pi",
|
|
839
|
+
traits: { ...localTraits, hasToolSearch: true },
|
|
840
|
+
mcpServers: ["linear"],
|
|
841
|
+
});
|
|
842
|
+
expect(result).toContain(
|
|
843
|
+
"Connected MCP servers: linear. Load their tools with your harness tool search.",
|
|
844
|
+
);
|
|
845
|
+
expect(result).not.toContain("Their tools are in your tool list.");
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
test("pi without deferral keeps installed MCP servers in the tool list", async () => {
|
|
849
|
+
const result = await getBasePrompt({
|
|
850
|
+
...minimalArgs,
|
|
851
|
+
provider: "pi",
|
|
852
|
+
traits: { ...localTraits, hasToolSearch: false },
|
|
853
|
+
mcpServers: ["linear"],
|
|
854
|
+
});
|
|
855
|
+
expect(result).toContain("Connected MCP servers: linear. Their tools are in your tool list.");
|
|
856
|
+
});
|
|
857
|
+
|
|
790
858
|
test("omits the MCP server line for an empty list", async () => {
|
|
791
859
|
const result = await getBasePrompt({ ...minimalArgs, mcpServers: [] });
|
|
792
860
|
expect(result).not.toContain("Connected MCP servers");
|
|
793
861
|
});
|
|
794
862
|
});
|
|
795
863
|
|
|
864
|
+
describe("getBasePrompt: lead docs pointer", () => {
|
|
865
|
+
const DOCS_LINE =
|
|
866
|
+
"Load the `swarm-docs` skill only when a person asks for something no skill, workflow, schedule or memory of yours covers, or asks where something is documented. Then follow its offer rules.";
|
|
867
|
+
const withDocs = [
|
|
868
|
+
{ name: "commit", description: "Create a commit" },
|
|
869
|
+
{ name: "swarm-docs", description: "docs index" },
|
|
870
|
+
];
|
|
871
|
+
|
|
872
|
+
test("renders for a lead that has the swarm-docs skill", async () => {
|
|
873
|
+
const result = await getBasePrompt({
|
|
874
|
+
...minimalArgs,
|
|
875
|
+
role: "lead",
|
|
876
|
+
traits: localTraits,
|
|
877
|
+
skillsSummary: withDocs,
|
|
878
|
+
});
|
|
879
|
+
expect(result).toContain(DOCS_LINE);
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
test("drops the line when swarm-docs is disabled for the lead", async () => {
|
|
883
|
+
const result = await getBasePrompt({
|
|
884
|
+
...minimalArgs,
|
|
885
|
+
role: "lead",
|
|
886
|
+
traits: localTraits,
|
|
887
|
+
skillsSummary: [{ name: "commit", description: "Create a commit" }],
|
|
888
|
+
});
|
|
889
|
+
expect(result).not.toContain(DOCS_LINE);
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
test("renders when the skill list is unknown (claude-managed)", async () => {
|
|
893
|
+
const result = await getBasePrompt({ ...minimalArgs, role: "lead", traits: managedTraits });
|
|
894
|
+
expect(result).toContain(DOCS_LINE);
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
test("never renders for a worker", async () => {
|
|
898
|
+
const result = await getBasePrompt({
|
|
899
|
+
...minimalArgs,
|
|
900
|
+
traits: localTraits,
|
|
901
|
+
skillsSummary: withDocs,
|
|
902
|
+
});
|
|
903
|
+
expect(result).not.toContain(DOCS_LINE);
|
|
904
|
+
});
|
|
905
|
+
});
|
|
906
|
+
|
|
796
907
|
// ---------------------------------------------------------------------------
|
|
797
908
|
// 10. Repository
|
|
798
909
|
// ---------------------------------------------------------------------------
|
|
@@ -1015,14 +1126,16 @@ describe("getBasePrompt: size budget", () => {
|
|
|
1015
1126
|
expect(result.length).toBeLessThan(5_750);
|
|
1016
1127
|
});
|
|
1017
1128
|
|
|
1018
|
-
|
|
1129
|
+
// The lead ceiling includes the swarm-docs pointer (~190 chars), which renders
|
|
1130
|
+
// while the skill list is still unknown.
|
|
1131
|
+
test("a fresh claude lead stays under 6,100 characters", async () => {
|
|
1019
1132
|
const result = await getBasePrompt({
|
|
1020
1133
|
...minimalArgs,
|
|
1021
1134
|
role: "lead",
|
|
1022
1135
|
name: "Cora",
|
|
1023
1136
|
traits: localTraits,
|
|
1024
1137
|
});
|
|
1025
|
-
expect(result.length).toBeLessThan(
|
|
1138
|
+
expect(result.length).toBeLessThan(6_100);
|
|
1026
1139
|
});
|
|
1027
1140
|
|
|
1028
1141
|
test("Picateclas spawn-OOM hardening: the kitchen sink stays below MAX_ARG_STRLEN", async () => {
|
|
@@ -1125,9 +1238,7 @@ describe("getBasePrompt: harness tool discovery", () => {
|
|
|
1125
1238
|
|
|
1126
1239
|
test.each([
|
|
1127
1240
|
"pi",
|
|
1128
|
-
"codex",
|
|
1129
1241
|
"claude-managed",
|
|
1130
|
-
"opencode",
|
|
1131
1242
|
"acp",
|
|
1132
1243
|
])("%s without tool search gets direct-tool guidance", async (provider) => {
|
|
1133
1244
|
const result = await getBasePrompt({
|
|
@@ -1152,6 +1263,30 @@ describe("getBasePrompt: harness tool discovery", () => {
|
|
|
1152
1263
|
expect(result).toContain('import { store_progress } from "agent-swarm"');
|
|
1153
1264
|
});
|
|
1154
1265
|
|
|
1266
|
+
test("codex gets exec guidance with the normalized tool name", async () => {
|
|
1267
|
+
const result = await getBasePrompt({
|
|
1268
|
+
...minimalArgs,
|
|
1269
|
+
provider: "codex",
|
|
1270
|
+
traits: { ...localTraits, hasToolSearch: false },
|
|
1271
|
+
});
|
|
1272
|
+
expect(result).not.toContain(deferred);
|
|
1273
|
+
expect(result).not.toContain("already in your tool list");
|
|
1274
|
+
expect(result).toContain("await tools.mcp__agent_swarm__store_progress({ ... })");
|
|
1275
|
+
expect(result).toContain("`ALL_TOOLS`");
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
test("opencode gets execute guidance with the bracketed tool path", async () => {
|
|
1279
|
+
const result = await getBasePrompt({
|
|
1280
|
+
...minimalArgs,
|
|
1281
|
+
provider: "opencode",
|
|
1282
|
+
traits: { ...localTraits, hasToolSearch: false },
|
|
1283
|
+
});
|
|
1284
|
+
expect(result).not.toContain(deferred);
|
|
1285
|
+
expect(result).not.toContain("already in your tool list");
|
|
1286
|
+
expect(result).toContain('await tools.swarm["store-progress"]({ ... })');
|
|
1287
|
+
expect(result).toContain("`search({ query })`");
|
|
1288
|
+
});
|
|
1289
|
+
|
|
1155
1290
|
test("omitted tool-search capability defaults to direct tools", async () => {
|
|
1156
1291
|
const result = await getBasePrompt({ ...minimalArgs, traits: localTraits });
|
|
1157
1292
|
expect(result).not.toContain(deferred);
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
resolveCodexModel,
|
|
35
35
|
} from "../providers/codex-models";
|
|
36
36
|
import type { ProviderEvent, ProviderResult, ProviderSessionConfig } from "../providers/types";
|
|
37
|
+
import { isCodexAuthFailureReason } from "../utils/codex-auth-failure";
|
|
37
38
|
import { HELD_NOTE_TEXT, holdTemplateRender } from "./fixtures/held-template-render";
|
|
38
39
|
|
|
39
40
|
/**
|
|
@@ -1177,23 +1178,38 @@ describe("CodexSession event mapping", () => {
|
|
|
1177
1178
|
expect(result.failureReason).toContain("[rate-limit]");
|
|
1178
1179
|
});
|
|
1179
1180
|
|
|
1180
|
-
test
|
|
1181
|
+
test.each([
|
|
1182
|
+
undefined,
|
|
1183
|
+
0,
|
|
1184
|
+
2,
|
|
1185
|
+
])("turn.failed auth error identifies pool slot %p", async (codexSlot) => {
|
|
1186
|
+
const raw = "Request failed: HTTP 401 Unauthorized — Invalid API key provided.";
|
|
1181
1187
|
const events: ThreadEvent[] = [
|
|
1182
1188
|
{ type: "thread.started", thread_id: "thread-auth" },
|
|
1183
1189
|
{ type: "turn.started" },
|
|
1184
1190
|
{
|
|
1185
1191
|
type: "turn.failed",
|
|
1186
|
-
error: { message:
|
|
1192
|
+
error: { message: raw },
|
|
1187
1193
|
},
|
|
1188
1194
|
];
|
|
1189
1195
|
|
|
1190
1196
|
const { emitted, result } = await runSessionWithFakeThread(
|
|
1191
1197
|
events,
|
|
1192
|
-
testConfig({
|
|
1198
|
+
testConfig({
|
|
1199
|
+
logFile: join(tmpLogDir, "auth.log"),
|
|
1200
|
+
cwd: "",
|
|
1201
|
+
codexSlot,
|
|
1202
|
+
apiUrl: "",
|
|
1203
|
+
apiKey: "",
|
|
1204
|
+
}),
|
|
1193
1205
|
);
|
|
1194
1206
|
|
|
1207
|
+
const expected =
|
|
1208
|
+
codexSlot === undefined
|
|
1209
|
+
? `[auth-error] Codex authentication failed — check OPENAI_API_KEY or ChatGPT login. Original error: ${raw}`
|
|
1210
|
+
: `[auth-error] Codex pool slot ${codexSlot} authentication failed — re-run agent-swarm codex-login --slot ${codexSlot}. Original error: ${raw}`;
|
|
1195
1211
|
const errorEvent = emitted.find((e) => e.type === "error");
|
|
1196
|
-
expect(errorEvent?.type === "error" && errorEvent.message).
|
|
1212
|
+
expect(errorEvent?.type === "error" && errorEvent.message).toBe(expected);
|
|
1197
1213
|
|
|
1198
1214
|
const resultEvent = emitted.findLast((e) => e.type === "result");
|
|
1199
1215
|
expect(resultEvent?.type === "result" && resultEvent.errorCategory).toBe(
|
|
@@ -1201,7 +1217,8 @@ describe("CodexSession event mapping", () => {
|
|
|
1201
1217
|
);
|
|
1202
1218
|
|
|
1203
1219
|
expect(result.isError).toBe(true);
|
|
1204
|
-
expect(result.failureReason).
|
|
1220
|
+
expect(result.failureReason).toBe(expected);
|
|
1221
|
+
expect(isCodexAuthFailureReason(result.failureReason)).toBe(true);
|
|
1205
1222
|
});
|
|
1206
1223
|
|
|
1207
1224
|
test("abort() resolves the session with cancelled result", async () => {
|
|
@@ -2517,7 +2534,9 @@ describe("CodexSession — rate-limit error preservation", () => {
|
|
|
2517
2534
|
expect(resetMs).toBeGreaterThan(Date.now());
|
|
2518
2535
|
});
|
|
2519
2536
|
|
|
2520
|
-
test(
|
|
2537
|
+
test.each([
|
|
2538
|
+
0, 2,
|
|
2539
|
+
])("revoked-while-valid OAuth failure identifies pool slot %i after transport exit", async (codexSlot) => {
|
|
2521
2540
|
// A credential revoked server-side while its access token is still
|
|
2522
2541
|
// unexpired never enters getValidCodexOAuth (the token looks fresh, it
|
|
2523
2542
|
// fast-returns) — the spawned Codex CLI self-refreshes straight from the
|
|
@@ -2534,13 +2553,32 @@ describe("CodexSession — rate-limit error preservation", () => {
|
|
|
2534
2553
|
];
|
|
2535
2554
|
const sdkThrow = new Error("Codex Exec exited with code 1: Reading prompt from stdin");
|
|
2536
2555
|
|
|
2537
|
-
const { result } = await runSessionWithThrowingThread(
|
|
2556
|
+
const { emitted, result } = await runSessionWithThrowingThread(
|
|
2538
2557
|
events,
|
|
2539
2558
|
sdkThrow,
|
|
2540
|
-
testConfig({
|
|
2559
|
+
testConfig({
|
|
2560
|
+
logFile: join(tmpLogDir, "authfail-preserve.log"),
|
|
2561
|
+
cwd: "",
|
|
2562
|
+
codexSlot,
|
|
2563
|
+
apiUrl: "",
|
|
2564
|
+
apiKey: "",
|
|
2565
|
+
}),
|
|
2541
2566
|
);
|
|
2542
2567
|
|
|
2543
2568
|
expect(result.failureReason).toMatch(/\[auth-error\]/);
|
|
2569
|
+
expect(result.failureReason).toContain(`pool slot ${codexSlot}`);
|
|
2570
|
+
expect(result.failureReason).toContain(`agent-swarm codex-login --slot ${codexSlot}`);
|
|
2571
|
+
expect(result.failureReason).toContain(refreshFailMsg);
|
|
2572
|
+
expect(isCodexAuthFailureReason(result.failureReason)).toBe(true);
|
|
2573
|
+
const errorEvents = emitted.filter((e) => e.type === "error");
|
|
2574
|
+
expect(errorEvents.length).toBeGreaterThan(0);
|
|
2575
|
+
for (const event of errorEvents) {
|
|
2576
|
+
expect(event.message).toBe(result.failureReason);
|
|
2577
|
+
}
|
|
2578
|
+
const resultEvent = emitted.findLast((e) => e.type === "result");
|
|
2579
|
+
expect(resultEvent?.type === "result" && resultEvent.errorCategory).toBe(
|
|
2580
|
+
"authentication_failed",
|
|
2581
|
+
);
|
|
2544
2582
|
expect(result.isError).toBe(true);
|
|
2545
2583
|
});
|
|
2546
2584
|
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
2
2
|
import { createServer, type Server } from "node:http";
|
|
3
3
|
import { decryptSecret, encryptSecret, getEncryptionKey } from "../be/crypto";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
closeDb,
|
|
6
|
+
getDbClient,
|
|
7
|
+
getKeyStatuses,
|
|
8
|
+
getSwarmConfigs,
|
|
9
|
+
initDb,
|
|
10
|
+
upsertSwarmConfig,
|
|
11
|
+
} from "../be/db";
|
|
5
12
|
import { getCodexDeviceFlow, upsertCodexDeviceFlow } from "../be/db/codex-oauth-device-flows";
|
|
6
13
|
import type { OnboardingState } from "../be/onboarding";
|
|
7
14
|
import { handleCodexOAuthDevice } from "../http/codex-oauth-device";
|
|
@@ -92,6 +99,7 @@ afterAll(async () => {
|
|
|
92
99
|
beforeEach(async () => {
|
|
93
100
|
await getDbClient().run("DELETE FROM codex_oauth_device_flows");
|
|
94
101
|
await getDbClient().run("DELETE FROM swarm_config");
|
|
102
|
+
await getDbClient().run("DELETE FROM api_key_status");
|
|
95
103
|
fetchImpl = async () => {
|
|
96
104
|
throw new Error("Unexpected fetch");
|
|
97
105
|
};
|
|
@@ -231,6 +239,9 @@ describe("Codex device HTTP routes", () => {
|
|
|
231
239
|
const onboarding = await getSwarmConfigs({ scope: "global", key: "onboarding_state" });
|
|
232
240
|
const state = JSON.parse(onboarding[0]!.value) as OnboardingState;
|
|
233
241
|
expect(state.steps.ai).toMatchObject({ status: "done", method: "codex_device" });
|
|
242
|
+
// The sign-in itself checks the account: the account list says "Signed in".
|
|
243
|
+
const statuses = await getKeyStatuses("CODEX_OAUTH", "global", null);
|
|
244
|
+
expect(statuses.map((s) => [s.keySuffix, s.keyIndex])).toEqual([["-full", 1]]);
|
|
234
245
|
});
|
|
235
246
|
|
|
236
247
|
test("throttles upstream polling to the requested interval", async () => {
|
|
@@ -11,6 +11,15 @@ describe("Comb config keys", () => {
|
|
|
11
11
|
}
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
+
test("AGENT_FS_PROXY accepts boolean literals only", () => {
|
|
15
|
+
for (const value of ["true", "false", "1", "0", " TRUE "]) {
|
|
16
|
+
expect(validateConfigValue("AGENT_FS_PROXY", value)).toBeNull();
|
|
17
|
+
}
|
|
18
|
+
for (const value of ["yes", "on", ""]) {
|
|
19
|
+
expect(validateConfigValue("AGENT_FS_PROXY", value)).toContain("Invalid AGENT_FS_PROXY");
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
14
23
|
test("AGENT_FS_PUBLIC_URL accepts blank or an http(s) base URL", () => {
|
|
15
24
|
for (const value of ["", "http://localhost:7433", "https://agent-fs.example.com/base"]) {
|
|
16
25
|
expect(validateConfigValue("AGENT_FS_PUBLIC_URL", value)).toBeNull();
|
|
@@ -28,6 +37,17 @@ describe("Comb config keys", () => {
|
|
|
28
37
|
}
|
|
29
38
|
});
|
|
30
39
|
|
|
40
|
+
test("AGENT_FS_LINK_TARGET accepts dashboard, live, or blank", () => {
|
|
41
|
+
for (const value of ["dashboard", "live", "", " live "]) {
|
|
42
|
+
expect(validateConfigValue("AGENT_FS_LINK_TARGET", value)).toBeNull();
|
|
43
|
+
}
|
|
44
|
+
for (const value of ["Dashboard2", "comb", true]) {
|
|
45
|
+
expect(validateConfigValue("AGENT_FS_LINK_TARGET", value)).toContain(
|
|
46
|
+
"Invalid AGENT_FS_LINK_TARGET",
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
31
51
|
test("OPENROUTER_BASE_URL keeps its rules after sharing the URL validator", () => {
|
|
32
52
|
expect(validateConfigValue("OPENROUTER_BASE_URL", "")).toBeNull();
|
|
33
53
|
expect(validateConfigValue("OPENROUTER_BASE_URL", "https://api.example.com/v1")).toBeNull();
|