@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
package/src/http/api-keys.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
recordKeyRateLimitWindows,
|
|
11
11
|
recordKeySeatMismatch,
|
|
12
12
|
recordKeyUsage,
|
|
13
|
+
recordKeyValidation,
|
|
13
14
|
setApiKeyName,
|
|
14
15
|
setApiKeyPlan,
|
|
15
16
|
} from "../be/db";
|
|
@@ -79,6 +80,39 @@ const reportRateLimit = route({
|
|
|
79
80
|
auth: { apiKey: true },
|
|
80
81
|
});
|
|
81
82
|
|
|
83
|
+
const reportValidation = route({
|
|
84
|
+
method: "post",
|
|
85
|
+
path: "/api/keys/report-validation",
|
|
86
|
+
pattern: ["api", "keys", "report-validation"],
|
|
87
|
+
summary: "Record live validation results for a credential pool",
|
|
88
|
+
tags: ["API Keys"],
|
|
89
|
+
body: z.object({
|
|
90
|
+
keyType: z.string(),
|
|
91
|
+
keys: z
|
|
92
|
+
.array(
|
|
93
|
+
z.object({
|
|
94
|
+
keyIndex: z.number().int().min(0),
|
|
95
|
+
keySuffix: z.string().min(1).max(10),
|
|
96
|
+
ok: z.boolean(),
|
|
97
|
+
httpStatus: z.number().int().optional(),
|
|
98
|
+
}),
|
|
99
|
+
)
|
|
100
|
+
.min(1)
|
|
101
|
+
.max(100),
|
|
102
|
+
scope: z.string().optional(),
|
|
103
|
+
scopeId: z.string().optional(),
|
|
104
|
+
}),
|
|
105
|
+
responses: {
|
|
106
|
+
200: { description: "Validation recorded", schema: successMessageSchema },
|
|
107
|
+
400: { description: "Validation error" },
|
|
108
|
+
401: { description: "Unauthorized" },
|
|
109
|
+
},
|
|
110
|
+
auth: { apiKey: true },
|
|
111
|
+
rbac: {
|
|
112
|
+
ungated: "worker credential telemetry, same posture as POST /api/keys/report-rate-limit",
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
82
116
|
const reportAuthFailure = route({
|
|
83
117
|
method: "post",
|
|
84
118
|
path: "/api/keys/report-auth-failure",
|
|
@@ -223,6 +257,7 @@ const getAvailable = route({
|
|
|
223
257
|
schema: z.object({
|
|
224
258
|
success: z.literal(true),
|
|
225
259
|
availableIndices: z.array(z.number().int()),
|
|
260
|
+
rejectedIndices: z.array(z.number().int()).optional(),
|
|
226
261
|
totalKeys: z.number().int(),
|
|
227
262
|
/** Indices excluded only by an active model-scoped window block. Present only when `model` was passed. */
|
|
228
263
|
modelBlockedIndices: z.array(z.number().int()).optional(),
|
|
@@ -273,6 +308,8 @@ const ApiKeyStatusSchema = z.object({
|
|
|
273
308
|
/** Auth failures in a row since the last success or clear. */
|
|
274
309
|
consecutiveAuthFailures: z.number().int(),
|
|
275
310
|
lastAuthFailureAt: z.string().nullable(),
|
|
311
|
+
lastValidationError: z.string().nullable(),
|
|
312
|
+
lastValidationFailedAt: z.string().nullable(),
|
|
276
313
|
/** Derived, readable view of any rejected model-scoped window (Fable/Opus/Sonnet) on this key. */
|
|
277
314
|
modelLimits: z.array(
|
|
278
315
|
z.object({
|
|
@@ -555,6 +592,21 @@ export async function handleApiKeys(
|
|
|
555
592
|
return true;
|
|
556
593
|
}
|
|
557
594
|
|
|
595
|
+
// POST /api/keys/report-validation
|
|
596
|
+
if (reportValidation.match(req.method, pathSegments)) {
|
|
597
|
+
const parsed = await reportValidation.parse(req, res, pathSegments, queryParams);
|
|
598
|
+
if (!parsed) return true;
|
|
599
|
+
|
|
600
|
+
const { keyType, keys, scope, scopeId } = parsed.body;
|
|
601
|
+
try {
|
|
602
|
+
await recordKeyValidation(keyType, keys, scope, scopeId ?? null);
|
|
603
|
+
reportValidation.respond(res, 200, { success: true, message: "Key validation recorded" });
|
|
604
|
+
} catch (err) {
|
|
605
|
+
jsonError(res, err instanceof Error ? err.message : "Failed to record key validation", 500);
|
|
606
|
+
}
|
|
607
|
+
return true;
|
|
608
|
+
}
|
|
609
|
+
|
|
558
610
|
// POST /api/keys/report-auth-failure
|
|
559
611
|
if (reportAuthFailure.match(req.method, pathSegments)) {
|
|
560
612
|
const parsed = await reportAuthFailure.parse(req, res, pathSegments, queryParams);
|
|
@@ -650,6 +702,7 @@ export async function handleApiKeys(
|
|
|
650
702
|
getAvailable.respond(res, 200, {
|
|
651
703
|
success: true,
|
|
652
704
|
availableIndices: result.availableIndices,
|
|
705
|
+
rejectedIndices: result.rejectedIndices,
|
|
653
706
|
totalKeys,
|
|
654
707
|
authFailureFence: result.authFailureFence,
|
|
655
708
|
...(model !== undefined
|
package/src/http/assets.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { getApp } from "../apps/store";
|
|
4
|
+
import { normalizeAssetKey } from "../assets/key";
|
|
5
|
+
import { sourceMoveError, sourceOfKey } from "../assets/source-key";
|
|
4
6
|
import { auditAssetKeys } from "../be/asset-key-audit";
|
|
5
7
|
import { AssetKeyAuthorizationError, authorizeAssetKeyWrite } from "../be/asset-key-auth";
|
|
6
8
|
import { resolveHttpAuditUserId } from "../be/audit-user";
|
|
@@ -15,6 +17,7 @@ import {
|
|
|
15
17
|
upsertAssetKeyMapping,
|
|
16
18
|
} from "../be/db";
|
|
17
19
|
import { getScriptById } from "../be/scripts/db";
|
|
20
|
+
import { getSessionRoot } from "../be/session-shares";
|
|
18
21
|
import { can, type RbacResource } from "../rbac";
|
|
19
22
|
import {
|
|
20
23
|
type AssetEntityType,
|
|
@@ -334,6 +337,18 @@ export async function handleAssets(
|
|
|
334
337
|
);
|
|
335
338
|
return true;
|
|
336
339
|
}
|
|
340
|
+
const destination = normalizeAssetKey(parsed.body.key);
|
|
341
|
+
if (sourceOfKey(destination)) {
|
|
342
|
+
// The session root's own source key is the only allowed source key
|
|
343
|
+
// (Undo). A root we cannot load allows none.
|
|
344
|
+
const rootId = task.parentTaskId ? (await getSessionRoot(task.id))?.id : task.id;
|
|
345
|
+
const root = rootId === task.id ? task : rootId ? await getTaskById(rootId) : null;
|
|
346
|
+
const sourceError = sourceMoveError(destination, root ?? { id: task.id });
|
|
347
|
+
if (sourceError) {
|
|
348
|
+
jsonError(res, sourceError, 400);
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
337
352
|
// Only the tasks checked here move: a subtask created after this read stays put.
|
|
338
353
|
const tasks = parsed.body.cascade ? await getRootTaskChain(task.id) : [task];
|
|
339
354
|
for (const target of tasks) {
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { decryptSecret, getEncryptionKey } from "../be/crypto";
|
|
4
|
+
import { deleteKv, deleteSwarmConfigByKey, getDbClient, getKeyStatuses, getKv } from "../be/db";
|
|
5
|
+
import { retirePoolKeyStatus } from "../be/db/api-keys";
|
|
6
|
+
import { deriveCodexKeySuffix } from "../providers/codex-oauth/auth-json";
|
|
7
|
+
import { decodeJwt, extractChatgptPlanType } from "../providers/codex-oauth/flow";
|
|
8
|
+
import { MAX_CODEX_OAUTH_SLOT } from "../providers/codex-oauth/storage";
|
|
9
|
+
import {
|
|
10
|
+
CODEX_AUTH_FAILURE_BENCH_THRESHOLD,
|
|
11
|
+
CODEX_AUTH_WATCH_NAMESPACE,
|
|
12
|
+
codexAuthBenchMarkerKey,
|
|
13
|
+
} from "../utils/codex-auth-failure";
|
|
14
|
+
import { ensureConfigAdmin, requestSecretReadScope, runConfigWriteSideEffects } from "./config";
|
|
15
|
+
import { route } from "./route-def";
|
|
16
|
+
import { jsonError } from "./utils";
|
|
17
|
+
|
|
18
|
+
export const CodexAccountSchema = z.object({
|
|
19
|
+
slot: z.number().int().min(0),
|
|
20
|
+
configKey: z.string(),
|
|
21
|
+
legacy: z.boolean(),
|
|
22
|
+
suffix: z.string().nullable(),
|
|
23
|
+
plan: z.string().nullable(),
|
|
24
|
+
email: z.string().nullable(),
|
|
25
|
+
status: z.enum(["ok", "needs_login", "unchecked"]),
|
|
26
|
+
});
|
|
27
|
+
export type CodexAccount = z.infer<typeof CodexAccountSchema>;
|
|
28
|
+
|
|
29
|
+
export const CodexAccountsResponseSchema = z.object({ accounts: z.array(CodexAccountSchema) });
|
|
30
|
+
export type CodexAccountsResponse = z.infer<typeof CodexAccountsResponseSchema>;
|
|
31
|
+
|
|
32
|
+
const listAccounts = route({
|
|
33
|
+
method: "get",
|
|
34
|
+
path: "/api/codex-oauth/accounts",
|
|
35
|
+
pattern: ["api", "codex-oauth", "accounts"],
|
|
36
|
+
summary: "List stored Codex ChatGPT accounts",
|
|
37
|
+
tags: ["Codex OAuth"],
|
|
38
|
+
responses: {
|
|
39
|
+
200: { description: "Stored account metadata", schema: CodexAccountsResponseSchema },
|
|
40
|
+
403: { description: "Caller cannot read all secrets" },
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const deleteAccount = route({
|
|
45
|
+
method: "delete",
|
|
46
|
+
path: "/api/codex-oauth/accounts/{slot}",
|
|
47
|
+
pattern: ["api", "codex-oauth", "accounts", null],
|
|
48
|
+
summary: "Remove a stored Codex ChatGPT account",
|
|
49
|
+
tags: ["Codex OAuth"],
|
|
50
|
+
rbac: { permission: "config.delete.any" },
|
|
51
|
+
params: z.object({ slot: z.coerce.number().int().min(0).max(MAX_CODEX_OAUTH_SLOT) }),
|
|
52
|
+
responses: {
|
|
53
|
+
200: { description: "Remaining accounts", schema: CodexAccountsResponseSchema },
|
|
54
|
+
403: { description: "Config administration and permission to read all secrets required" },
|
|
55
|
+
404: { description: "Account slot does not exist" },
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
type AccountRow = { key: string; value: string; encrypted: number };
|
|
60
|
+
|
|
61
|
+
async function accountRows(): Promise<Array<AccountRow & { slot: number; legacy: boolean }>> {
|
|
62
|
+
const rows = await getDbClient().query<AccountRow>(
|
|
63
|
+
"SELECT key, value, encrypted FROM swarm_config WHERE scope = 'global' AND (key = 'codex_oauth' OR key GLOB 'codex_oauth_[0-9]*')",
|
|
64
|
+
);
|
|
65
|
+
const hasSlotZero = rows.some((row) => row.key === "codex_oauth_0");
|
|
66
|
+
return rows
|
|
67
|
+
.flatMap((row) => {
|
|
68
|
+
if (row.key === "codex_oauth") {
|
|
69
|
+
return hasSlotZero ? [] : [{ ...row, slot: 0, legacy: true }];
|
|
70
|
+
}
|
|
71
|
+
const match = /^codex_oauth_(0|[1-9]\d*)$/.exec(row.key);
|
|
72
|
+
if (!match) return [];
|
|
73
|
+
const slot = Number(match[1]);
|
|
74
|
+
return slot <= MAX_CODEX_OAUTH_SLOT ? [{ ...row, slot, legacy: false }] : [];
|
|
75
|
+
})
|
|
76
|
+
.sort((a, b) => a.slot - b.slot);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const AccountCredentialsSchema = z.object({
|
|
80
|
+
access: z.string().min(1),
|
|
81
|
+
refresh: z.string().min(1),
|
|
82
|
+
expires: z.number().finite(),
|
|
83
|
+
accountId: z.string().min(1),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
function readCredentials(row: AccountRow): z.infer<typeof AccountCredentialsSchema> | null {
|
|
87
|
+
try {
|
|
88
|
+
const value = row.encrypted ? decryptSecret(row.value, getEncryptionKey()) : row.value;
|
|
89
|
+
return AccountCredentialsSchema.parse(JSON.parse(value));
|
|
90
|
+
} catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function accountSuffix(row: AccountRow): string | null {
|
|
96
|
+
const credentials = readCredentials(row);
|
|
97
|
+
return credentials ? deriveCodexKeySuffix(credentials.access, credentials.accountId) : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function accountsResponse(): Promise<CodexAccountsResponse> {
|
|
101
|
+
const rows = await accountRows();
|
|
102
|
+
const statuses = await getKeyStatuses("CODEX_OAUTH", "global", null);
|
|
103
|
+
const accounts: CodexAccount[] = [];
|
|
104
|
+
for (const row of rows) {
|
|
105
|
+
const account: CodexAccount = {
|
|
106
|
+
slot: row.slot,
|
|
107
|
+
configKey: row.key,
|
|
108
|
+
legacy: row.legacy,
|
|
109
|
+
suffix: null,
|
|
110
|
+
plan: null,
|
|
111
|
+
email: null,
|
|
112
|
+
status: "needs_login",
|
|
113
|
+
};
|
|
114
|
+
const credentials = readCredentials(row);
|
|
115
|
+
if (!credentials) {
|
|
116
|
+
accounts.push(account);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
account.suffix = deriveCodexKeySuffix(credentials.access, credentials.accountId);
|
|
120
|
+
account.plan = extractChatgptPlanType(credentials.access);
|
|
121
|
+
const profile = decodeJwt(credentials.access)?.["https://api.openai.com/profile"];
|
|
122
|
+
if (profile && typeof profile === "object" && "email" in profile) {
|
|
123
|
+
account.email = typeof profile.email === "string" && profile.email ? profile.email : null;
|
|
124
|
+
}
|
|
125
|
+
const status = statuses.find((entry) => entry.keySuffix === account.suffix);
|
|
126
|
+
const marker = await getKv(CODEX_AUTH_WATCH_NAMESPACE, codexAuthBenchMarkerKey(account.suffix));
|
|
127
|
+
const statusBenched =
|
|
128
|
+
status &&
|
|
129
|
+
status.consecutiveAuthFailures >= CODEX_AUTH_FAILURE_BENCH_THRESHOLD &&
|
|
130
|
+
status.status === "rate_limited";
|
|
131
|
+
account.status = marker || statusBenched ? "needs_login" : status ? "ok" : "unchecked";
|
|
132
|
+
accounts.push(account);
|
|
133
|
+
}
|
|
134
|
+
return { accounts };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Both responses list account emails and plans, so both routes need this gate. */
|
|
138
|
+
async function ensureSecretRead(req: IncomingMessage, res: ServerResponse): Promise<boolean> {
|
|
139
|
+
if ((await requestSecretReadScope(req)) === "all") return true;
|
|
140
|
+
jsonError(res, "Reading Codex accounts requires access to all secrets", 403);
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function handleCodexOAuthAccounts(
|
|
145
|
+
req: IncomingMessage,
|
|
146
|
+
res: ServerResponse,
|
|
147
|
+
pathSegments: string[],
|
|
148
|
+
queryParams: URLSearchParams,
|
|
149
|
+
): Promise<boolean> {
|
|
150
|
+
if (listAccounts.match(req.method, pathSegments)) {
|
|
151
|
+
const parsed = await listAccounts.parse(req, res, pathSegments, queryParams);
|
|
152
|
+
if (!parsed) return true;
|
|
153
|
+
if (!(await ensureSecretRead(req, res))) return true;
|
|
154
|
+
listAccounts.respond(res, 200, await accountsResponse());
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (deleteAccount.match(req.method, pathSegments)) {
|
|
159
|
+
const parsed = await deleteAccount.parse(req, res, pathSegments, queryParams);
|
|
160
|
+
if (!parsed) return true;
|
|
161
|
+
if (!(await ensureConfigAdmin(req, res, "config.delete.any"))) return true;
|
|
162
|
+
if (!(await ensureSecretRead(req, res))) return true;
|
|
163
|
+
const deletedKey = await getDbClient().transaction(async () => {
|
|
164
|
+
const rows = await accountRows();
|
|
165
|
+
const account = rows.find((row) => row.slot === parsed.params.slot);
|
|
166
|
+
if (!account || !(await deleteSwarmConfigByKey("global", null, account.key))) return null;
|
|
167
|
+
// A new sign-in takes the first free slot, so it must not inherit this
|
|
168
|
+
// account's auth bench (keyed by slot) or its bench marker.
|
|
169
|
+
const suffix = accountSuffix(account);
|
|
170
|
+
if (suffix) {
|
|
171
|
+
const remaining = rows.flatMap((row) => {
|
|
172
|
+
const keySuffix = row === account ? null : accountSuffix(row);
|
|
173
|
+
return keySuffix ? [{ keySuffix, keyIndex: row.slot }] : [];
|
|
174
|
+
});
|
|
175
|
+
await retirePoolKeyStatus("CODEX_OAUTH", suffix, remaining);
|
|
176
|
+
if (!remaining.some((row) => row.keySuffix === suffix)) {
|
|
177
|
+
await deleteKv(CODEX_AUTH_WATCH_NAMESPACE, codexAuthBenchMarkerKey(suffix));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return account.key;
|
|
181
|
+
});
|
|
182
|
+
if (deletedKey === null) {
|
|
183
|
+
jsonError(res, "Codex account slot does not exist", 404);
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
await runConfigWriteSideEffects("global", deletedKey, "");
|
|
187
|
+
deleteAccount.respond(res, 200, await accountsResponse());
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
return false;
|
|
191
|
+
}
|
package/src/http/comb.ts
CHANGED
|
@@ -2,7 +2,12 @@ import type { IncomingMessage, ServerResponse } from "node:http";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { resolveHttpAuditUserId } from "../be/audit-user";
|
|
4
4
|
import { getCombConfig } from "../comb/config";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
REVIEW_BATCH_MAX,
|
|
7
|
+
REVIEW_NOTE_MAX,
|
|
8
|
+
ReviewBatchError,
|
|
9
|
+
sendReviewBatch,
|
|
10
|
+
} from "../comb/review-batch";
|
|
6
11
|
import { can } from "../rbac";
|
|
7
12
|
import { TaskCreationBlockedError } from "../tasks/errors";
|
|
8
13
|
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
@@ -24,6 +29,12 @@ const resultSchema = z.object({
|
|
|
24
29
|
skipped: z.array(skippedSchema),
|
|
25
30
|
/** Comments of an earlier send whose missing "sent" reply this batch posted again. */
|
|
26
31
|
repaired: z.array(z.object({ id: z.string(), taskId: z.string() })),
|
|
32
|
+
/**
|
|
33
|
+
* With a new task only. `online`: a lead got it. `offline`: every lead is offline,
|
|
34
|
+
* so the task waits for one. `none`: no lead, so the task waits
|
|
35
|
+
* unassigned in the pool.
|
|
36
|
+
*/
|
|
37
|
+
lead: z.enum(["online", "offline", "none"]).optional(),
|
|
27
38
|
});
|
|
28
39
|
|
|
29
40
|
const batchErrorSchema = z.object({ error: z.string(), skipped: z.array(skippedSchema) });
|
|
@@ -41,7 +52,7 @@ const reviewBatchRoute = route({
|
|
|
41
52
|
pattern: ["api", "comb", "review-batches"],
|
|
42
53
|
summary: "Send agent-fs comments to the swarm as one lead task",
|
|
43
54
|
description:
|
|
44
|
-
"Comb's 'Send to swarm'. The server reads each comment again from agent-fs with its bootstrap key, skips replies, resolved comments, and comments already sent, and creates ONE task for the lead. Each sent comment gets a `[comb:sent task=<id>]` reply from the swarm service account. A comment is sent at most once. When an earlier send lost its reply, the batch posts that reply again (`repaired`). Answers 404 while COMB_ENABLED is off.",
|
|
55
|
+
"Comb's 'Send to swarm'. The server reads each comment again from agent-fs with its bootstrap key, skips replies, resolved comments, and comments already sent, and creates ONE task for the lead. Each sent comment gets a `[comb:sent task=<id>]` reply from the swarm service account. A comment is sent at most once. An optional `note` from the sender goes into the task text. When an earlier send lost its reply, the batch posts that reply again (`repaired`). Answers 404 while COMB_ENABLED is off.",
|
|
45
56
|
tags: ["Comb"],
|
|
46
57
|
body: z.object({
|
|
47
58
|
orgId: z.string().min(1),
|
|
@@ -54,6 +65,8 @@ const reviewBatchRoute = route({
|
|
|
54
65
|
.min(1)
|
|
55
66
|
.max(1024)
|
|
56
67
|
.refine(isDrivePath, "scopePath must start with '/' and have no '.' or '..' segment"),
|
|
68
|
+
/** The sender's note for the lead about the whole batch. */
|
|
69
|
+
note: z.string().max(REVIEW_NOTE_MAX).optional(),
|
|
57
70
|
}),
|
|
58
71
|
responses: {
|
|
59
72
|
200: {
|
package/src/http/config.ts
CHANGED
|
@@ -23,8 +23,9 @@ import {
|
|
|
23
23
|
isInternalConfigKey,
|
|
24
24
|
validateConfigEntry,
|
|
25
25
|
} from "../be/swarm-config-guard";
|
|
26
|
+
import { isScriptRunToken } from "../be/users";
|
|
26
27
|
import { can, isRbacEnabled, type PermissionVerb } from "../rbac";
|
|
27
|
-
import { SwarmConfigSchema } from "../types";
|
|
28
|
+
import { type SwarmConfig, SwarmConfigSchema } from "../types";
|
|
28
29
|
import { getRequestAuth, type HttpRequestAuth } from "../utils/request-auth-context";
|
|
29
30
|
import { registerVolatileSecret } from "../utils/secret-scrubber";
|
|
30
31
|
import { reloadGlobalConfigsAndIntegrations, scheduleIntegrationsReload } from "./core";
|
|
@@ -66,7 +67,7 @@ export function readsRepoSecrets(
|
|
|
66
67
|
}).allow;
|
|
67
68
|
}
|
|
68
69
|
|
|
69
|
-
async function requestSecretReadScope(
|
|
70
|
+
export async function requestSecretReadScope(
|
|
70
71
|
req: IncomingMessage,
|
|
71
72
|
resolvedScope?: { agentId?: string; repoId?: string },
|
|
72
73
|
): Promise<"all" | "self" | "none"> {
|
|
@@ -111,6 +112,49 @@ async function resolveSecretsRead(
|
|
|
111
112
|
};
|
|
112
113
|
}
|
|
113
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Ids of the rows a script run's token reads unmasked on its own
|
|
117
|
+
* `/api/config/resolved` (its agent over global, no repo secrets). The run
|
|
118
|
+
* already holds these values in `ctx.swarm.config`. `undefined` for any other
|
|
119
|
+
* caller.
|
|
120
|
+
*/
|
|
121
|
+
async function scriptRunHeldConfigIds(req: IncomingMessage): Promise<Set<string> | undefined> {
|
|
122
|
+
const auth = getRequestAuth(req);
|
|
123
|
+
if (auth?.kind !== "agent" || auth.tokenKind !== "agent") return undefined;
|
|
124
|
+
if (!(await isScriptRunToken(auth.tokenId))) return undefined;
|
|
125
|
+
const held = stripApiOnlyKeys(
|
|
126
|
+
await getResolvedConfig(auth.agentId, undefined, { includeRepoSecrets: false }),
|
|
127
|
+
);
|
|
128
|
+
return new Set(held.map((config) => config.id));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Mask a raw list or by-id read for this caller. A script run's token gets
|
|
133
|
+
* the rows it holds (see {@link scriptRunHeldConfigIds}) unmasked, so
|
|
134
|
+
* `config_list` and `ctx.swarm.config` agree; every other secret row stays
|
|
135
|
+
* masked. Unmasked secret values are registered with the scrubber.
|
|
136
|
+
*/
|
|
137
|
+
async function maskConfigRead(
|
|
138
|
+
req: IncomingMessage,
|
|
139
|
+
configs: SwarmConfig[],
|
|
140
|
+
includeSecrets: boolean,
|
|
141
|
+
): Promise<{ result: SwarmConfig[]; secretsNote: string }> {
|
|
142
|
+
const { effectiveIncludeSecrets, secretsNote } = await resolveSecretsRead(req, includeSecrets);
|
|
143
|
+
const held =
|
|
144
|
+
includeSecrets && !effectiveIncludeSecrets ? await scriptRunHeldConfigIds(req) : undefined;
|
|
145
|
+
const reveal = (config: SwarmConfig) => effectiveIncludeSecrets || !!held?.has(config.id);
|
|
146
|
+
const result = configs.map((config) =>
|
|
147
|
+
config.isSecret && !reveal(config) ? maskSecrets([config])[0]! : config,
|
|
148
|
+
);
|
|
149
|
+
for (const config of result) {
|
|
150
|
+
if (config.isSecret && config.value && reveal(config)) {
|
|
151
|
+
registerVolatileSecret(config.value, `config:${config.key}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const anyMasked = result.some((config) => config.isSecret && !reveal(config));
|
|
155
|
+
return { result, secretsNote: held && !anyMasked ? "" : secretsNote };
|
|
156
|
+
}
|
|
157
|
+
|
|
114
158
|
/**
|
|
115
159
|
* Gate a config write/delete over HTTP (DES-445 follow-up).
|
|
116
160
|
*
|
|
@@ -455,13 +499,9 @@ export async function handleConfig(
|
|
|
455
499
|
jsonError(res, "Config not found", 404);
|
|
456
500
|
return true;
|
|
457
501
|
}
|
|
458
|
-
const {
|
|
459
|
-
const singleResult = effectiveIncludeSecrets ? config : maskSecrets([config])[0]!;
|
|
460
|
-
if (effectiveIncludeSecrets && singleResult.isSecret && singleResult.value) {
|
|
461
|
-
registerVolatileSecret(singleResult.value, `config:${singleResult.key}`);
|
|
462
|
-
}
|
|
502
|
+
const { result, secretsNote } = await maskConfigRead(req, [config], includeSecrets);
|
|
463
503
|
getConfigById.respond(res, 200, {
|
|
464
|
-
...
|
|
504
|
+
...result[0]!,
|
|
465
505
|
...(secretsNote ? { message: `Found 1 config(s).${secretsNote}` } : {}),
|
|
466
506
|
});
|
|
467
507
|
return true;
|
|
@@ -471,7 +511,6 @@ export async function handleConfig(
|
|
|
471
511
|
const parsed = await listConfig.parse(req, res, pathSegments, queryParams);
|
|
472
512
|
if (!parsed) return true;
|
|
473
513
|
const includeSecrets = parsed.query.includeSecrets === "true";
|
|
474
|
-
const { effectiveIncludeSecrets, secretsNote } = await resolveSecretsRead(req, includeSecrets);
|
|
475
514
|
const configs = stripApiOnlyKeys(
|
|
476
515
|
await getSwarmConfigs({
|
|
477
516
|
scope: parsed.query.scope || undefined,
|
|
@@ -479,14 +518,7 @@ export async function handleConfig(
|
|
|
479
518
|
key: parsed.query.key,
|
|
480
519
|
}),
|
|
481
520
|
);
|
|
482
|
-
const listResult =
|
|
483
|
-
if (effectiveIncludeSecrets) {
|
|
484
|
-
for (const c of listResult) {
|
|
485
|
-
if (c.isSecret && c.value) {
|
|
486
|
-
registerVolatileSecret(c.value, `config:${c.key}`);
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
}
|
|
521
|
+
const { result: listResult, secretsNote } = await maskConfigRead(req, configs, includeSecrets);
|
|
490
522
|
listConfig.respond(res, 200, {
|
|
491
523
|
configs: listResult,
|
|
492
524
|
...(secretsNote ? { message: `Found ${listResult.length} config(s).${secretsNote}` } : {}),
|
package/src/http/core.ts
CHANGED
|
@@ -95,6 +95,12 @@ import {
|
|
|
95
95
|
*/
|
|
96
96
|
const injectedEnvOriginals = new Map<string, { original: string | undefined; injected: string }>();
|
|
97
97
|
|
|
98
|
+
/** Read the deployment value without a stored config's temporary env injection. */
|
|
99
|
+
export function getDeploymentEnvValue(key: string): string | undefined {
|
|
100
|
+
const tracked = injectedEnvOriginals.get(key);
|
|
101
|
+
return tracked && process.env[key] === tracked.injected ? tracked.original : process.env[key];
|
|
102
|
+
}
|
|
103
|
+
|
|
98
104
|
export async function loadGlobalConfigsIntoEnv(override = false): Promise<string[]> {
|
|
99
105
|
const globalConfigs = await getInjectableGlobalConfigs();
|
|
100
106
|
const updated: string[] = [];
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
localTasksPerWeek,
|
|
12
12
|
} from "../fleet-benchmark";
|
|
13
13
|
import { isTelemetryEnabled } from "../telemetry";
|
|
14
|
-
import { COMPANY_TEAM_SIZES, COMPANY_USE_CASES } from "../utils/company-profile";
|
|
14
|
+
import { COMPANY_TEAM_SIZES, COMPANY_USE_CASES, reportedUseCase } from "../utils/company-profile";
|
|
15
15
|
import { normalizeOrgWebsite } from "../utils/org-website";
|
|
16
16
|
import { globalConfig } from "./global-config";
|
|
17
17
|
import { rejectGuest } from "./request-principal";
|
|
@@ -188,6 +188,9 @@ export async function buildFleetBenchmarkResponse(
|
|
|
188
188
|
normalizeOrgWebsite(website) && team.success && use.success
|
|
189
189
|
? { teamSize: team.data, useCase: use.data }
|
|
190
190
|
: null;
|
|
191
|
+
// The proxy answers 400 for a use case its catalog does not list yet, so
|
|
192
|
+
// that use case compares against the whole fleet.
|
|
193
|
+
const proxyPeerGroup = peerGroup && reportedUseCase(peerGroup.useCase) ? peerGroup : null;
|
|
191
194
|
const empty = {
|
|
192
195
|
level: peerGroup ? ("full" as const) : ("basic" as const),
|
|
193
196
|
peerGroup: null,
|
|
@@ -215,7 +218,7 @@ export async function buildFleetBenchmarkResponse(
|
|
|
215
218
|
if (finishedTasks < FLEET_MIN_FINISHED_TASKS)
|
|
216
219
|
return { ...empty, state: "not_enough_tasks", local };
|
|
217
220
|
|
|
218
|
-
const fleet = await fleetBenchmarks(now.getTime(),
|
|
221
|
+
const fleet = await fleetBenchmarks(now.getTime(), proxyPeerGroup);
|
|
219
222
|
const responsePeerGroup =
|
|
220
223
|
peerGroup && fleet?.peer_group
|
|
221
224
|
? {
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import {
|
|
4
|
+
findAcceptedWorkflowOperation,
|
|
5
|
+
type WorkflowIdempotencyClaim,
|
|
6
|
+
} from "../be/db/workflow-idempotency";
|
|
7
|
+
import { getRequestAuth } from "../utils/request-auth-context";
|
|
8
|
+
import { jsonError } from "./utils";
|
|
9
|
+
|
|
10
|
+
/** Visible ASCII, at most 255 characters: room for a UUID, a ULID or a client's own scheme. */
|
|
11
|
+
const IDEMPOTENCY_KEY_PATTERN = /^[\x21-\x7E]{1,255}$/;
|
|
12
|
+
|
|
13
|
+
/** Set on a response answered from an earlier accepted request. */
|
|
14
|
+
export const IDEMPOTENT_REPLAYED_HEADER = "Idempotent-Replayed";
|
|
15
|
+
|
|
16
|
+
/** OpenAPI declaration of the optional `Idempotency-Key` request header. */
|
|
17
|
+
export const idempotencyKeyHeader = (accepted: string) =>
|
|
18
|
+
z.object({
|
|
19
|
+
"Idempotency-Key": z
|
|
20
|
+
.string()
|
|
21
|
+
.regex(IDEMPOTENCY_KEY_PATTERN)
|
|
22
|
+
.optional()
|
|
23
|
+
.describe(
|
|
24
|
+
"Optional client-generated key, 1-255 visible ASCII characters (a UUID works). " +
|
|
25
|
+
`The first request the server accepts under a key ${accepted}; a retry with the same key ` +
|
|
26
|
+
"and an equivalent body gets that request's status and body back (with " +
|
|
27
|
+
`\`${IDEMPOTENT_REPLAYED_HEADER}: true\`) and changes nothing. The key is scoped to the ` +
|
|
28
|
+
"caller's identity and this operation; it is not a credential. The same key with a " +
|
|
29
|
+
"different body is a 409. A request rejected before it is accepted (validation, auth, " +
|
|
30
|
+
"not found, disabled) does not use up the key. Accepted keys do not expire.",
|
|
31
|
+
),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
/** A request that sent a usable `Idempotency-Key`. */
|
|
35
|
+
export interface IdempotencyKeyRequest {
|
|
36
|
+
/** Stable identity of the authenticated caller, never a credential value. */
|
|
37
|
+
principal: string;
|
|
38
|
+
key: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The caller identity a key is scoped to. A user or session token names its
|
|
43
|
+
* user or agent; rotating the token keeps the scope. Requests on the swarm API
|
|
44
|
+
* key are the operator, or the agent named by X-Agent-ID: every worker shares
|
|
45
|
+
* that key, so this keeps their keys apart (it is not an access boundary; the
|
|
46
|
+
* swarm key may act as any agent). A guest page session has no identity.
|
|
47
|
+
*/
|
|
48
|
+
function idempotencyPrincipal(req: IncomingMessage, myAgentId: string | undefined): string | null {
|
|
49
|
+
const auth = getRequestAuth(req);
|
|
50
|
+
switch (auth?.kind) {
|
|
51
|
+
case "user":
|
|
52
|
+
return `user:${auth.userId}`;
|
|
53
|
+
case "agent":
|
|
54
|
+
return `agent:${auth.agentId}`;
|
|
55
|
+
case "controller":
|
|
56
|
+
return `controller:${auth.controllerId}`;
|
|
57
|
+
case "guest":
|
|
58
|
+
return null;
|
|
59
|
+
default:
|
|
60
|
+
return myAgentId ? `agent:${myAgentId}` : "operator";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Read the `Idempotency-Key` header. Undefined when none was sent; null when
|
|
66
|
+
* it is malformed or the caller has no identity to scope it to, after writing
|
|
67
|
+
* the 400.
|
|
68
|
+
*/
|
|
69
|
+
export function readIdempotencyKey(
|
|
70
|
+
req: IncomingMessage,
|
|
71
|
+
res: ServerResponse,
|
|
72
|
+
myAgentId: string | undefined,
|
|
73
|
+
): IdempotencyKeyRequest | null | undefined {
|
|
74
|
+
const raw = req.headers["idempotency-key"];
|
|
75
|
+
if (raw === undefined) return undefined;
|
|
76
|
+
const key = Array.isArray(raw) ? raw.join(", ") : raw;
|
|
77
|
+
if (!IDEMPOTENCY_KEY_PATTERN.test(key)) {
|
|
78
|
+
jsonError(res, "Idempotency-Key must be 1-255 visible ASCII characters", 400);
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const principal = idempotencyPrincipal(req, myAgentId);
|
|
82
|
+
if (!principal) {
|
|
83
|
+
jsonError(res, "Idempotency-Key needs an identified caller", 400);
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
return { principal, key };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Answer from the operation already accepted under this claim's scope, if
|
|
91
|
+
* any. Returns false, writing nothing, when there is none. Otherwise writes a
|
|
92
|
+
* 409 when the stored request differs, a 410 when its workflow was deleted,
|
|
93
|
+
* or hands the stored body to `send` with the replay header set, and returns
|
|
94
|
+
* true. Runs after authentication and before any check that may have changed
|
|
95
|
+
* since the original request.
|
|
96
|
+
*/
|
|
97
|
+
export async function replayAcceptedOperation(
|
|
98
|
+
res: ServerResponse,
|
|
99
|
+
claim: WorkflowIdempotencyClaim,
|
|
100
|
+
send: (body: unknown) => void,
|
|
101
|
+
): Promise<boolean> {
|
|
102
|
+
const accepted = await findAcceptedWorkflowOperation(claim);
|
|
103
|
+
if (!accepted) return false;
|
|
104
|
+
if (accepted.fingerprint !== claim.fingerprint) {
|
|
105
|
+
jsonError(res, "Idempotency-Key was already used for a different request", 409);
|
|
106
|
+
} else if (accepted.resourceDeletedAt !== null) {
|
|
107
|
+
jsonError(
|
|
108
|
+
res,
|
|
109
|
+
"The workflow this Idempotency-Key created or triggered was deleted; send a new key to start over",
|
|
110
|
+
410,
|
|
111
|
+
);
|
|
112
|
+
} else {
|
|
113
|
+
res.setHeader(IDEMPOTENT_REPLAYED_HEADER, "true");
|
|
114
|
+
send(accepted.body);
|
|
115
|
+
}
|
|
116
|
+
return true;
|
|
117
|
+
}
|
package/src/http/index.ts
CHANGED
|
@@ -76,6 +76,7 @@ import { handleApprovalRequests } from "./approval-requests";
|
|
|
76
76
|
import { handleApps } from "./apps";
|
|
77
77
|
import { handleAssets } from "./assets";
|
|
78
78
|
import { handleBudgets } from "./budgets";
|
|
79
|
+
import { handleCodexOAuthAccounts } from "./codex-oauth-accounts";
|
|
79
80
|
import { handleCodexOAuthDevice } from "./codex-oauth-device";
|
|
80
81
|
import { handleCodexOAuthKeepWarm } from "./codex-oauth-keep-warm";
|
|
81
82
|
import { handleComb } from "./comb";
|
|
@@ -115,6 +116,7 @@ import { handlePageProxy } from "./page-proxy";
|
|
|
115
116
|
import { handlePages } from "./pages";
|
|
116
117
|
import { handlePagesPublic } from "./pages-public";
|
|
117
118
|
import { handlePoll } from "./poll";
|
|
119
|
+
import { handlePoolKeys } from "./pool-keys";
|
|
118
120
|
import { handlePricing } from "./pricing";
|
|
119
121
|
import { handlePromptTemplates } from "./prompt-templates";
|
|
120
122
|
import { handleRanking } from "./ranking";
|
|
@@ -360,6 +362,7 @@ const httpServer = createHttpServer(async (req, res) => {
|
|
|
360
362
|
() => handleStatus(req, res, pathSegments, queryParams),
|
|
361
363
|
() => handleOnboarding(req, res, pathSegments, queryParams),
|
|
362
364
|
() => handleCodexOAuthDevice(req, res, pathSegments, queryParams),
|
|
365
|
+
() => handleCodexOAuthAccounts(req, res, pathSegments, queryParams),
|
|
363
366
|
() => handleActiveSessions(req, res, pathSegments, queryParams, myAgentId),
|
|
364
367
|
() => handlePricing(req, res, pathSegments, queryParams, myAgentId),
|
|
365
368
|
() => handleSchedules(req, res, pathSegments, queryParams, myAgentId),
|
|
@@ -367,6 +370,7 @@ const httpServer = createHttpServer(async (req, res) => {
|
|
|
367
370
|
() => handleWorkflowEvents(req, res, pathSegments, queryParams),
|
|
368
371
|
() => handleApprovalRequests(req, res, pathSegments, queryParams),
|
|
369
372
|
() => handleApps(req, res, pathSegments, queryParams, myAgentId),
|
|
373
|
+
() => handlePoolKeys(req, res, pathSegments, queryParams),
|
|
370
374
|
() => handleConfig(req, res, pathSegments, queryParams),
|
|
371
375
|
() => handleFs(req, res, pathSegments, queryParams, myAgentId),
|
|
372
376
|
() => handleComb(req, res, pathSegments, queryParams, myAgentId),
|
|
@@ -898,6 +902,13 @@ httpServer
|
|
|
898
902
|
.catch((err) => {
|
|
899
903
|
console.error("[boot-scrub-sweep] startup scrub failed (non-fatal):", err);
|
|
900
904
|
});
|
|
905
|
+
|
|
906
|
+
// Source key backfill is idempotent, resumable, and non-fatal.
|
|
907
|
+
import("../be/session-source-backfill")
|
|
908
|
+
.then(({ runSessionSourceBackfill }) => runSessionSourceBackfill())
|
|
909
|
+
.catch((err) => {
|
|
910
|
+
console.error("[session-source-backfill] startup run failed (non-fatal):", err);
|
|
911
|
+
});
|
|
901
912
|
})
|
|
902
913
|
.on("error", (err) => {
|
|
903
914
|
console.error("HTTP Server Error:", err);
|