@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
|
@@ -2,9 +2,14 @@ import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:
|
|
|
2
2
|
import {
|
|
3
3
|
CREDENTIAL_RETRY_INTERVAL_MS,
|
|
4
4
|
type CredentialRefreshState,
|
|
5
|
+
credentialFingerprint,
|
|
6
|
+
KEY_RECHECK_INTERVAL_MS,
|
|
7
|
+
recheckableFailures,
|
|
5
8
|
refreshCredentialStatus,
|
|
9
|
+
reportKeyValidation,
|
|
6
10
|
} from "../commands/credential-refresh";
|
|
7
11
|
import * as credentials from "../commands/provider-credentials";
|
|
12
|
+
import { fetchResolvedEnv } from "../commands/runner";
|
|
8
13
|
import type { AgentCredStatus } from "../types";
|
|
9
14
|
|
|
10
15
|
const api = { apiUrl: "http://unused.test", apiKey: "test", agentId: crypto.randomUUID() };
|
|
@@ -27,7 +32,14 @@ describe("runner credential refresh", () => {
|
|
|
27
32
|
let send: ReturnType<typeof spyOn<typeof credentials, "sendCredStatusReport">>;
|
|
28
33
|
|
|
29
34
|
beforeEach(() => {
|
|
30
|
-
state = {
|
|
35
|
+
state = {
|
|
36
|
+
harnessProvider: "claude",
|
|
37
|
+
ready: true,
|
|
38
|
+
lastRefreshAt: 0,
|
|
39
|
+
inFlight: false,
|
|
40
|
+
fingerprint: credentialFingerprint({}),
|
|
41
|
+
rejectedKeys: 0,
|
|
42
|
+
};
|
|
31
43
|
build = spyOn(credentials, "buildCredStatusReport").mockResolvedValue(ready);
|
|
32
44
|
send = spyOn(credentials, "sendCredStatusReport").mockResolvedValue(undefined);
|
|
33
45
|
});
|
|
@@ -43,6 +55,136 @@ describe("runner credential refresh", () => {
|
|
|
43
55
|
expect(state.ready).toBe(true);
|
|
44
56
|
});
|
|
45
57
|
|
|
58
|
+
test("a changed credential fingerprint refreshes a ready worker immediately", async () => {
|
|
59
|
+
const env = { ANTHROPIC_API_KEY: "test-pool-a,test-pool-b" };
|
|
60
|
+
await refreshCredentialStatus(api, state, "claude", env, 1);
|
|
61
|
+
expect(build).toHaveBeenCalledWith("claude", env, {}, "post_task");
|
|
62
|
+
expect(state.fingerprint).toBe(credentialFingerprint(env));
|
|
63
|
+
await refreshCredentialStatus(api, state, "claude", env, 2);
|
|
64
|
+
expect(build).toHaveBeenCalledTimes(1);
|
|
65
|
+
await refreshCredentialStatus(api, state, "claude", {}, 3);
|
|
66
|
+
expect(build).toHaveBeenCalledTimes(2);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("an unchanged fingerprint does not refresh a ready worker", async () => {
|
|
70
|
+
await refreshCredentialStatus(api, state, "claude", {}, KEY_RECHECK_INTERVAL_MS);
|
|
71
|
+
expect(build).not.toHaveBeenCalled();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("fingerprints ignore environment order and unrelated values", () => {
|
|
75
|
+
expect(credentialFingerprint({ ANTHROPIC_API_KEY: "test-a", PATH: "a" })).toBe(
|
|
76
|
+
credentialFingerprint({ PATH: "b", ANTHROPIC_API_KEY: "test-a" }),
|
|
77
|
+
);
|
|
78
|
+
for (const key of ["ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_BASE_URL", "OPENAI_API_KEY"]) {
|
|
79
|
+
expect(credentialFingerprint({ [key]: "changed" })).not.toBe(credentialFingerprint({}));
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("resolved credential env preserves the full pool before selection", async () => {
|
|
84
|
+
const originalFetch = globalThis.fetch;
|
|
85
|
+
globalThis.fetch = (async (url) => {
|
|
86
|
+
if (String(url).includes("/api/config/resolved")) {
|
|
87
|
+
return Response.json({
|
|
88
|
+
configs: [{ key: "ANTHROPIC_API_KEY", value: "test-aaa11,test-bbb22" }],
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return Response.json({ availableIndices: [1], rejectedIndices: [0] });
|
|
92
|
+
}) as typeof fetch;
|
|
93
|
+
try {
|
|
94
|
+
const result = await fetchResolvedEnv(api.apiUrl, api.apiKey, api.agentId, {
|
|
95
|
+
HARNESS_PROVIDER: "claude",
|
|
96
|
+
});
|
|
97
|
+
expect(result.credentialEnv.ANTHROPIC_API_KEY).toBe("test-aaa11,test-bbb22");
|
|
98
|
+
expect(result.env.ANTHROPIC_API_KEY).toBe("test-bbb22");
|
|
99
|
+
expect(result.credentialEnv).not.toBe(result.env);
|
|
100
|
+
} finally {
|
|
101
|
+
globalThis.fetch = originalFetch;
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("rejected keys refresh after ten minutes, even when the worker is ready", async () => {
|
|
106
|
+
state.rejectedKeys = 1;
|
|
107
|
+
await refreshCredentialStatus(api, state, "claude", {}, KEY_RECHECK_INTERVAL_MS - 1);
|
|
108
|
+
expect(build).not.toHaveBeenCalled();
|
|
109
|
+
await refreshCredentialStatus(api, state, "claude", {}, KEY_RECHECK_INTERVAL_MS);
|
|
110
|
+
expect(build).toHaveBeenCalledTimes(1);
|
|
111
|
+
expect(state.rejectedKeys).toBe(0);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("a failed live test without per-key results remains eligible for recheck", async () => {
|
|
115
|
+
build.mockResolvedValueOnce({
|
|
116
|
+
...ready,
|
|
117
|
+
liveTest: { ok: false, error: "timeout", latency_ms: 1 },
|
|
118
|
+
});
|
|
119
|
+
await refreshCredentialStatus(api, state, "pi", {}, 1);
|
|
120
|
+
expect(state.rejectedKeys).toBe(1);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("refresh reports per-key results once per credential type", async () => {
|
|
124
|
+
const keys = [
|
|
125
|
+
{
|
|
126
|
+
keyType: "ANTHROPIC_API_KEY",
|
|
127
|
+
index: 0,
|
|
128
|
+
suffix: "aaa11",
|
|
129
|
+
ok: false,
|
|
130
|
+
httpStatus: 401,
|
|
131
|
+
latency_ms: 1,
|
|
132
|
+
},
|
|
133
|
+
{ keyType: "ANTHROPIC_API_KEY", index: 1, suffix: "bbb22", ok: true, latency_ms: 1 },
|
|
134
|
+
{ keyType: "OPENAI_API_KEY", index: 0, suffix: "ccc33", ok: true, latency_ms: 1 },
|
|
135
|
+
];
|
|
136
|
+
build.mockResolvedValueOnce({ ...ready, liveTest: { ok: false, latency_ms: 1, keys } });
|
|
137
|
+
const requests: Array<{ url: string; init?: RequestInit }> = [];
|
|
138
|
+
const originalFetch = globalThis.fetch;
|
|
139
|
+
globalThis.fetch = (async (url, init) => {
|
|
140
|
+
requests.push({ url: String(url), init });
|
|
141
|
+
return Response.json({ success: true });
|
|
142
|
+
}) as typeof fetch;
|
|
143
|
+
try {
|
|
144
|
+
await refreshCredentialStatus(api, state, "pi", {}, 1);
|
|
145
|
+
expect(state.rejectedKeys).toBe(1);
|
|
146
|
+
expect(requests).toHaveLength(2);
|
|
147
|
+
expect(requests[0]!.url).toBe(`${api.apiUrl}/api/keys/report-validation`);
|
|
148
|
+
expect(requests[0]!.init?.headers).toMatchObject({
|
|
149
|
+
Authorization: `Bearer ${api.apiKey}`,
|
|
150
|
+
"X-Agent-ID": api.agentId,
|
|
151
|
+
});
|
|
152
|
+
expect(JSON.parse(String(requests[0]!.init?.body))).toEqual({
|
|
153
|
+
keyType: "ANTHROPIC_API_KEY",
|
|
154
|
+
keys: [
|
|
155
|
+
{ keyIndex: 0, keySuffix: "aaa11", ok: false, httpStatus: 401 },
|
|
156
|
+
{ keyIndex: 1, keySuffix: "bbb22", ok: true },
|
|
157
|
+
],
|
|
158
|
+
});
|
|
159
|
+
expect(JSON.parse(String(requests[1]!.init?.body))).toEqual({
|
|
160
|
+
keyType: "OPENAI_API_KEY",
|
|
161
|
+
keys: [{ keyIndex: 0, keySuffix: "ccc33", ok: true }],
|
|
162
|
+
});
|
|
163
|
+
expect(requests[0]!.init?.signal).toBeInstanceOf(AbortSignal);
|
|
164
|
+
} finally {
|
|
165
|
+
globalThis.fetch = originalFetch;
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("validation reporting contains HTTP and network failures", async () => {
|
|
170
|
+
const originalFetch = globalThis.fetch;
|
|
171
|
+
const warn = spyOn(console, "warn").mockImplementation(() => {});
|
|
172
|
+
const fetchMock = mock<typeof fetch>()
|
|
173
|
+
.mockResolvedValueOnce(new Response(null, { status: 503 }))
|
|
174
|
+
.mockRejectedValueOnce(new Error("network unavailable"));
|
|
175
|
+
globalThis.fetch = fetchMock as typeof fetch;
|
|
176
|
+
try {
|
|
177
|
+
await reportKeyValidation(api, [
|
|
178
|
+
{ keyType: "ANTHROPIC_API_KEY", index: 0, suffix: "aaa11", ok: true, latency_ms: 1 },
|
|
179
|
+
{ keyType: "OPENAI_API_KEY", index: 0, suffix: "bbb22", ok: true, latency_ms: 1 },
|
|
180
|
+
]);
|
|
181
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
182
|
+
expect(warn).toHaveBeenCalledTimes(2);
|
|
183
|
+
} finally {
|
|
184
|
+
globalThis.fetch = originalFetch;
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
46
188
|
test("a failed readiness write is throttled and retried even when the snapshot is ready", async () => {
|
|
47
189
|
send.mockRejectedValueOnce(new Error("HTTP 503"));
|
|
48
190
|
await expect(refreshCredentialStatus(api, state, "pi", {}, 1)).rejects.toThrow("HTTP 503");
|
|
@@ -113,3 +255,32 @@ describe("runner credential refresh", () => {
|
|
|
113
255
|
expect(build).toHaveBeenCalledTimes(1);
|
|
114
256
|
});
|
|
115
257
|
});
|
|
258
|
+
|
|
259
|
+
describe("recheckableFailures", () => {
|
|
260
|
+
const key = (keyType: string, ok: boolean) => ({
|
|
261
|
+
keyType,
|
|
262
|
+
index: 0,
|
|
263
|
+
suffix: "abcde",
|
|
264
|
+
ok,
|
|
265
|
+
latency_ms: 1,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("a Claude subscription token never joins the periodic recheck", () => {
|
|
269
|
+
const liveTest = {
|
|
270
|
+
ok: false,
|
|
271
|
+
error: "rejected",
|
|
272
|
+
latency_ms: 1,
|
|
273
|
+
testedAt: 1,
|
|
274
|
+
keys: [key("CLAUDE_CODE_OAUTH_TOKEN", false), key("ANTHROPIC_API_KEY", false)],
|
|
275
|
+
};
|
|
276
|
+
expect(recheckableFailures(liveTest)).toBe(1);
|
|
277
|
+
expect(
|
|
278
|
+
recheckableFailures({ ...liveTest, keys: [key("CLAUDE_CODE_OAUTH_TOKEN", false)] }),
|
|
279
|
+
).toBe(0);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("a failed test without per-key results counts once", () => {
|
|
283
|
+
expect(recheckableFailures({ ok: false, error: "x", latency_ms: 1, testedAt: 1 })).toBe(1);
|
|
284
|
+
expect(recheckableFailures(null)).toBe(0);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
@@ -19,8 +19,13 @@ import {
|
|
|
19
19
|
const TEST_DB_PATH = "./test-db-retention.sqlite";
|
|
20
20
|
const NOW = new Date("2026-08-23T12:00:00.000Z");
|
|
21
21
|
const RETENTION_KEYS = DB_RETENTION_TABLES.map((table) => table.envKey);
|
|
22
|
-
/**
|
|
23
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Plain time-horizon tables. Count-based context_versions and status-filtered
|
|
24
|
+
* script_runs have their own suites.
|
|
25
|
+
*/
|
|
26
|
+
const AGE_TABLES = DB_RETENTION_TABLES.filter(
|
|
27
|
+
(table) => table.policy === "age" && !("terminalStatuses" in table),
|
|
28
|
+
);
|
|
24
29
|
type AgeTable = (typeof AGE_TABLES)[number];
|
|
25
30
|
const TUNING_KEYS = [
|
|
26
31
|
"DB_RETENTION_TICK_BUDGET_MS",
|
|
@@ -135,12 +140,13 @@ afterEach(async () => {
|
|
|
135
140
|
});
|
|
136
141
|
|
|
137
142
|
describe("DB retention", () => {
|
|
138
|
-
test("keeps the closed allowlist limited to the
|
|
143
|
+
test("keeps the closed allowlist limited to the five approved tables", () => {
|
|
139
144
|
expect(DB_RETENTION_TABLES.map((table) => table.table)).toEqual([
|
|
140
145
|
"session_logs",
|
|
141
146
|
"agent_log",
|
|
142
147
|
"events",
|
|
143
148
|
"context_versions",
|
|
149
|
+
"script_runs",
|
|
144
150
|
]);
|
|
145
151
|
});
|
|
146
152
|
|
|
@@ -529,6 +529,7 @@ value:recordKeyAuthFailure
|
|
|
529
529
|
value:recordKeyRateLimitWindows
|
|
530
530
|
value:recordKeySeatMismatch
|
|
531
531
|
value:recordKeyUsage
|
|
532
|
+
value:recordKeyValidation
|
|
532
533
|
value:recordSlackMessage
|
|
533
534
|
value:recordTaskProviderIfUnset
|
|
534
535
|
value:recordTaskPullRequestAttachments
|
|
@@ -552,6 +552,7 @@ value:recordKeyAuthFailure
|
|
|
552
552
|
value:recordKeyRateLimitWindows
|
|
553
553
|
value:recordKeySeatMismatch
|
|
554
554
|
value:recordKeyUsage
|
|
555
|
+
value:recordKeyValidation
|
|
555
556
|
value:recordSlackMessage
|
|
556
557
|
value:recordTaskProviderIfUnset
|
|
557
558
|
value:recordTaskPullRequestAttachments
|
|
@@ -684,7 +685,6 @@ STEERING_MESSAGE_SELECT
|
|
|
684
685
|
UNATTRIBUTED_USER_ID
|
|
685
686
|
VERSIONABLE_FIELDS
|
|
686
687
|
WEEK_SECONDS
|
|
687
|
-
WORKFLOW_RUN_SUMMARY_COLUMNS
|
|
688
688
|
abandonSlackOutcomeDelivery
|
|
689
689
|
acceptTask
|
|
690
690
|
assertSkillFileLimits
|
|
@@ -1097,8 +1097,6 @@ rowToUser
|
|
|
1097
1097
|
rowToUserFavorite
|
|
1098
1098
|
rowToWaitState
|
|
1099
1099
|
rowToWorkflow
|
|
1100
|
-
rowToWorkflowRun
|
|
1101
|
-
rowToWorkflowRunStep
|
|
1102
1100
|
rowToWorkflowSummary
|
|
1103
1101
|
rowToWorkflowVersion
|
|
1104
1102
|
scriptRunUpdateSets
|
|
@@ -78,7 +78,7 @@ describe("docs API index footprint", () => {
|
|
|
78
78
|
expect(tasks).toContain('"path":"/api/task-templates","method":"get"');
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
-
test("OpenAPI and generated pages keep all
|
|
81
|
+
test("OpenAPI and generated pages keep all 456 operations", async () => {
|
|
82
82
|
const spec = await Bun.file(resolve(repoRoot, "openapi.json")).json();
|
|
83
83
|
const operationKeys = [];
|
|
84
84
|
const tags = new Set();
|
|
@@ -103,8 +103,8 @@ describe("docs API index footprint", () => {
|
|
|
103
103
|
expect(spec.paths["/api/agents/{id}/runtime"]?.get).toBeDefined();
|
|
104
104
|
|
|
105
105
|
const generatedKeys = await generatedOperationKeys();
|
|
106
|
-
expect(operationKeys).toHaveLength(
|
|
107
|
-
expect(generatedKeys).toHaveLength(
|
|
106
|
+
expect(operationKeys).toHaveLength(456);
|
|
107
|
+
expect(generatedKeys).toHaveLength(456);
|
|
108
108
|
expect(new Set(generatedKeys)).toEqual(new Set(operationKeys));
|
|
109
109
|
});
|
|
110
110
|
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
createWorkflowRunStep,
|
|
15
15
|
getDbClient,
|
|
16
16
|
getKv,
|
|
17
|
+
getMostRecentTaskInThread,
|
|
17
18
|
getTaskByWorkflowRunStepId,
|
|
18
19
|
initDb,
|
|
19
20
|
startTask,
|
|
@@ -22,9 +23,12 @@ import * as dbClient from "../be/db-client";
|
|
|
22
23
|
import type { InstallExtensionArgs as ExtensionInstallBody } from "../be/extensions/db";
|
|
23
24
|
import { installExtension, listExtensionRuns } from "../be/extensions/db";
|
|
24
25
|
import { getCatalogEntry } from "../extensions/catalog";
|
|
26
|
+
import type { TaskBlockedEvent, TaskCreateEvent } from "../extensions/contract";
|
|
25
27
|
import { disableExtension, enableExtension, stopExtensionRuntime } from "../extensions/lifecycle";
|
|
28
|
+
import { initExtensionPostBridge, teardownExtensionPostBridge } from "../extensions/post-bridge";
|
|
26
29
|
import { handleTasks } from "../http/tasks";
|
|
27
30
|
import { dispatchScheduleTarget } from "../scheduler/scheduler";
|
|
31
|
+
import { bufferThreadMessage, instantFlush } from "../slack/thread-buffer";
|
|
28
32
|
import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
|
|
29
33
|
import { createWorkerTaskFollowUp } from "../tasks/worker-follow-up";
|
|
30
34
|
import { registerSendTaskTool, sendTaskHandler } from "../tools/send-task";
|
|
@@ -145,6 +149,276 @@ describe("extension task boundaries", () => {
|
|
|
145
149
|
await client.run("DELETE FROM agents");
|
|
146
150
|
});
|
|
147
151
|
|
|
152
|
+
describe("post.task.blocked", () => {
|
|
153
|
+
const events: unknown[] = [];
|
|
154
|
+
const capture = (event: unknown) => {
|
|
155
|
+
events.push(event);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
beforeAll(() => {
|
|
159
|
+
initExtensionPostBridge();
|
|
160
|
+
workflowEventBus.on("task.create_blocked", capture);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
afterAll(() => {
|
|
164
|
+
workflowEventBus.off("task.create_blocked", capture);
|
|
165
|
+
teardownExtensionPostBridge();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
beforeEach(() => {
|
|
169
|
+
events.length = 0;
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
async function waitForCounter(
|
|
173
|
+
extension: Awaited<ReturnType<typeof installAndEnable>>,
|
|
174
|
+
key: string,
|
|
175
|
+
count = 1,
|
|
176
|
+
): Promise<void> {
|
|
177
|
+
const deadline = Date.now() + 3_000;
|
|
178
|
+
while (Date.now() < deadline) {
|
|
179
|
+
const entry = await getKv(`task:agent:${extension.agentId}`, key);
|
|
180
|
+
if (entry?.value === count) return;
|
|
181
|
+
await Bun.sleep(10);
|
|
182
|
+
}
|
|
183
|
+
throw new Error(`post.task.blocked was not recorded ${count} time(s)`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function recordedBlock(
|
|
187
|
+
extension: Awaited<ReturnType<typeof installAndEnable>>,
|
|
188
|
+
count = 1,
|
|
189
|
+
): Promise<TaskBlockedEvent> {
|
|
190
|
+
await waitForCounter(extension, "ext:record-blocks:count", count);
|
|
191
|
+
return (await getKv(`task:agent:${extension.agentId}`, "ext:record-blocks:last"))
|
|
192
|
+
?.value as TaskBlockedEvent;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
test.each([
|
|
196
|
+
"rest",
|
|
197
|
+
"mcp",
|
|
198
|
+
"slack",
|
|
199
|
+
"schedule",
|
|
200
|
+
] as const)("%s delivers one blocked event with its source and requester", async (origin) => {
|
|
201
|
+
const blocker = await installAndEnable("block-tasks-from-source", { source: origin });
|
|
202
|
+
const recorder = await installAndEnable("record-blocks");
|
|
203
|
+
let requester: TaskBlockedEvent["requester"] = {};
|
|
204
|
+
let scheduleId: string | undefined;
|
|
205
|
+
|
|
206
|
+
if (origin === "rest") {
|
|
207
|
+
const user = await createUser({ name: "Blocked REST requester" });
|
|
208
|
+
requester = { userId: user.id };
|
|
209
|
+
expect(
|
|
210
|
+
(await postTask({ task: "blocked REST task", requestedByUserId: user.id })).status,
|
|
211
|
+
).toBe(422);
|
|
212
|
+
} else if (origin === "mcp") {
|
|
213
|
+
const caller = await createAgent({ name: "blocked-mcp-caller", status: "idle" });
|
|
214
|
+
requester = { agentId: caller.id };
|
|
215
|
+
const server = new McpServer({ name: "blocked-event-send", version: "1.0.0" });
|
|
216
|
+
registerSendTaskTool(server);
|
|
217
|
+
const result = await registeredSendTask(server).handler(sendTaskArgs("blocked MCP task"), {
|
|
218
|
+
sessionId: "blocked-event",
|
|
219
|
+
requestInfo: { headers: { "x-agent-id": caller.id } },
|
|
220
|
+
});
|
|
221
|
+
expect(result.isError).toBe(true);
|
|
222
|
+
} else if (origin === "slack") {
|
|
223
|
+
requester = { slackUserId: "U_BLOCKED_EVENT" };
|
|
224
|
+
await createAgent({ name: "blocked-slack-lead", isLead: true, status: "idle" });
|
|
225
|
+
bufferThreadMessage(
|
|
226
|
+
"C_BLOCKED_EVENT",
|
|
227
|
+
"1000.0001",
|
|
228
|
+
"blocked Slack task",
|
|
229
|
+
"U_BLOCKED_EVENT",
|
|
230
|
+
"1000.0002",
|
|
231
|
+
);
|
|
232
|
+
await instantFlush("C_BLOCKED_EVENT:1000.0001");
|
|
233
|
+
} else {
|
|
234
|
+
const schedule = await createScheduledTask({
|
|
235
|
+
name: "blocked-event-schedule",
|
|
236
|
+
taskTemplate: "blocked schedule task",
|
|
237
|
+
intervalMs: 60_000,
|
|
238
|
+
});
|
|
239
|
+
scheduleId = schedule.id;
|
|
240
|
+
await expect(dispatchScheduleTarget(schedule)).rejects.toThrow(
|
|
241
|
+
"Task source schedule is blocked",
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const event = await recordedBlock(recorder);
|
|
246
|
+
expect(events).toHaveLength(1);
|
|
247
|
+
expect(event).toMatchObject({
|
|
248
|
+
extension: { id: blocker.id, name: "block-tasks-from-source" },
|
|
249
|
+
origin,
|
|
250
|
+
source: origin === "rest" ? "api" : origin,
|
|
251
|
+
requester,
|
|
252
|
+
});
|
|
253
|
+
expect(event.requester).toEqual(requester);
|
|
254
|
+
if (scheduleId) expect(event.context?.scheduleId).toBe(scheduleId);
|
|
255
|
+
expect(event).not.toHaveProperty("task");
|
|
256
|
+
expect(event).not.toHaveProperty("taskId");
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("scrubs and bounds the payload, preserving correlation on each retry", async () => {
|
|
260
|
+
const secret = `ghp_${"Z".repeat(36)}`;
|
|
261
|
+
await installSource(
|
|
262
|
+
"secret-blocker",
|
|
263
|
+
`
|
|
264
|
+
import { block, type SwarmExtension } from "swarm-extension";
|
|
265
|
+
const extension: SwarmExtension = (api) => {
|
|
266
|
+
api.on("pre.task.create", () => block(${JSON.stringify(`Ticket required: ${secret}`)}));
|
|
267
|
+
};
|
|
268
|
+
export default extension;
|
|
269
|
+
`,
|
|
270
|
+
);
|
|
271
|
+
const recorder = await installAndEnable("record-blocks");
|
|
272
|
+
const context = {
|
|
273
|
+
contextKey: "github:acme/project:7",
|
|
274
|
+
scheduleId: crypto.randomUUID(),
|
|
275
|
+
workflowRunId: crypto.randomUUID(),
|
|
276
|
+
workflowRunStepId: crypto.randomUUID(),
|
|
277
|
+
parentTaskId: crypto.randomUUID(),
|
|
278
|
+
vcsRepo: "acme/project",
|
|
279
|
+
vcsNumber: 7,
|
|
280
|
+
slackChannelId: "C_CORRELATED_BLOCK",
|
|
281
|
+
slackThreadTs: "2000.0001",
|
|
282
|
+
};
|
|
283
|
+
const requester = {
|
|
284
|
+
userId: crypto.randomUUID(),
|
|
285
|
+
agentId: crypto.randomUUID(),
|
|
286
|
+
slackUserId: "U_CORRELATED_BLOCK",
|
|
287
|
+
};
|
|
288
|
+
for (const attempt of [1, 2]) {
|
|
289
|
+
await expect(
|
|
290
|
+
createTaskWithSiblingAwareness(
|
|
291
|
+
`${"x".repeat(480)} ${secret}`,
|
|
292
|
+
{
|
|
293
|
+
...context,
|
|
294
|
+
source: "github",
|
|
295
|
+
requestedByUserId: requester.userId,
|
|
296
|
+
creatorAgentId: requester.agentId,
|
|
297
|
+
slackUserId: requester.slackUserId,
|
|
298
|
+
},
|
|
299
|
+
{ origin: "webhook" },
|
|
300
|
+
),
|
|
301
|
+
).rejects.toThrow("Ticket required:");
|
|
302
|
+
const event = await recordedBlock(recorder, attempt);
|
|
303
|
+
expect(events).toHaveLength(attempt);
|
|
304
|
+
expect(event.reason).toContain("[REDACTED:");
|
|
305
|
+
expect(event.reason).not.toContain(secret);
|
|
306
|
+
expect(event.description).toHaveLength(500);
|
|
307
|
+
expect(event.description).not.toContain("ghp_");
|
|
308
|
+
expect(event.context).toEqual(context);
|
|
309
|
+
expect(event.requester).toEqual(requester);
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("a rejected model rewrite emits a block attributed to its editor", async () => {
|
|
314
|
+
const codex = await createAgent({
|
|
315
|
+
name: "blocked-rewrite-codex",
|
|
316
|
+
harnessProvider: "codex",
|
|
317
|
+
status: "idle",
|
|
318
|
+
});
|
|
319
|
+
const editor = await installSource(
|
|
320
|
+
"incompatible-model",
|
|
321
|
+
`
|
|
322
|
+
import { modify, type SwarmExtension } from "swarm-extension";
|
|
323
|
+
const extension: SwarmExtension = (api) => {
|
|
324
|
+
api.on("pre.task.create", () => modify({ model: "claude-opus-5-5" }));
|
|
325
|
+
};
|
|
326
|
+
export default extension;
|
|
327
|
+
`,
|
|
328
|
+
);
|
|
329
|
+
const recorder = await installAndEnable("record-blocks");
|
|
330
|
+
const result = await postTask({
|
|
331
|
+
task: "incompatible rewrite",
|
|
332
|
+
agentId: codex.id,
|
|
333
|
+
routingReason: "human_pinned",
|
|
334
|
+
});
|
|
335
|
+
expect(result.status).toBe(422);
|
|
336
|
+
const event = await recordedBlock(recorder);
|
|
337
|
+
expect(events).toHaveLength(1);
|
|
338
|
+
expect(event.extension).toEqual({ id: editor.id, name: "incompatible-model" });
|
|
339
|
+
expect(event.reason).toContain("rewrite");
|
|
340
|
+
expect(event.reason).toContain("codex");
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test("the creator is skipped while the blocking extension receives its own block", async () => {
|
|
344
|
+
const creator = await installAndEnable("record-blocks");
|
|
345
|
+
const blocker = await installSource(
|
|
346
|
+
"blocking-recorder",
|
|
347
|
+
`
|
|
348
|
+
import { block, type SwarmExtension } from "swarm-extension";
|
|
349
|
+
const extension: SwarmExtension = (api) => {
|
|
350
|
+
api.on("pre.task.create", () => block("blocked by recorder"));
|
|
351
|
+
api.on("post.task.blocked", async (_event, ctx) => { await ctx.state.incr("count"); });
|
|
352
|
+
};
|
|
353
|
+
export default extension;
|
|
354
|
+
`,
|
|
355
|
+
);
|
|
356
|
+
const observerBundle = await loadBundleFixture("record-blocks");
|
|
357
|
+
const installedObserver = await installExtension({
|
|
358
|
+
...observerBundle,
|
|
359
|
+
manifest: { ...observerBundle.manifest, name: "last-block-observer" },
|
|
360
|
+
priority: 1_000,
|
|
361
|
+
});
|
|
362
|
+
const observer = await enableExtension(installedObserver.extension.id);
|
|
363
|
+
const server = new McpServer({ name: "extension-blocked-event", version: "1.0.0" });
|
|
364
|
+
registerSendTaskTool(server);
|
|
365
|
+
const result = await registeredSendTask(server).handler(
|
|
366
|
+
sendTaskArgs("extension task"),
|
|
367
|
+
markExtensionRequestOrigin({
|
|
368
|
+
sessionId: "blocked-extension",
|
|
369
|
+
requestInfo: { headers: { "x-agent-id": creator.agentId! } },
|
|
370
|
+
}),
|
|
371
|
+
);
|
|
372
|
+
expect(result.isError).toBe(true);
|
|
373
|
+
// The final subscriber acknowledges only after every earlier recipient has finished.
|
|
374
|
+
await waitForCounter(observer, "ext:last-block-observer:count");
|
|
375
|
+
expect(
|
|
376
|
+
(await getKv(`task:agent:${blocker.agentId}`, "ext:blocking-recorder:count"))?.value,
|
|
377
|
+
).toBe(1);
|
|
378
|
+
expect(events).toHaveLength(1);
|
|
379
|
+
expect(events[0]).toMatchObject({ origin: "extension:record-blocks" });
|
|
380
|
+
expect(await getKv(`task:agent:${creator.agentId}`, "ext:record-blocks:count")).toBeNull();
|
|
381
|
+
expect(await listExtensionRuns(creator.id)).toEqual([]);
|
|
382
|
+
const runs = await listExtensionRuns(blocker.id);
|
|
383
|
+
expect(runs).toContainEqual(
|
|
384
|
+
expect.objectContaining({
|
|
385
|
+
event: "post.task.blocked",
|
|
386
|
+
agentId: creator.agentId,
|
|
387
|
+
subject: "extension:record-blocks: blocking-recorder",
|
|
388
|
+
}),
|
|
389
|
+
);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test("continue and valid rewrites emit no blocked event", async () => {
|
|
393
|
+
const recorder = await installAndEnable("record-blocks");
|
|
394
|
+
expect((await postTask({ task: "continued task" })).status).toBe(201);
|
|
395
|
+
await installAndEnable("rewrite-task-priority");
|
|
396
|
+
const modified = await postTask({ task: "rewritten task" });
|
|
397
|
+
expect(modified.status).toBe(201);
|
|
398
|
+
expect(modified.body.priority).toBe(1);
|
|
399
|
+
expect(events).toEqual([]);
|
|
400
|
+
expect(await getKv(`task:agent:${recorder.agentId}`, "ext:record-blocks:count")).toBeNull();
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test("a throwing bus observer does not replace the blocked response", async () => {
|
|
404
|
+
await installAndEnable("block-tasks-from-source", { source: "rest" });
|
|
405
|
+
let calls = 0;
|
|
406
|
+
const observer = () => {
|
|
407
|
+
calls++;
|
|
408
|
+
throw new Error("broken block observer");
|
|
409
|
+
};
|
|
410
|
+
const error = spyOn(console, "error").mockImplementation(() => {});
|
|
411
|
+
workflowEventBus.on("task.create_blocked", observer);
|
|
412
|
+
try {
|
|
413
|
+
expect((await postTask({ task: "still blocked" })).status).toBe(422);
|
|
414
|
+
expect(calls).toBe(1);
|
|
415
|
+
} finally {
|
|
416
|
+
workflowEventBus.off("task.create_blocked", observer);
|
|
417
|
+
error.mockRestore();
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
|
|
148
422
|
test("REST task creation applies rewrites and reports blocks as 422", async () => {
|
|
149
423
|
const rewrite = await installAndEnable("rewrite-task-priority");
|
|
150
424
|
const created = await postTask({ task: "rewrite this task" });
|
|
@@ -278,6 +552,57 @@ describe("extension task boundaries", () => {
|
|
|
278
552
|
);
|
|
279
553
|
});
|
|
280
554
|
|
|
555
|
+
test("Slack event metadata retains mixed-sender batches and rejects metadata rewrites", async () => {
|
|
556
|
+
await createAgent({ name: "slack-metadata-lead", isLead: true, status: "idle" });
|
|
557
|
+
await installSource(
|
|
558
|
+
"a-rewrite-slack-metadata",
|
|
559
|
+
`import type { SwarmExtension } from "swarm-extension";
|
|
560
|
+
const extension: SwarmExtension = (api) => {
|
|
561
|
+
api.on("pre.task.create", () => ({ action: "modify", data: {
|
|
562
|
+
priority: 7, slack: { trigger: "mention", messages: [] }
|
|
563
|
+
} as never }));
|
|
564
|
+
};
|
|
565
|
+
export default extension;
|
|
566
|
+
`,
|
|
567
|
+
);
|
|
568
|
+
const recorder = await installAndEnable("record-task-create");
|
|
569
|
+
const channelId = "C_EVENT_METADATA";
|
|
570
|
+
const threadTs = "3000.0001";
|
|
571
|
+
bufferThreadMessage(channelId, threadTs, "first request", "U_FIRST", "3000.0002");
|
|
572
|
+
bufferThreadMessage(channelId, threadTs, "second request", "U_SECOND", "3000.0003");
|
|
573
|
+
|
|
574
|
+
await instantFlush(`${channelId}:${threadTs}`);
|
|
575
|
+
|
|
576
|
+
const event = (await getKv(`task:agent:${recorder.agentId}`, "ext:record-task-create:last"))
|
|
577
|
+
?.value as TaskCreateEvent & { slack?: unknown };
|
|
578
|
+
expect(event.slack).toEqual({
|
|
579
|
+
trigger: "thread-buffer",
|
|
580
|
+
messages: [
|
|
581
|
+
{ ts: "3000.0002", userId: "U_FIRST", text: "first request" },
|
|
582
|
+
{ ts: "3000.0003", userId: "U_SECOND", text: "second request" },
|
|
583
|
+
],
|
|
584
|
+
});
|
|
585
|
+
expect(event.origin).toBe("slack");
|
|
586
|
+
expect(event.options).toMatchObject({
|
|
587
|
+
slackTriggerMessageTs: "3000.0003",
|
|
588
|
+
slackUserId: "U_FIRST",
|
|
589
|
+
priority: 7,
|
|
590
|
+
});
|
|
591
|
+
expect(event.options).not.toHaveProperty("slack");
|
|
592
|
+
const task = await getMostRecentTaskInThread(channelId, threadTs);
|
|
593
|
+
expect(task).toMatchObject({
|
|
594
|
+
priority: 7,
|
|
595
|
+
slackTriggerMessageTs: "3000.0003",
|
|
596
|
+
slackUserId: "U_FIRST",
|
|
597
|
+
});
|
|
598
|
+
expect(task).not.toHaveProperty("slack");
|
|
599
|
+
|
|
600
|
+
await createTaskWithSiblingAwareness("ordinary API task", { source: "api" });
|
|
601
|
+
const apiEvent = (await getKv(`task:agent:${recorder.agentId}`, "ext:record-task-create:last"))
|
|
602
|
+
?.value as TaskCreateEvent & { slack?: unknown };
|
|
603
|
+
expect(apiEvent).not.toHaveProperty("slack");
|
|
604
|
+
});
|
|
605
|
+
|
|
281
606
|
test("scheduler and workflow executor provide their task creation origins", async () => {
|
|
282
607
|
const extension = await installSource(
|
|
283
608
|
"record-task-origin",
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
fetchResolvedEnv,
|
|
15
15
|
RELOADABLE_ENV_KEYS,
|
|
16
16
|
} from "../commands/runner";
|
|
17
|
+
import { contextBoundNoteThreshold } from "../hooks/context-bound-note";
|
|
17
18
|
import { checkCodexCredentials } from "../providers/codex-adapter";
|
|
18
19
|
import { MASKED_SECRET_VALUE } from "../utils/config-secrets";
|
|
19
20
|
import { registerVolatileSecret } from "../utils/secret-scrubber";
|
|
@@ -795,6 +796,22 @@ describe("agent-scoped PI_CODEMODE_MODELS reaches the worker env", () => {
|
|
|
795
796
|
});
|
|
796
797
|
});
|
|
797
798
|
|
|
799
|
+
describe("agent-scoped CONTEXT_BOUND_NOTE_TOKENS turns the note on for one agent", () => {
|
|
800
|
+
// Each session's env is this agent's /api/config/resolved result (global,
|
|
801
|
+
// then agent, then repo rows), and every adapter reads the threshold from it.
|
|
802
|
+
test("the agent with the row gets the threshold; an agent without it stays off", async () => {
|
|
803
|
+
mockResponsesByAgentId.set("bound-note-pilot", {
|
|
804
|
+
status: 200,
|
|
805
|
+
body: { configs: [{ key: "CONTEXT_BOUND_NOTE_TOKENS", value: "150000" }] },
|
|
806
|
+
});
|
|
807
|
+
const baseEnv: Record<string, string | undefined> = {};
|
|
808
|
+
const pilot = await fetchResolvedEnv(testUrl, "key", "bound-note-pilot", baseEnv);
|
|
809
|
+
const other = await fetchResolvedEnv(testUrl, "key", "bound-note-other", baseEnv);
|
|
810
|
+
expect(contextBoundNoteThreshold(pilot.env)).toBe(150_000);
|
|
811
|
+
expect(contextBoundNoteThreshold(other.env)).toBe(0);
|
|
812
|
+
});
|
|
813
|
+
});
|
|
814
|
+
|
|
798
815
|
describe("deleting a reloadable config row", () => {
|
|
799
816
|
// One key per test: the runner remembers each overridden key's boot value
|
|
800
817
|
// for the life of the process.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SwarmExtension } from "swarm-extension";
|
|
2
|
+
|
|
3
|
+
const extension: SwarmExtension = (api) => {
|
|
4
|
+
api.on("post.task.blocked", async (event, ctx) => {
|
|
5
|
+
await ctx.state.set("last", event);
|
|
6
|
+
await ctx.state.incr("count");
|
|
7
|
+
});
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default extension;
|