@desplega.ai/agent-swarm 2.5.3 → 2.6.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 +2 -2
- package/dist/{acp-adapter-gr2fcyyj.js → acp-adapter-dgfakfpw.js} +9 -9
- package/dist/{actions-wv766ghf.js → actions-fbqma8aa.js} +21 -21
- package/dist/{amp-adapter-speb906t.js → amp-adapter-en0jy6hs.js} +4 -4
- package/dist/{app-ag29n14f.js → app-k8a5cgpp.js} +3 -3
- package/dist/{assistant-3x8bz3eg.js → assistant-a16433gr.js} +30 -26
- package/dist/{boot-reembed-jsmfrpzf.js → boot-reembed-6c1sjfnr.js} +1 -1
- package/dist/{boot-reembed-5b3d2eea.js → boot-reembed-f4nn561n.js} +1 -1
- package/dist/{boot-scrub-sweep-s73gerxy.js → boot-scrub-sweep-d4w9gajf.js} +4 -4
- package/dist/{boot-seeds-5zmax2wr.js → boot-seeds-9bmkspyw.js} +4 -4
- package/dist/{claude-adapter-hswdnm3t.js → claude-adapter-nys1kjxa.js} +9 -9
- package/dist/{claude-managed-adapter-cvbzj1h6.js → claude-managed-adapter-55207973.js} +3 -3
- package/dist/{claude-managed-setup-3k8m3hhc.js → claude-managed-setup-3cm35gey.js} +11 -4
- package/dist/{claude-sdk-session-fkbyk66x.js → claude-sdk-session-nw5yd8cp.js} +9313 -9260
- package/dist/{cli-210xd1f4.js → cli-03gg9pd1.js} +4 -4
- package/dist/{cli-kxpfx2xr.js → cli-0z1xjzwa.js} +2 -2
- package/dist/cli-1tem6ns9.js +894 -0
- package/dist/{cli-25j01dpj.js → cli-24j1jeeb.js} +3 -3
- package/dist/{cli-ta8bd7th.js → cli-2sjhpsns.js} +2 -2
- package/dist/{cli-p859hcc8.js → cli-39aet655.js} +2 -2
- package/dist/{cli-g1cw7xtt.js → cli-39rrw8yp.js} +1 -1
- package/dist/{cli-e2ar4bp1.js → cli-3aqd4jaa.js} +121 -47
- package/dist/{cli-jjznk3f7.js → cli-3fwqy71j.js} +69 -35
- package/dist/{cli-p9rafdrb.js → cli-3gfvmzht.js} +7 -7
- package/dist/{cli-03vpq45j.js → cli-3ndmpxxv.js} +2 -2
- package/dist/{cli-aecdcr77.js → cli-4hxrf0p4.js} +228 -74
- package/dist/{cli-mz9afxxf.js → cli-4rf0f5vg.js} +15 -3
- package/dist/{cli-qzme5949.js → cli-5rj83bam.js} +284 -202
- package/dist/{cli-fsdprwrg.js → cli-5sckpqcx.js} +2 -2
- package/dist/{cli-cz5nmdxt.js → cli-683y0b3a.js} +2 -2
- package/dist/{cli-3ngdk5te.js → cli-6y51ryva.js} +2 -2
- package/dist/{cli-n4c8e3s5.js → cli-708wdy32.js} +1 -1
- package/dist/{cli-pasyvjmm.js → cli-737pyqxr.js} +1 -1
- package/dist/{cli-gx4rm0qx.js → cli-7enjwya9.js} +1 -1
- package/dist/{cli-6x9ma5w5.js → cli-803188pb.js} +1 -1
- package/dist/{cli-avdvgcay.js → cli-9abdgn5t.js} +28 -9
- package/dist/{cli-99xgw26r.js → cli-a2rr0fmg.js} +1 -1
- package/dist/{cli-e5bxnjps.js → cli-a34pr3w1.js} +7 -7
- package/dist/{cli-502eqv93.js → cli-abkm71vr.js} +1 -1
- package/dist/{cli-mmhj8vd7.js → cli-ag2yrnt5.js} +11 -2
- package/dist/{cli-n45mr7zh.js → cli-aj8djyqt.js} +5 -5
- package/dist/{cli-1fc85mvt.js → cli-art7c1zh.js} +5 -5
- package/dist/{cli-40jsnrjm.js → cli-az25fn5f.js} +10 -8
- package/dist/{cli-9s05422w.js → cli-bqztfyve.js} +1 -1
- package/dist/{cli-bn0rewsy.js → cli-c081bj3b.js} +2 -2
- package/dist/{cli-svrj71pg.js → cli-c2habvsf.js} +12 -12
- package/dist/{cli-s49hpn37.js → cli-ca6banwz.js} +85 -19
- package/dist/{cli-hhxjnjzs.js → cli-cz37jqss.js} +12 -10
- package/dist/{cli-6n8axjv1.js → cli-d2dpf95n.js} +2 -2
- package/dist/{cli-tx00c0h6.js → cli-dqygeeh5.js} +17 -1
- package/dist/{cli-z21whrrg.js → cli-emhjcj2y.js} +17 -17
- package/dist/{cli-e8jyq7d8.js → cli-esakngh8.js} +7 -3
- package/dist/{cli-ha9rmwgq.js → cli-fb8m699c.js} +2 -2
- package/dist/{cli-35gtk1p6.js → cli-ffvsj2qw.js} +44 -13
- package/dist/{cli-ce0ak9ms.js → cli-g20jjwt4.js} +26 -26
- package/dist/{cli-kxawjrxg.js → cli-g8ka5j4j.js} +772 -194
- package/dist/{cli-vwn81jbs.js → cli-gf95q3bm.js} +5 -5
- package/dist/{cli-s53vp709.js → cli-gnsb1zxa.js} +2 -2
- package/dist/{cli-nwyw3zhg.js → cli-gxee0bqa.js} +1 -1
- package/dist/{cli-gpv60278.js → cli-hfeb68z4.js} +7 -7
- package/dist/{cli-0ebey9hg.js → cli-j6rktc91.js} +1 -1
- package/dist/{cli-ewm3pwbq.js → cli-k21gx5ht.js} +46 -23
- package/dist/{cli-egjggehm.js → cli-kf4jree9.js} +2 -2
- package/dist/{cli-dabehxhp.js → cli-kt907da6.js} +1 -1
- package/dist/{cli-p0rzjkya.js → cli-m5a9m0ak.js} +32 -13
- package/dist/{cli-fzcn3d87.js → cli-m70bajzr.js} +3 -3
- package/dist/{cli-w1zwctj2.js → cli-mtnj4fwa.js} +11 -5
- package/dist/{cli-gpsz7g88.js → cli-mvjz5an3.js} +16 -12
- package/dist/{cli-hcpj2xqz.js → cli-n7asmce6.js} +225 -3
- package/dist/{cli-y561h5a9.js → cli-nn6vx2mq.js} +5 -5
- package/dist/cli-nq677csy.js +140 -0
- package/dist/{cli-a5ebc95n.js → cli-pfk92eqz.js} +18 -15
- package/dist/{cli-p6zs9h4y.js → cli-rr7zx19j.js} +767 -249
- package/dist/{cli-qgf8emrn.js → cli-sqma6vw1.js} +10 -7
- package/dist/{cli-0xk505e8.js → cli-sxgtf5fw.js} +1 -1
- package/dist/{cli-nzfnt2gs.js → cli-tf1n0r0b.js} +1 -1
- package/dist/{cli-qqzmjxkf.js → cli-v49fkghp.js} +11 -6
- package/dist/{cli-x77gen2p.js → cli-w2656ka1.js} +7 -7
- package/dist/{cli-5kqnemen.js → cli-yvy48785.js} +1 -1
- package/dist/{cli-8s5wx3nz.js → cli-zqne1qcj.js} +44 -17
- package/dist/cli.js +22 -22
- package/dist/{codex-adapter-n8ka66vf.js → codex-adapter-d95q4qsn.js} +7 -7
- package/dist/{codex-hook-e2he7aca.js → codex-hook-brpnycn5.js} +1 -1
- package/dist/{codex-login-rn7c5gxt.js → codex-login-8cg0vhf2.js} +1 -1
- package/dist/{codex-session-runner-fsebts37.js → codex-session-runner-gdwd8h0c.js} +8 -8
- package/dist/{commands-t04rt2pc.js → commands-jqmgk54b.js} +2 -2
- package/dist/{controller-rgna04r2.js → controller-z3jq589f.js} +30 -4
- package/dist/{controllers-connect-bqm1rtft.js → controllers-connect-r2za39ef.js} +23 -23
- package/dist/{cursor-adapter-swr2k95y.js → cursor-adapter-4mr4jgkw.js} +2 -3
- package/dist/{db-a4m7haw0.js → db-ppv2tze7.js} +3 -3
- package/dist/{db-24ck09xy.js → db-w36wwb5g.js} +4 -2
- package/dist/{devin-adapter-4ycy4vrf.js → devin-adapter-byen2cxt.js} +1 -1
- package/dist/{dispatcher-nwf9ss0q.js → dispatcher-pv5h77vf.js} +20 -20
- package/dist/{docker-nfy2gc8t.js → docker-fdzyewpz.js} +4 -4
- package/dist/{dsh-adapter-kpdnhjst.js → dsh-adapter-d4pdpb3r.js} +2 -3
- 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-4sp4dvmc.js} +1 -1
- package/dist/{event-bus-3z78ep5n.js → event-bus-pghg8hqy.js} +1 -1
- package/dist/{execute-0xpz8950.js → execute-jev839y1.js} +1 -1
- package/dist/{external-channel-j1e2p34p.js → external-channel-y6nkzhnn.js} +2 -2
- package/dist/{grok-adapter-qtjz0whk.js → grok-adapter-t08b9ntm.js} +10 -10
- package/dist/handlers-eqsnmv95.js +41 -0
- package/dist/{heartbeat-vtesda5d.js → heartbeat-vn31xwdk.js} +23 -23
- package/dist/{hook-b2hhj6rd.js → hook-6d0zbvvt.js} +6 -5
- package/dist/{hook-8wqaszda.js → hook-cpjs8q51.js} +13 -7
- package/dist/{http-mxcqhhcs.js → http-9c6medda.js} +1085 -136
- package/dist/{index-spwxjgay.js → index-38t35h0n.js} +20 -20
- package/dist/{index-1r8wfpzs.js → index-g6k9tctj.js} +1 -1
- package/dist/{index-srg522m6.js → index-k0yt1tvq.js} +20 -20
- package/dist/{index-31ceyf42.js → index-pkma1yf8.js} +1 -1
- package/dist/{index-n1vbvta9.js → index-qhjf2htm.js} +29 -24
- package/dist/index-yqgbfgaj.js +43 -0
- package/dist/{keepalive-dsd8rtgx.js → keepalive-em0117bp.js} +4 -4
- package/dist/lead-23e7z59t.js +79 -0
- package/dist/{maintenance-jqmqwnet.js → maintenance-kbme3qy9.js} +1 -1
- package/dist/{oauth-refresh-sweep-q2c3bnvm.js → oauth-refresh-sweep-xysj4yse.js} +3 -3
- package/dist/{onboard-w4yd7s8t.js → onboard-p6q0vz97.js} +3 -3
- package/dist/{opencode-adapter-9tppvxar.js → opencode-adapter-esv3k9yn.js} +7 -7
- package/dist/{otel-impl-f36byzbd.js → otel-impl-54qhqh1z.js} +2 -2
- package/dist/{pi-mono-adapter-tqsge1ay.js → pi-mono-adapter-mymbwhj0.js} +41 -14
- package/dist/{pricing-refresh-xvak5p4v.js → pricing-refresh-yb75wv33.js} +3 -3
- package/dist/{rbac-roles-gfn30fb8.js → rbac-roles-1vp2mw4p.js} +2 -2
- package/dist/{rbac-roles-682k2mt5.js → rbac-roles-dexhc9x8.js} +1 -1
- package/dist/render-v2-tjay5hhf.js +16 -0
- package/dist/{resume-2dvq348g.js → resume-8ayf37g5.js} +11 -9
- package/dist/{runtime-bt5pkpm1.js → runtime-hqe5cqcd.js} +1 -1
- package/dist/{session-access-7f21ekq9.js → session-access-fdhp987s.js} +4 -4
- package/dist/session-source-backfill-qgtc86z1.js +110 -0
- package/dist/{setup-epffxtbk.js → setup-4rrmqcq3.js} +1 -1
- package/dist/{virtual-modules-mnfbh5mr.js → virtual-modules-52km00d6.js} +2 -2
- package/dist/{work-objects-xz1a5rqk.js → work-objects-p28fgd70.js} +23 -23
- package/dist/worker-e3w7vye1.js +79 -0
- package/dist/{x-pbpezr0s.js → x-wxrzvzx8.js} +2 -2
- package/openapi.json +1323 -16
- package/package.json +5 -5
- package/plugin/commands/create-pr.md +2 -2
- package/plugin/pi-skills/create-pr/SKILL.md +2 -2
- package/src/assets/source-key.ts +95 -0
- package/src/be/db/api-keys.ts +80 -0
- package/src/be/db/approvals.ts +48 -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 +21 -6
- package/src/be/db/workflow-idempotency.ts +183 -0
- package/src/be/db/workflow-runs.ts +500 -7
- package/src/be/db-retention.ts +61 -6
- package/src/be/db.ts +126 -131
- package/src/be/memory/providers/openai-embedding.ts +5 -1
- package/src/be/migrations/242_workflow_idempotency_keys.sql +40 -0
- package/src/be/migrations/244_api_key_validation.sql +4 -0
- package/src/be/onboarding.ts +4 -0
- package/src/be/scripts/typecheck.ts +1 -1
- package/src/be/sealed-json.ts +1 -0
- package/src/be/seed-scripts/catalog/compound-insights.inline.ts +2 -2
- package/src/be/seed-scripts/catalog/compound-insights.ts +2 -2
- package/src/be/seed-skills/bundled-files.generated.json +1 -1
- package/src/be/session-source-backfill.ts +163 -0
- package/src/be/sql-connections.ts +7 -1
- package/src/be/swarm-config-guard.ts +3 -0
- package/src/be/users.ts +9 -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/controller.ts +33 -0
- package/src/commands/credential-refresh.ts +76 -1
- package/src/commands/credential-wait.ts +2 -2
- package/src/commands/provider-credentials.ts +210 -170
- package/src/commands/runner.ts +31 -6
- package/src/extensions/apply-task-create.ts +64 -11
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +30 -0
- package/src/extensions/dispatcher.ts +4 -0
- package/src/extensions/post-bridge.ts +8 -0
- package/src/hooks/context-bound-note.ts +196 -0
- package/src/hooks/hook.ts +12 -0
- package/src/http/all-routes.ts +2 -0
- package/src/http/api-keys.ts +53 -0
- 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 +49 -17
- package/src/http/core.ts +6 -0
- package/src/http/fleet-benchmark.ts +5 -2
- package/src/http/idempotency-key.ts +117 -0
- package/src/http/index.ts +11 -0
- package/src/http/integrations.ts +5 -1
- package/src/http/kv.ts +8 -3
- package/src/http/onboarding.ts +6 -1
- package/src/http/pool-keys.ts +354 -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 +207 -33
- package/src/integrations/chat-sdk/inbound.ts +27 -0
- package/src/kv-reserved-namespaces.ts +19 -0
- package/src/prompts/context-bound-note.ts +48 -0
- package/src/prompts/session-templates.ts +25 -1
- package/src/providers/acp-targets.ts +1 -1
- package/src/providers/amp-adapter.ts +30 -8
- package/src/providers/claude-adapter.ts +34 -5
- package/src/providers/claude-managed-adapter.ts +42 -9
- package/src/providers/claude-sdk-session.ts +10 -0
- package/src/providers/claude-session-events.ts +13 -0
- package/src/providers/codex-adapter.ts +83 -11
- package/src/providers/opencode-adapter.ts +18 -0
- package/src/providers/pi-mono-adapter.ts +35 -0
- package/src/runtime-providers/docker.ts +6 -2
- package/src/scheduler/scheduler.ts +3 -2
- package/src/scripts-runtime/swarm-sdk.ts +7 -1
- package/src/scripts-runtime/types/stdlib.d.ts +1 -1
- package/src/scripts-runtime/types/swarm-extension.d.ts +52 -8
- package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -1
- 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/amp-adapter.test.ts +97 -0
- package/src/tests/api-keys-validation.test.ts +170 -0
- package/src/tests/approval-requests.test.ts +10 -7
- package/src/tests/approval-sweeps.test.ts +12 -6
- package/src/tests/base-prompt.test.ts +1 -1
- package/src/tests/claude-hook-output.test.ts +79 -1
- package/src/tests/claude-managed-adapter.test.ts +321 -0
- package/src/tests/claude-routes-e2e.test.ts +7 -2
- package/src/tests/codex-adapter.test.ts +163 -0
- 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/config-pool-keys.test.ts +533 -0
- package/src/tests/context-bound-note.test.ts +269 -0
- package/src/tests/credential-check.test.ts +290 -2
- 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/extensions-pre-task.test.ts +325 -0
- package/src/tests/fetch-resolved-env.test.ts +17 -0
- package/src/tests/fixtures/extensions/record-blocks/hooks.ts +10 -0
- package/src/tests/fixtures/extensions/record-blocks/manifest.json +7 -0
- 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/fixtures/fake-amp.ts +22 -8
- package/src/tests/fixtures/held-template-render.ts +42 -0
- package/src/tests/fixtures/workflow-idempotency-trigger.child.ts +73 -0
- package/src/tests/fleet-benchmark.test.ts +8 -0
- package/src/tests/get-workflow-run-tool.test.ts +98 -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/onboarding.test.ts +108 -1
- package/src/tests/openai-embedding-credentials.test.ts +35 -0
- package/src/tests/openapi-route-headers.test.ts +2 -0
- package/src/tests/opencode-adapter.test.ts +134 -0
- package/src/tests/openrouter-attribution-call-sites.test.ts +2 -2
- package/src/tests/org-website.test.ts +36 -0
- package/src/tests/pi-mono-adapter.test.ts +105 -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/runner-push-delivery.test.ts +25 -0
- package/src/tests/runtime-controller-liveness.test.ts +133 -0
- package/src/tests/runtime-providers-docker.test.ts +2 -0
- package/src/tests/script-connections.test.ts +5 -0
- package/src/tests/script-run-config-scope.test.ts +91 -3
- package/src/tests/script-runs-retention.test.ts +212 -0
- package/src/tests/sdk-allowlist.test.ts +39 -0
- package/src/tests/session-logs.test.ts +70 -0
- 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 +336 -2
- package/src/tests/slack-inbound-files.test.ts +103 -23
- package/src/tests/status.test.ts +101 -18
- 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/used-by-route.test.ts +11 -0
- package/src/tests/workflow-cascade-cancel.test.ts +1341 -0
- package/src/tests/workflow-foreach-supersede-retry.test.ts +488 -0
- package/src/tests/workflow-http-v2.test.ts +131 -0
- package/src/tests/workflow-idempotency.test.ts +915 -0
- package/src/tests/workflow-step-order.test.ts +92 -0
- package/src/tests/workflow-sub-workflow.test.ts +30 -0
- package/src/tools/get-tasks.ts +8 -0
- package/src/tools/kv/kv-read-auth.ts +4 -2
- package/src/tools/task-action.ts +1 -1
- package/src/tools/workflows/cancel-workflow-run.ts +1 -1
- package/src/tools/workflows/get-workflow-run.ts +14 -4
- package/src/types.ts +24 -0
- package/src/utils/company-profile.ts +51 -1
- package/src/utils/context-window.ts +8 -6
- package/src/utils/credentials.ts +5 -2
- 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/approval-cancel.ts +15 -13
- package/src/workflows/approval-notifications.ts +5 -1
- package/src/workflows/checkpoint.ts +6 -3
- package/src/workflows/engine.ts +30 -8
- package/src/workflows/executors/foreach.ts +2 -2
- package/src/workflows/executors/sub-workflow.ts +8 -1
- package/src/workflows/foreach-driver.ts +10 -3
- package/src/workflows/foreach-join.ts +29 -6
- package/src/workflows/recovery.ts +6 -1
- package/src/workflows/resume.ts +168 -56
- package/src/workflows/retry-poller.ts +49 -3
- package/src/workflows/task-step-routing.ts +29 -7
- package/templates/ai-toolbox.manifest.json +8 -8
- package/templates/community/open-prompt-showreel/PLAYBOOK.md +5 -6
- package/templates/community/open-prompt-showreel/lead-prompt.md +1 -1
- package/templates/community/open-prompt-showreel/skills/open-prompt-showreel/SKILL.md +4 -4
- package/templates/skills/agent-browser/SKILL.md +4 -4
- package/templates/skills/agent-browser/content.md +4 -4
- package/templates/skills/code-quality/config.json +1 -1
- package/templates/skills/code-quality/content.md +2 -2
- package/templates/skills/delegate-work/content.md +29 -25
- package/templates/skills/delegate-work/files/scripts/codex-exec.sh +5 -5
- package/templates/skills/qa/content.md +2 -2
- package/templates/skills/researching/content.md +4 -0
- package/templates/skills/swarm-scripts/SKILL.md +1 -0
- package/templates/skills/swarm-scripts/content.md +1 -0
- package/templates/skills/work-on-task/SKILL.md +1 -1
- package/templates/skills/work-on-task/content.md +1 -1
- package/dist/cli-7421jzsw.js +0 -84
- package/dist/cli-e45e4m6b.js +0 -413
- package/dist/cli-zw88zjba.js +0 -307
- package/dist/handlers-19xr6dnz.js +0 -41
- package/dist/index-kw2mmat7.js +0 -43
- package/dist/lead-zyerk1x2.js +0 -79
- package/dist/render-v2-hgytz8nn.js +0 -12
- package/dist/worker-42t8emjt.js +0 -79
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { afterEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
import { checkProviderPoolKey } from "../utils/provider-key-probes";
|
|
3
|
+
|
|
4
|
+
describe("API pool key probes", () => {
|
|
5
|
+
const realFetch = globalThis.fetch;
|
|
6
|
+
const originalNodeEnv = process.env.NODE_ENV;
|
|
7
|
+
const originalPrivateUrls = process.env.ALLOW_PRIVATE_NETWORK_URLS;
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
globalThis.fetch = realFetch;
|
|
11
|
+
if (originalNodeEnv === undefined) delete process.env.NODE_ENV;
|
|
12
|
+
else process.env.NODE_ENV = originalNodeEnv;
|
|
13
|
+
if (originalPrivateUrls === undefined) delete process.env.ALLOW_PRIVATE_NETWORK_URLS;
|
|
14
|
+
else process.env.ALLOW_PRIVATE_NETWORK_URLS = originalPrivateUrls;
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("uses the supplied OpenRouter URL and forbids redirects", async () => {
|
|
18
|
+
const calls: Array<{ url: string; init?: RequestInit }> = [];
|
|
19
|
+
globalThis.fetch = mock(async (url: string | URL | Request, init?: RequestInit) => {
|
|
20
|
+
calls.push({ url: String(url), init });
|
|
21
|
+
return new Response("{}", { status: calls.length === 1 ? 404 : 200 });
|
|
22
|
+
}) as typeof fetch;
|
|
23
|
+
expect(
|
|
24
|
+
await checkProviderPoolKey("OPENROUTER_API_KEY", "fake-abcde", {
|
|
25
|
+
OPENROUTER_BASE_URL: "https://gateway.example/v1/",
|
|
26
|
+
}),
|
|
27
|
+
).toMatchObject({ ok: true });
|
|
28
|
+
expect(calls.map((call) => call.url)).toEqual([
|
|
29
|
+
"https://gateway.example/v1/key",
|
|
30
|
+
"https://gateway.example/v1/models",
|
|
31
|
+
]);
|
|
32
|
+
expect(calls.every((call) => call.init?.redirect === "manual")).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("rejects a private registry URL before sending credentials", async () => {
|
|
36
|
+
process.env.NODE_ENV = "production";
|
|
37
|
+
delete process.env.ALLOW_PRIVATE_NETWORK_URLS;
|
|
38
|
+
const fetch = mock(async () => new Response("{}"));
|
|
39
|
+
globalThis.fetch = fetch as unknown as typeof globalThis.fetch;
|
|
40
|
+
const result = await checkProviderPoolKey("NAN_API_KEY", "fake-abcde", {
|
|
41
|
+
NAN_BASE_URL: "https://127.0.0.1/v1",
|
|
42
|
+
});
|
|
43
|
+
expect(result.ok).toBe(false);
|
|
44
|
+
expect(result.httpStatus).toBeUndefined();
|
|
45
|
+
expect(fetch).not.toHaveBeenCalled();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("supports registry aliases and configured URLs", async () => {
|
|
49
|
+
const fetch = mock(async (url: string | URL | Request, init?: RequestInit) => {
|
|
50
|
+
expect(String(url)).toBe("https://gateway.example/v1/models");
|
|
51
|
+
expect(init?.headers).toEqual({ Authorization: "Bearer fake-abcde" });
|
|
52
|
+
expect(init?.redirect).toBe("manual");
|
|
53
|
+
return new Response("Unauthorized", { status: 401 });
|
|
54
|
+
});
|
|
55
|
+
globalThis.fetch = fetch as typeof globalThis.fetch;
|
|
56
|
+
expect(
|
|
57
|
+
await checkProviderPoolKey("HELMCODE_API_NAN", "fake-abcde", {
|
|
58
|
+
NAN_BASE_URL: "https://gateway.example/v1",
|
|
59
|
+
}),
|
|
60
|
+
).toMatchObject({ ok: false, httpStatus: 401 });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test.each([
|
|
64
|
+
[200, "{}", true, false],
|
|
65
|
+
[403, '{"error":{"type":"permission_error"}}', true, false],
|
|
66
|
+
[403, '{"error":{"type":"other"}}', false, false],
|
|
67
|
+
[401, "Unauthorized", false, false],
|
|
68
|
+
[429, "Throttled", true, true],
|
|
69
|
+
[503, "Unavailable", true, true],
|
|
70
|
+
])("preserves Claude OAuth semantics for HTTP %i", async (status, body, ok, skipped) => {
|
|
71
|
+
globalThis.fetch = mock(async (url: string | URL | Request, init?: RequestInit) => {
|
|
72
|
+
expect(String(url)).toBe("https://api.anthropic.com/api/oauth/profile");
|
|
73
|
+
expect(init?.redirect).toBe("manual");
|
|
74
|
+
expect(init?.signal).toBeInstanceOf(AbortSignal);
|
|
75
|
+
return new Response(body, { status });
|
|
76
|
+
}) as typeof fetch;
|
|
77
|
+
const result = await checkProviderPoolKey("CLAUDE_CODE_OAUTH_TOKEN", "fake-abcde", {});
|
|
78
|
+
expect(result.ok).toBe(ok);
|
|
79
|
+
expect(result.skipped === true).toBe(skipped);
|
|
80
|
+
if (!ok) expect(result.httpStatus).toBe(status);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("checks an Anthropic key at its gateway, keeping the URL path prefix", async () => {
|
|
84
|
+
const calls: Array<{ url: string; init?: RequestInit }> = [];
|
|
85
|
+
globalThis.fetch = mock(async (url: string | URL | Request, init?: RequestInit) => {
|
|
86
|
+
calls.push({ url: String(url), init });
|
|
87
|
+
return new Response("Unauthorized", { status: 401 });
|
|
88
|
+
}) as unknown as typeof fetch;
|
|
89
|
+
const result = await checkProviderPoolKey("ANTHROPIC_API_KEY", "fake-abcde", {
|
|
90
|
+
ANTHROPIC_BASE_URL: "https://gateway.example/api",
|
|
91
|
+
});
|
|
92
|
+
expect(result).toMatchObject({ ok: false, httpStatus: 401 });
|
|
93
|
+
expect(calls.map((call) => call.url)).toEqual(["https://gateway.example/api/v1/models"]);
|
|
94
|
+
expect(new Headers(calls[0]?.init?.headers).get("x-api-key")).toBe("fake-abcde");
|
|
95
|
+
expect(calls[0]?.init?.redirect).toBe("manual");
|
|
96
|
+
|
|
97
|
+
globalThis.fetch = mock(async () => new Response("{}")) as unknown as typeof fetch;
|
|
98
|
+
expect(
|
|
99
|
+
await checkProviderPoolKey("ANTHROPIC_API_KEY", "fake-abcde", {
|
|
100
|
+
ANTHROPIC_BASE_URL: "https://gateway.example/api",
|
|
101
|
+
}),
|
|
102
|
+
).toMatchObject({ ok: true });
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("a gateway that lists no models leaves the Anthropic key unchecked", async () => {
|
|
106
|
+
globalThis.fetch = mock(
|
|
107
|
+
async () => new Response("Not found", { status: 404 }),
|
|
108
|
+
) as unknown as typeof fetch;
|
|
109
|
+
expect(
|
|
110
|
+
await checkProviderPoolKey("ANTHROPIC_API_KEY", "fake-abcde", {
|
|
111
|
+
ANTHROPIC_BASE_URL: "https://gateway.example",
|
|
112
|
+
}),
|
|
113
|
+
).toMatchObject({ ok: true, skipped: true });
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("an Anthropic key stays on api.anthropic.com when the gateway uses ANTHROPIC_AUTH_TOKEN", async () => {
|
|
117
|
+
const urls: string[] = [];
|
|
118
|
+
globalThis.fetch = mock(async (url: string | URL | Request) => {
|
|
119
|
+
urls.push(String(url));
|
|
120
|
+
return new Response("{}");
|
|
121
|
+
}) as unknown as typeof fetch;
|
|
122
|
+
expect(
|
|
123
|
+
await checkProviderPoolKey("ANTHROPIC_API_KEY", "fake-abcde", {
|
|
124
|
+
ANTHROPIC_BASE_URL: "https://gateway.example",
|
|
125
|
+
ANTHROPIC_AUTH_TOKEN: "gateway-token",
|
|
126
|
+
}),
|
|
127
|
+
).toMatchObject({ ok: true });
|
|
128
|
+
expect(urls).toEqual(["https://api.anthropic.com/v1/models"]);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("a private Anthropic gateway gets no credentials and no rejection", async () => {
|
|
132
|
+
process.env.NODE_ENV = "production";
|
|
133
|
+
delete process.env.ALLOW_PRIVATE_NETWORK_URLS;
|
|
134
|
+
const fetch = mock(async () => new Response("{}"));
|
|
135
|
+
globalThis.fetch = fetch as unknown as typeof globalThis.fetch;
|
|
136
|
+
const result = await checkProviderPoolKey("ANTHROPIC_API_KEY", "fake-abcde", {
|
|
137
|
+
ANTHROPIC_BASE_URL: "http://127.0.0.1:4000",
|
|
138
|
+
});
|
|
139
|
+
expect(result.ok).toBe(false);
|
|
140
|
+
expect(result.httpStatus).toBeUndefined();
|
|
141
|
+
expect(fetch).not.toHaveBeenCalled();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("skips keys without an API-safe probe", async () => {
|
|
145
|
+
const fetch = mock(async () => new Response("{}"));
|
|
146
|
+
globalThis.fetch = fetch as unknown as typeof globalThis.fetch;
|
|
147
|
+
for (const keyType of ["DEVIN_API_KEY", "AMP_API_KEY", "CURSOR_API_KEY"]) {
|
|
148
|
+
expect(await checkProviderPoolKey(keyType, "fake-abcde", {})).toMatchObject({
|
|
149
|
+
ok: true,
|
|
150
|
+
skipped: true,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
expect(fetch).not.toHaveBeenCalled();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("reports network errors without exposing the submitted key", async () => {
|
|
157
|
+
globalThis.fetch = mock(async () => {
|
|
158
|
+
throw new Error("Rejected fake-abcde");
|
|
159
|
+
}) as unknown as typeof fetch;
|
|
160
|
+
const result = await checkProviderPoolKey("OPENAI_API_KEY", "fake-abcde", {});
|
|
161
|
+
expect(result.ok).toBe(false);
|
|
162
|
+
expect(result.error).not.toContain("fake-abcde");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test.each([
|
|
166
|
+
"ANTHROPIC_API_KEY",
|
|
167
|
+
"OPENAI_API_KEY",
|
|
168
|
+
"OPENROUTER_API_KEY",
|
|
169
|
+
"NAN_API_KEY",
|
|
170
|
+
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
171
|
+
])("redacts a long echoed %s before truncating the provider error", async (keyType) => {
|
|
172
|
+
const apiKey = "Z".repeat(240);
|
|
173
|
+
globalThis.fetch = mock(
|
|
174
|
+
async () => new Response(`Rejected ${apiKey}`, { status: 403 }),
|
|
175
|
+
) as unknown as typeof fetch;
|
|
176
|
+
const result = await checkProviderPoolKey(keyType, apiKey, {});
|
|
177
|
+
expect(result).toMatchObject({ ok: false, httpStatus: 403 });
|
|
178
|
+
expect(result.error).toContain("[REDACTED]");
|
|
179
|
+
expect(result.error).not.toContain(apiKey.slice(0, 40));
|
|
180
|
+
});
|
|
181
|
+
});
|
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
import { expect, test } from "bun:test";
|
|
2
2
|
import { closeSync, openSync } from "node:fs";
|
|
3
3
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
4
|
+
import { createServer } from "node:net";
|
|
4
5
|
import { tmpdir } from "node:os";
|
|
5
6
|
import { join } from "node:path";
|
|
6
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
makeScratchDir,
|
|
9
|
+
removeScratchDir,
|
|
10
|
+
type SwarmServer,
|
|
11
|
+
spawnSwarmServer,
|
|
12
|
+
waitForListen,
|
|
13
|
+
} from "./rbac-e2e-helpers";
|
|
14
|
+
import {
|
|
15
|
+
getFreePort,
|
|
16
|
+
listenOnFreePort,
|
|
17
|
+
PortInUseError,
|
|
18
|
+
SERVER_BOOT_HOOK_TIMEOUT_MS,
|
|
19
|
+
startOnFreePort,
|
|
20
|
+
} from "./test-net";
|
|
7
21
|
|
|
8
22
|
test("waitForListen includes a bounded server log when the process exits early", async () => {
|
|
9
23
|
const dir = await mkdtemp(join(tmpdir(), "rbac-e2e-helper-"));
|
|
@@ -51,3 +65,76 @@ test("waitForListen includes a bounded server log when the process exits early",
|
|
|
51
65
|
await rm(dir, { recursive: true, force: true });
|
|
52
66
|
}
|
|
53
67
|
});
|
|
68
|
+
|
|
69
|
+
test("startOnFreePort retries only PortInUseError, up to its attempt bound", async () => {
|
|
70
|
+
const ports = [41001, 41002, 41003, 41004];
|
|
71
|
+
const getPort = async () => ports.shift() ?? 0;
|
|
72
|
+
const tried: number[] = [];
|
|
73
|
+
|
|
74
|
+
const port = await startOnFreePort(
|
|
75
|
+
async (p) => {
|
|
76
|
+
tried.push(p);
|
|
77
|
+
if (p === 41001) throw new PortInUseError("taken");
|
|
78
|
+
return p;
|
|
79
|
+
},
|
|
80
|
+
{ getPort },
|
|
81
|
+
);
|
|
82
|
+
expect(port).toBe(41002);
|
|
83
|
+
expect(tried).toEqual([41001, 41002]);
|
|
84
|
+
|
|
85
|
+
const other = await startOnFreePort(
|
|
86
|
+
async () => {
|
|
87
|
+
throw new Error("boot crashed");
|
|
88
|
+
},
|
|
89
|
+
{ getPort },
|
|
90
|
+
).catch((error: unknown) => error);
|
|
91
|
+
expect((other as Error).message).toBe("boot crashed");
|
|
92
|
+
expect(ports).toEqual([41004]);
|
|
93
|
+
|
|
94
|
+
let calls = 0;
|
|
95
|
+
const exhausted = await startOnFreePort(
|
|
96
|
+
async () => {
|
|
97
|
+
calls++;
|
|
98
|
+
throw new PortInUseError(`taken ${calls}`);
|
|
99
|
+
},
|
|
100
|
+
{ getPort: async () => 0 },
|
|
101
|
+
).catch((error: unknown) => error);
|
|
102
|
+
expect(exhausted).toBeInstanceOf(PortInUseError);
|
|
103
|
+
expect((exhausted as Error).message).toBe("taken 3");
|
|
104
|
+
expect(calls).toBe(3);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test(
|
|
108
|
+
"spawnSwarmServer starts again on a new port when its first port is taken",
|
|
109
|
+
async () => {
|
|
110
|
+
const dir = await makeScratchDir();
|
|
111
|
+
const holder = createServer();
|
|
112
|
+
const heldPort = await listenOnFreePort(holder);
|
|
113
|
+
let server: SwarmServer | undefined;
|
|
114
|
+
try {
|
|
115
|
+
const offered: number[] = [];
|
|
116
|
+
server = await spawnSwarmServer({
|
|
117
|
+
dbPath: join(dir, "test.sqlite"),
|
|
118
|
+
logPath: join(dir, "server.log"),
|
|
119
|
+
getPort: async () => {
|
|
120
|
+
const port = offered.length === 0 ? heldPort : await getFreePort();
|
|
121
|
+
offered.push(port);
|
|
122
|
+
return port;
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
expect(offered).toHaveLength(2);
|
|
127
|
+
expect(offered[0]).toBe(heldPort);
|
|
128
|
+
expect(server.port).toBe(offered[1]);
|
|
129
|
+
expect(server.port).not.toBe(heldPort);
|
|
130
|
+
expect((await fetch(`${server.base}/docs`)).ok).toBe(true);
|
|
131
|
+
const log = await Bun.file(server.logPath).text();
|
|
132
|
+
expect(log).toContain(`Is port ${heldPort} in use?`);
|
|
133
|
+
} finally {
|
|
134
|
+
await server?.stop();
|
|
135
|
+
holder.close();
|
|
136
|
+
await removeScratchDir(dir);
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
SERVER_BOOT_HOOK_TIMEOUT_MS * 2,
|
|
140
|
+
);
|
|
@@ -14,7 +14,7 @@ import { openSync } from "node:fs";
|
|
|
14
14
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
15
15
|
import { tmpdir } from "node:os";
|
|
16
16
|
import { dirname, join } from "node:path";
|
|
17
|
-
import { getFreePort } from "./test-net";
|
|
17
|
+
import { getFreePort, PORT_IN_USE_LOG, PortInUseError, startOnFreePort } from "./test-net";
|
|
18
18
|
|
|
19
19
|
// Re-exported for the suites that imported it from here before test-net.ts existed.
|
|
20
20
|
export { getFreePort };
|
|
@@ -50,6 +50,9 @@ export async function removeScratchDir(dir: string): Promise<void> {
|
|
|
50
50
|
* Spawn `bun src/http.ts` against `dbPath` on a free port. Integrations are
|
|
51
51
|
* disabled; both API-key env vars are pinned so a developer's shell env can't
|
|
52
52
|
* leak in (AGENT_SWARM_API_KEY takes precedence in getApiKey()).
|
|
53
|
+
*
|
|
54
|
+
* With waitForListen, a child that exits because another process took its
|
|
55
|
+
* port during boot is started again on a new port (startOnFreePort).
|
|
53
56
|
*/
|
|
54
57
|
export async function spawnSwarmServer(opts: {
|
|
55
58
|
dbPath: string;
|
|
@@ -57,9 +60,38 @@ export async function spawnSwarmServer(opts: {
|
|
|
57
60
|
env?: Record<string, string>;
|
|
58
61
|
/** Poll /docs until the server accepts requests (default true). */
|
|
59
62
|
waitForListen?: boolean;
|
|
63
|
+
/** Port source for each attempt (default getFreePort). Tests use it to force a taken port. */
|
|
64
|
+
getPort?: () => Promise<number>;
|
|
60
65
|
}): Promise<SwarmServer> {
|
|
61
|
-
const port = await getFreePort();
|
|
62
66
|
const logFd = openSync(opts.logPath, "a");
|
|
67
|
+
return startOnFreePort(
|
|
68
|
+
async (port) => {
|
|
69
|
+
const log = Bun.file(opts.logPath);
|
|
70
|
+
const logStart = (await log.exists()) ? log.size : 0;
|
|
71
|
+
const server = launchSwarmServer(opts, port, logFd);
|
|
72
|
+
if (opts.waitForListen === false) return server;
|
|
73
|
+
try {
|
|
74
|
+
await waitForListen(server);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (server.proc.exitCode !== null) {
|
|
77
|
+
const attemptLog = await Bun.file(opts.logPath).slice(logStart).text();
|
|
78
|
+
if (PORT_IN_USE_LOG.test(attemptLog)) {
|
|
79
|
+
throw new PortInUseError((error as Error).message, { cause: error });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
return server;
|
|
85
|
+
},
|
|
86
|
+
{ getPort: opts.getPort },
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function launchSwarmServer(
|
|
91
|
+
opts: { dbPath: string; logPath: string; env?: Record<string, string> },
|
|
92
|
+
port: number,
|
|
93
|
+
logFd: number,
|
|
94
|
+
): SwarmServer {
|
|
63
95
|
const proc = Bun.spawn(["bun", "src/http.ts"], {
|
|
64
96
|
cwd: REPO_ROOT,
|
|
65
97
|
stdout: logFd,
|
|
@@ -93,12 +125,10 @@ export async function spawnSwarmServer(opts: {
|
|
|
93
125
|
...opts.env,
|
|
94
126
|
},
|
|
95
127
|
});
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const server: SwarmServer = {
|
|
128
|
+
return {
|
|
99
129
|
proc,
|
|
100
130
|
port,
|
|
101
|
-
base
|
|
131
|
+
base: `http://localhost:${port}`,
|
|
102
132
|
dbPath: opts.dbPath,
|
|
103
133
|
logPath: opts.logPath,
|
|
104
134
|
async stop() {
|
|
@@ -107,11 +137,6 @@ export async function spawnSwarmServer(opts: {
|
|
|
107
137
|
return proc.exitCode;
|
|
108
138
|
},
|
|
109
139
|
};
|
|
110
|
-
|
|
111
|
-
if (opts.waitForListen !== false) {
|
|
112
|
-
await waitForListen(server);
|
|
113
|
-
}
|
|
114
|
-
return server;
|
|
115
140
|
}
|
|
116
141
|
|
|
117
142
|
export async function waitForListen(server: SwarmServer, deadlineMs = 90_000): Promise<void> {
|
|
@@ -139,6 +139,31 @@ async function nudge(kind: WorkerNudge["kind"], taskId?: string) {
|
|
|
139
139
|
expect((await waiting)?.kind).toBe(kind);
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
test("shutdown stops an open poll window before it can claim a resume", async () => {
|
|
143
|
+
let shuttingDown = false;
|
|
144
|
+
// The second poll would claim the resume created during shutdown.
|
|
145
|
+
pollTriggerAfter = 2;
|
|
146
|
+
const sleep = spyOn(Bun, "sleep").mockImplementation(async () => {
|
|
147
|
+
expect(requests.filter((r) => r === "GET /api/poll")).toHaveLength(1);
|
|
148
|
+
shuttingDown = true;
|
|
149
|
+
});
|
|
150
|
+
try {
|
|
151
|
+
const trigger = await pollForTrigger({
|
|
152
|
+
apiUrl,
|
|
153
|
+
apiKey,
|
|
154
|
+
agentId,
|
|
155
|
+
pollInterval: 2,
|
|
156
|
+
pollTimeout: 5000,
|
|
157
|
+
shouldStop: () => shuttingDown,
|
|
158
|
+
});
|
|
159
|
+
expect(shuttingDown).toBe(true);
|
|
160
|
+
expect(trigger).toBeNull();
|
|
161
|
+
expect(requests.filter((r) => r === "GET /api/poll")).toHaveLength(1);
|
|
162
|
+
} finally {
|
|
163
|
+
sleep.mockRestore();
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
142
167
|
async function poll(now: number, mode: DeliveryMode = "push") {
|
|
143
168
|
const checks = delivery.checks(mode, now);
|
|
144
169
|
const schedule = delivery.schedule({
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { afterEach, expect, spyOn, test } from "bun:test";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { createServer as createHttpServer } from "node:http";
|
|
4
|
+
import { createServer as createNetServer, type Socket } from "node:net";
|
|
5
|
+
import { WebSocketServer } from "ws";
|
|
6
|
+
import { startControllerClient } from "../commands/controller";
|
|
7
|
+
import { CONTROLLER_PROTOCOL_VERSION } from "../runtime-providers/controller-protocol";
|
|
8
|
+
import type { RuntimeProvider } from "../runtime-providers/types";
|
|
9
|
+
import { listenOnFreePort } from "./test-net";
|
|
10
|
+
|
|
11
|
+
const LIVENESS_MS = 300;
|
|
12
|
+
const WELCOME = JSON.stringify({
|
|
13
|
+
type: "welcome",
|
|
14
|
+
controllerId: "controller-1",
|
|
15
|
+
version: CONTROLLER_PROTOCOL_VERSION,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const provider = {
|
|
19
|
+
kind: "docker",
|
|
20
|
+
traits: { canProvision: true, ephemeralDisk: true, supportsPause: false },
|
|
21
|
+
checkConfig: () => ({ ready: true, missing: [] }),
|
|
22
|
+
} as unknown as RuntimeProvider;
|
|
23
|
+
|
|
24
|
+
const cleanups: Array<() => void> = [];
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
for (const cleanup of cleanups.splice(0).reverse()) cleanup();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// A raw TCP WebSocket peer. Under Bun, `ws` maps to a native shim that always answers
|
|
30
|
+
// pings, so it cannot model a dead link. This peer completes the handshake, sends
|
|
31
|
+
// welcome, and then never answers a frame: what a cut network looks like to the client.
|
|
32
|
+
async function startRawServer(options: { pingEveryMs?: number } = {}) {
|
|
33
|
+
let connections = 0;
|
|
34
|
+
const sockets: Socket[] = [];
|
|
35
|
+
const server = createNetServer((socket) => {
|
|
36
|
+
sockets.push(socket);
|
|
37
|
+
socket.once("data", (data) => {
|
|
38
|
+
const key = /sec-websocket-key: *(\S+)/i.exec(data.toString())?.[1];
|
|
39
|
+
const accept = createHash("sha1")
|
|
40
|
+
.update(`${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`)
|
|
41
|
+
.digest("base64");
|
|
42
|
+
socket.write(
|
|
43
|
+
`HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${accept}\r\n\r\n`,
|
|
44
|
+
);
|
|
45
|
+
const payload = Buffer.from(WELCOME);
|
|
46
|
+
socket.write(Buffer.concat([Buffer.from([0x81, payload.length]), payload]));
|
|
47
|
+
connections++;
|
|
48
|
+
if (options.pingEveryMs) {
|
|
49
|
+
const timer = setInterval(
|
|
50
|
+
() => socket.write(Buffer.from([0x89, 0x00])),
|
|
51
|
+
options.pingEveryMs,
|
|
52
|
+
);
|
|
53
|
+
socket.once("close", () => clearInterval(timer));
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
socket.on("error", () => {});
|
|
57
|
+
});
|
|
58
|
+
const port = await listenOnFreePort(server);
|
|
59
|
+
cleanups.push(() => {
|
|
60
|
+
for (const socket of sockets) socket.destroy();
|
|
61
|
+
server.close();
|
|
62
|
+
});
|
|
63
|
+
return { apiUrl: `http://127.0.0.1:${port}`, connections: () => connections };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function startClient(apiUrl: string) {
|
|
67
|
+
const logSpy = spyOn(console, "log").mockImplementation(() => {});
|
|
68
|
+
const client = startControllerClient({
|
|
69
|
+
apiUrl,
|
|
70
|
+
token: "aseph_test-token",
|
|
71
|
+
provider,
|
|
72
|
+
target: {},
|
|
73
|
+
livenessTimeoutMs: LIVENESS_MS,
|
|
74
|
+
});
|
|
75
|
+
cleanups.push(() => {
|
|
76
|
+
client.stop();
|
|
77
|
+
logSpy.mockRestore();
|
|
78
|
+
});
|
|
79
|
+
return { logs: () => logSpy.mock.calls.flat().join("\n") };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function until(check: () => boolean, timeoutMs: number) {
|
|
83
|
+
const deadline = Date.now() + timeoutMs;
|
|
84
|
+
while (!check()) {
|
|
85
|
+
if (Date.now() > deadline) throw new Error("Timed out waiting for condition");
|
|
86
|
+
await Bun.sleep(20);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
test("a server that goes silent without closing makes the client reconnect", async () => {
|
|
91
|
+
const server = await startRawServer();
|
|
92
|
+
const { logs } = startClient(server.apiUrl);
|
|
93
|
+
await until(() => logs().includes("Connected as controller-1"), 2000);
|
|
94
|
+
const silentAt = Date.now();
|
|
95
|
+
// Detection within the timeout plus one check interval, then the first backoff (at most 1000ms).
|
|
96
|
+
await until(() => server.connections() === 2, LIVENESS_MS * 2 + 1500);
|
|
97
|
+
expect(logs()).toContain(`No traffic for ${LIVENESS_MS}ms`);
|
|
98
|
+
expect(logs()).toContain("Disconnected. Reconnect in");
|
|
99
|
+
expect(Date.now() - silentAt).toBeGreaterThanOrEqual(LIVENESS_MS);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("server ping frames keep an idle controller connected", async () => {
|
|
103
|
+
// Matches the API: it pings on an interval and sends no app message to an idle controller.
|
|
104
|
+
// This peer never answers the client's pings, so only the server pings count.
|
|
105
|
+
const server = await startRawServer({ pingEveryMs: LIVENESS_MS / 3 });
|
|
106
|
+
const { logs } = startClient(server.apiUrl);
|
|
107
|
+
await until(() => logs().includes("Connected as controller-1"), 2000);
|
|
108
|
+
await Bun.sleep(LIVENESS_MS * 4);
|
|
109
|
+
expect(server.connections()).toBe(1);
|
|
110
|
+
expect(logs()).not.toContain("Disconnected");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("pongs to client pings keep an idle controller connected", async () => {
|
|
114
|
+
// A healthy peer that sends nothing on its own still answers pings.
|
|
115
|
+
const http = createHttpServer();
|
|
116
|
+
const wss = new WebSocketServer({ server: http });
|
|
117
|
+
let connections = 0;
|
|
118
|
+
wss.on("connection", (ws) => {
|
|
119
|
+
connections++;
|
|
120
|
+
ws.send(WELCOME);
|
|
121
|
+
});
|
|
122
|
+
const port = await listenOnFreePort(http);
|
|
123
|
+
cleanups.push(() => {
|
|
124
|
+
for (const ws of wss.clients) ws.terminate();
|
|
125
|
+
wss.close();
|
|
126
|
+
http.close();
|
|
127
|
+
});
|
|
128
|
+
const { logs } = startClient(`http://127.0.0.1:${port}`);
|
|
129
|
+
await until(() => logs().includes("Connected as controller-1"), 2000);
|
|
130
|
+
await Bun.sleep(LIVENESS_MS * 4);
|
|
131
|
+
expect(connections).toBe(1);
|
|
132
|
+
expect(logs()).not.toContain("Disconnected");
|
|
133
|
+
});
|
|
@@ -310,6 +310,7 @@ describe("Docker runtime provider", () => {
|
|
|
310
310
|
await provider.terminate(target, handle);
|
|
311
311
|
expect(calls[0]?.init.method).toBe("DELETE");
|
|
312
312
|
expect(calls[0]?.url.searchParams.get("force")).toBe("true");
|
|
313
|
+
expect(calls[0]?.url.searchParams.get("v")).toBe("true");
|
|
313
314
|
});
|
|
314
315
|
|
|
315
316
|
test("termination succeeds when the container is already absent", async () => {
|
|
@@ -376,6 +377,7 @@ describe("Docker runtime provider", () => {
|
|
|
376
377
|
"DELETE /v1.47/containers/container-1",
|
|
377
378
|
]);
|
|
378
379
|
expect(calls[2]?.url.searchParams.get("force")).toBe("true");
|
|
380
|
+
expect(calls[2]?.url.searchParams.get("v")).toBe("true");
|
|
379
381
|
}
|
|
380
382
|
});
|
|
381
383
|
|
|
@@ -570,6 +570,9 @@ describe("script connections", () => {
|
|
|
570
570
|
markMigrationApplied(database, "151_repair_scheduled_task_required_params.sql");
|
|
571
571
|
// 152 indexes task routing context absent from this migration-112-only fixture.
|
|
572
572
|
markMigrationApplied(database, "152_routing_decisions.sql");
|
|
573
|
+
// 153 drops a column, so SQLite revalidates 144's agent_tasks triggers
|
|
574
|
+
// against this fixture's id-only agent_tasks (Bun < 1.4.4 hid the error).
|
|
575
|
+
markMigrationApplied(database, "153_deferred_task_wait_members.sql");
|
|
573
576
|
// 156 alters inbox_item_state, which this migration-112-only fixture does not create.
|
|
574
577
|
markMigrationApplied(database, "156_inbox_item_state_read_at.sql");
|
|
575
578
|
// 157 alters scheduled_tasks and slack_messages, neither of which this
|
|
@@ -648,6 +651,8 @@ describe("script connections", () => {
|
|
|
648
651
|
markMigrationApplied(database, "240_workflow_run_parent_step_attempt.sql");
|
|
649
652
|
// 241 indexes skills, which this migration-112-only fixture does not create.
|
|
650
653
|
markMigrationApplied(database, "241_skills_meta_fts.sql");
|
|
654
|
+
// 244 alters api_key_status, which this migration-112-only fixture does not create.
|
|
655
|
+
markMigrationApplied(database, "244_api_key_validation.sql");
|
|
651
656
|
|
|
652
657
|
const id = crypto.randomUUID();
|
|
653
658
|
const now = new Date().toISOString();
|