@desplega.ai/agent-swarm 2.5.4 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/{acp-adapter-ek38cwsv.js → acp-adapter-ma1js5n7.js} +9 -9
- package/dist/{actions-92mepq03.js → actions-1vhmtvg9.js} +21 -21
- package/dist/{amp-adapter-9gaxg015.js → amp-adapter-sxqqvmdk.js} +4 -4
- package/dist/{app-ctn8gsgx.js → app-mwx1d5a9.js} +3 -3
- package/dist/{assistant-zyr5kz47.js → assistant-yx1p6g82.js} +30 -26
- package/dist/{boot-reembed-w75maga2.js → boot-reembed-5h37a2c2.js} +1 -1
- package/dist/{boot-reembed-dwzfttdr.js → boot-reembed-7h78vmvv.js} +2 -1
- package/dist/{boot-scrub-sweep-e9kq43kf.js → boot-scrub-sweep-8mawd4gw.js} +4 -4
- package/dist/{boot-seeds-x28v3fhb.js → boot-seeds-ca84dhnn.js} +4 -4
- package/dist/{claude-adapter-f0afqnne.js → claude-adapter-wqt4hfkq.js} +9 -9
- package/dist/{claude-managed-adapter-x81a7r65.js → claude-managed-adapter-6fjqtr2z.js} +2 -2
- package/dist/{claude-managed-setup-bqzwjd9x.js → claude-managed-setup-rqv98sea.js} +10 -3
- package/dist/{claude-sdk-session-7b7whkaq.js → claude-sdk-session-f29f3b2x.js} +11 -11
- package/dist/{cli-sxgtf5fw.js → cli-07q03xm2.js} +1 -1
- package/dist/{cli-kxpfx2xr.js → cli-0z1xjzwa.js} +2 -2
- package/dist/{cli-g9vd9ece.js → cli-1hhbzkbb.js} +14 -16
- package/dist/{cli-qqzmjxkf.js → cli-1m9x8dr5.js} +11 -6
- package/dist/{cli-jbnn4vxb.js → cli-2pvzmas1.js} +3 -3
- package/dist/{cli-x77gen2p.js → cli-2y7v133v.js} +7 -7
- package/dist/{cli-dew6x255.js → cli-39aet655.js} +2 -2
- package/dist/{cli-g1cw7xtt.js → cli-39rrw8yp.js} +1 -1
- package/dist/{cli-jb30dzvk.js → cli-48gxjmcz.js} +530 -190
- package/dist/{cli-pzatvw94.js → cli-54sj64fc.js} +2 -2
- package/dist/{cli-bvpgr9ny.js → cli-60sty27p.js} +5 -5
- package/dist/{cli-asq5ydm4.js → cli-6a9qfa8b.js} +411 -69
- package/dist/{cli-277c31dw.js → cli-6mgdba42.js} +225 -3
- package/dist/{cli-19202kew.js → cli-6srv653c.js} +5 -5
- package/dist/{cli-n4c8e3s5.js → cli-708wdy32.js} +1 -1
- package/dist/{cli-pasyvjmm.js → cli-737pyqxr.js} +1 -1
- package/dist/{cli-6x9ma5w5.js → cli-803188pb.js} +1 -1
- package/dist/{cli-hh5we352.js → cli-86ehn4s5.js} +7 -7
- package/dist/{cli-9s05422w.js → cli-8mbc9za9.js} +1 -1
- package/dist/{cli-ff1tm9n9.js → cli-8wmj18cz.js} +1 -1
- package/dist/{cli-1r5ad7j5.js → cli-9gsm2rzp.js} +12 -12
- package/dist/{cli-mz9afxxf.js → cli-9pshx6rd.js} +17 -3
- package/dist/{cli-s682zgqz.js → cli-a6ds2epe.js} +1 -1
- package/dist/{cli-6a1xg1p7.js → cli-a89zdqz9.js} +2 -2
- package/dist/{cli-03vpq45j.js → cli-ay40dnj3.js} +2 -2
- package/dist/{cli-tsma7p8v.js → cli-aypd09nf.js} +2 -2
- package/dist/{cli-ethgy7ha.js → cli-b3yzyc57.js} +2 -2
- package/dist/{cli-bn0rewsy.js → cli-c081bj3b.js} +2 -2
- package/dist/{cli-mmhj8vd7.js → cli-c668vvcf.js} +11 -2
- package/dist/{cli-sbwsxvtd.js → cli-d20qvpr2.js} +2 -2
- package/dist/{cli-c952c7ac.js → cli-e1595bgx.js} +3 -3
- package/dist/{cli-621ce7kw.js → cli-f12mr20z.js} +3 -3
- package/dist/{cli-dqygeeh5.js → cli-f1e0ptjj.js} +10 -1
- package/dist/{cli-xzfq4at3.js → cli-f4dbh7a5.js} +16 -12
- package/dist/{cli-ha9rmwgq.js → cli-fb8m699c.js} +2 -2
- package/dist/{cli-5bnsc0e1.js → cli-gsfk58nw.js} +5 -5
- package/dist/cli-gt4h8cxw.js +895 -0
- package/dist/{cli-nwyw3zhg.js → cli-gxee0bqa.js} +1 -1
- package/dist/{cli-25n3mv5d.js → cli-h5jeg46p.js} +6 -6
- package/dist/{cli-gn5af0x5.js → cli-he9gbdx0.js} +62 -12
- package/dist/{cli-gbfxp48k.js → cli-j0ppywdr.js} +42 -35
- package/dist/{cli-k4tjsdx9.js → cli-j3y0x2n4.js} +277 -199
- package/dist/{cli-7p4jxgtt.js → cli-j9bpmykh.js} +75 -27
- package/dist/{cli-dabehxhp.js → cli-kt907da6.js} +1 -1
- package/dist/{cli-wx2nj19m.js → cli-kv7sbc2c.js} +6 -6
- package/dist/{cli-abs8ndtr.js → cli-m30k9777.js} +26 -26
- package/dist/{cli-cz5nmdxt.js → cli-m5e802r7.js} +2 -2
- package/dist/{cli-0shx9j93.js → cli-pfk92eqz.js} +2 -2
- package/dist/{cli-xtrz6bzs.js → cli-ptfq202x.js} +5 -3
- package/dist/{cli-6n8axjv1.js → cli-qj442fws.js} +2 -2
- package/dist/{cli-c61mbdds.js → cli-rd5m70gj.js} +4 -4
- package/dist/{cli-eftzyh9y.js → cli-s09j9vp5.js} +2 -2
- package/dist/{cli-m4ecdxvz.js → cli-s0mgx214.js} +7 -7
- package/dist/{cli-wghjkr0e.js → cli-sgqm2582.js} +10 -7
- package/dist/{cli-2655k16q.js → cli-sjctjr48.js} +2 -2
- package/dist/{cli-nzfnt2gs.js → cli-tf1n0r0b.js} +1 -1
- package/dist/{cli-x0f2v69c.js → cli-twctnkng.js} +12 -7
- package/dist/{cli-j6rktc91.js → cli-v6shg7na.js} +1 -1
- package/dist/{cli-gx4rm0qx.js → cli-vz78s5wm.js} +1 -1
- package/dist/{cli-gwhjmh8g.js → cli-wqr7cea4.js} +5 -5
- package/dist/{cli-e42vss7b.js → cli-x37r7qwf.js} +16 -8
- package/dist/{cli-4b2z6n0m.js → cli-yd2nreqw.js} +17 -8
- package/dist/{cli-64mh9mf3.js → cli-ymjpkxgn.js} +69 -17
- package/dist/{cli-5kqnemen.js → cli-yvy48785.js} +1 -1
- package/dist/{cli-9j129brq.js → cli-yxbsmx56.js} +6 -6
- package/dist/cli.js +22 -20
- package/dist/{codex-adapter-wch12q99.js → codex-adapter-zk59x2zh.js} +7 -7
- package/dist/{codex-hook-brpnycn5.js → codex-hook-xyjhvxwd.js} +1 -1
- package/dist/{codex-login-rn7c5gxt.js → codex-login-8cg0vhf2.js} +1 -1
- package/dist/{codex-session-runner-41pfcr95.js → codex-session-runner-kh6s4j3n.js} +8 -8
- package/dist/{commands-9849dkj6.js → commands-68et1qv2.js} +2 -2
- package/dist/{controller-ja0sd95p.js → controller-fyrbnzg4.js} +4 -4
- package/dist/{controllers-connect-xp05dj6x.js → controllers-connect-nep9h2xv.js} +23 -23
- package/dist/{cursor-adapter-623qxqew.js → cursor-adapter-yss3pg7t.js} +2 -2
- package/dist/{db-w759qmx0.js → db-28gjyvxj.js} +4 -2
- package/dist/{db-47vj40dy.js → db-8ftwvzba.js} +3 -3
- package/dist/{devin-adapter-4ycy4vrf.js → devin-adapter-byen2cxt.js} +1 -1
- package/dist/{dispatcher-7b8e4z2j.js → dispatcher-hayexmkr.js} +20 -20
- package/dist/{docker-s84hjs91.js → docker-fdzyewpz.js} +2 -2
- package/dist/{dsh-adapter-2ezw3xea.js → dsh-adapter-46wawff9.js} +2 -2
- package/dist/{e2b-9gkdhm3c.js → e2b-222hsajc.js} +2 -2
- package/dist/{e2b-cjtf8k1z.js → e2b-fn3zacvt.js} +2 -2
- package/dist/{easter-egg-3d-e4qv8dxj.js → easter-egg-3d-8d1e0h4m.js} +1 -1
- package/dist/{event-bus-3z78ep5n.js → event-bus-pghg8hqy.js} +1 -1
- package/dist/{execute-0xpz8950.js → execute-cjrfxfnm.js} +1 -1
- package/dist/{external-channel-e2s028da.js → external-channel-xbmxcg09.js} +2 -2
- package/dist/{grok-adapter-ys7gaqee.js → grok-adapter-ag4wks2a.js} +10 -10
- package/dist/handlers-tzmevqb5.js +41 -0
- package/dist/{heartbeat-w3mntghj.js → heartbeat-psfvcgwp.js} +23 -23
- package/dist/{hook-g7kvv6x9.js → hook-8pz04697.js} +6 -6
- package/dist/{hook-3jjywnpb.js → hook-cyq4mryn.js} +8 -8
- package/dist/{http-tkp7d4kb.js → http-haj1nd6k.js} +921 -169
- package/dist/{index-q7m26kvs.js → index-jwt2bj6f.js} +265 -97
- package/dist/{index-3n8m9ea2.js → index-q52f47pe.js} +1 -1
- package/dist/{index-31ceyf42.js → index-syvhnvtw.js} +1 -1
- package/dist/index-y1ajx04a.js +43 -0
- package/dist/{index-1ef1myq2.js → index-z5h22pes.js} +20 -20
- package/dist/{index-xdaahthb.js → index-zyq8ct52.js} +20 -20
- package/dist/{keepalive-v4ta8wgg.js → keepalive-xdvsamvj.js} +4 -4
- package/dist/lead-hqg57kdp.js +79 -0
- package/dist/{maintenance-84n1ex21.js → maintenance-4y2xzh3a.js} +1 -1
- package/dist/{oauth-refresh-sweep-vjnnandw.js → oauth-refresh-sweep-jd4r7zbs.js} +9 -4
- package/dist/{onboard-sfmpe4np.js → onboard-18447q9q.js} +6 -6
- package/dist/{opencode-adapter-jmpst4q4.js → opencode-adapter-26b2frdf.js} +7 -7
- package/dist/{otel-impl-v6cc0zve.js → otel-impl-f9tgr8yw.js} +2 -2
- package/dist/{pi-mono-adapter-fk8fkcf6.js → pi-mono-adapter-qn6qbga1.js} +38 -17
- package/dist/{pricing-refresh-fsk1e1zc.js → pricing-refresh-dw2tgb53.js} +3 -3
- package/dist/{rbac-roles-prmkdnkw.js → rbac-roles-aygm2760.js} +2 -2
- package/dist/{rbac-roles-zxmxqa39.js → rbac-roles-s4nwj2m3.js} +1 -1
- package/dist/render-v2-yfvdfr0x.js +16 -0
- package/dist/{resume-frdtgfa0.js → resume-1bmvjbf3.js} +9 -9
- package/dist/{runtime-bt5pkpm1.js → runtime-f7tpzedy.js} +1 -1
- package/dist/{session-access-fpez4y5q.js → session-access-r5garej6.js} +4 -4
- package/dist/session-source-backfill-424wzv0r.js +110 -0
- package/dist/{setup-mq6m3m8q.js → setup-8tygvxkq.js} +1 -1
- package/dist/{virtual-modules-mnfbh5mr.js → virtual-modules-xsw323b3.js} +2 -2
- package/dist/{work-objects-kfahhjsq.js → work-objects-ggnghpgf.js} +23 -23
- package/dist/worker-sqjd1nqf.js +79 -0
- package/dist/{x-pbpezr0s.js → x-wxrzvzx8.js} +2 -2
- package/openapi.json +1324 -14
- package/package.json +5 -3
- package/src/assets/source-key.ts +95 -0
- package/src/be/automation-preflight.ts +2 -0
- package/src/be/db/api-keys.ts +89 -0
- package/src/be/db/session-labels.ts +28 -0
- package/src/be/db/session-moves.ts +5 -6
- package/src/be/db/session-tree.ts +153 -14
- package/src/be/db/tasks/read.ts +11 -0
- package/src/be/db/tasks/write.ts +6 -1
- package/src/be/db/workflow-runs.ts +219 -6
- package/src/be/db-retention.ts +61 -6
- package/src/be/db.ts +135 -132
- package/src/be/memory/providers/openai-embedding.ts +5 -1
- package/src/be/memory/task-learning.ts +155 -0
- package/src/be/migrations/244_api_key_validation.sql +4 -0
- package/src/be/migrations/245_script_executor.sql +3 -0
- package/src/be/oauth-refresh-sweep.ts +2 -1
- package/src/be/onboarding.ts +4 -0
- package/src/be/scripts/boot-reembed.ts +2 -0
- package/src/be/scripts/db.ts +14 -4
- package/src/be/scripts/embeddings.ts +2 -0
- package/src/be/scripts/run-saved.ts +1 -0
- package/src/be/scripts/typecheck.ts +10 -4
- package/src/be/seed-skills/index.ts +7 -0
- package/src/be/session-source-backfill.ts +163 -0
- package/src/be/swarm-config-guard.ts +1 -0
- package/src/comb/review-batch.ts +28 -1
- package/src/comb/templates.ts +7 -2
- package/src/commands/claude-managed-setup.ts +7 -1
- package/src/commands/credential-refresh.ts +76 -1
- package/src/commands/credential-wait.ts +2 -2
- package/src/commands/onboard/compose-generator.ts +5 -3
- package/src/commands/provider-credentials.ts +212 -171
- package/src/commands/runner.ts +25 -4
- package/src/extensions/apply-task-create.ts +8 -1
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +5 -0
- package/src/heartbeat/heartbeat.ts +14 -1
- package/src/heartbeat/script-runner-probe.ts +47 -0
- package/src/http/all-routes.ts +2 -0
- package/src/http/api-keys.ts +77 -9
- package/src/http/assets.ts +15 -0
- package/src/http/codex-oauth-accounts.ts +191 -0
- package/src/http/comb.ts +15 -2
- package/src/http/config.ts +1 -1
- package/src/http/core.ts +6 -0
- package/src/http/fleet-benchmark.ts +5 -2
- package/src/http/idempotency-key.ts +1 -1
- package/src/http/index.ts +14 -3
- package/src/http/integrations.ts +5 -1
- package/src/http/kv.ts +8 -3
- package/src/http/onboarding.ts +6 -1
- package/src/http/pages-public.ts +38 -5
- package/src/http/pool-keys.ts +354 -0
- package/src/http/scripts.ts +27 -0
- package/src/http/sessions.ts +37 -12
- package/src/http/stats.ts +1 -0
- package/src/http/status.ts +26 -12
- package/src/http/tasks.ts +3 -0
- package/src/http/used-by.ts +7 -2
- package/src/http/workflows.ts +88 -9
- package/src/http/x.ts +5 -0
- package/src/integrations/chat-sdk/inbound.ts +27 -0
- package/src/kv-reserved-namespaces.ts +19 -0
- package/src/prompts/base-prompt.ts +21 -1
- package/src/prompts/session-templates.ts +17 -3
- package/src/providers/acp-targets.ts +1 -1
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/claude-adapter.ts +5 -1
- package/src/providers/claude-managed-adapter.ts +16 -9
- package/src/providers/codex-adapter.ts +71 -11
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/providers/pi-mono-adapter.ts +41 -11
- package/src/scheduler/scheduler.ts +5 -3
- package/src/scripts-runtime/eval-harness.ts +8 -1
- package/src/scripts-runtime/executors/native.ts +22 -16
- package/src/scripts-runtime/executors/quickjs-prelude.ts +103 -1
- package/src/scripts-runtime/executors/quickjs-runner.ts +2 -1
- package/src/scripts-runtime/executors/quickjs.ts +7 -0
- package/src/scripts-runtime/executors/registry.ts +32 -0
- package/src/scripts-runtime/executors/types.ts +7 -0
- package/src/scripts-runtime/host-validated-args.ts +133 -0
- package/src/scripts-runtime/loader.ts +30 -3
- package/src/scripts-runtime/swarm-sdk.ts +16 -3
- package/src/scripts-runtime/types/stdlib.d.ts +10 -2
- package/src/scripts-runtime/types/swarm-extension.d.ts +17 -8
- package/src/scripts-runtime/types/swarm-sdk.d.ts +10 -2
- package/src/slack/assistant.ts +7 -0
- package/src/slack/channel-labels.ts +74 -0
- package/src/slack/handlers.ts +8 -0
- package/src/slack/inbound-files.ts +5 -2
- package/src/slack/thread-buffer.ts +4 -0
- package/src/tasks/sibling-awareness.ts +12 -3
- package/src/telemetry-context.ts +5 -6
- package/src/telemetry-identity.ts +3 -3
- package/src/tests/api-key-status-by-suffix.test.ts +242 -0
- package/src/tests/api-keys-validation.test.ts +170 -0
- package/src/tests/base-prompt.test.ts +70 -2
- package/src/tests/claude-managed-adapter.test.ts +46 -0
- package/src/tests/claude-routes-e2e.test.ts +7 -2
- package/src/tests/codex-credential-pools.test.ts +120 -0
- package/src/tests/codex-gateway-route.test.ts +165 -0
- package/src/tests/codex-oauth-accounts.test.ts +357 -0
- package/src/tests/comb-review-batch.test.ts +63 -2
- package/src/tests/community-linear-delivery-loop.test.ts +2099 -0
- package/src/tests/config-pool-keys.test.ts +533 -0
- package/src/tests/credential-check.test.ts +294 -4
- package/src/tests/credential-refresh.test.ts +172 -1
- package/src/tests/db-retention.test.ts +9 -3
- package/src/tests/db-task-read-contract.test.ts +1 -0
- package/src/tests/db-task-write-contract.test.ts +1 -3
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/docs-index.test.ts +135 -0
- package/src/tests/dsh-adapter.test.ts +1 -1
- package/src/tests/extensions-pre-task.test.ts +53 -1
- package/src/tests/fetch-resolved-env.test.ts +16 -2
- package/src/tests/fixtures/extensions/record-task-create/hooks.ts +9 -0
- package/src/tests/fixtures/extensions/record-task-create/manifest.json +7 -0
- package/src/tests/fleet-benchmark.test.ts +8 -0
- package/src/tests/get-workflow-run-tool.test.ts +98 -0
- package/src/tests/heartbeat-checklist.test.ts +89 -0
- package/src/tests/heartbeat-script-runner-probe.test.ts +83 -0
- package/src/tests/integrations-http.test.ts +32 -2
- package/src/tests/internal-ai/complete-structured.test.ts +37 -1
- package/src/tests/internal-ai/credentials.test.ts +23 -0
- package/src/tests/kv-http.test.ts +32 -0
- package/src/tests/kv-tool.test.ts +25 -0
- package/src/tests/multi-runtime-registration.test.ts +12 -12
- package/src/tests/oauth-refresh-sweep.test.ts +34 -1
- package/src/tests/onboard-compose.test.ts +20 -0
- package/src/tests/onboarding.test.ts +108 -1
- package/src/tests/openai-embedding-credentials.test.ts +35 -0
- package/src/tests/opencode-adapter.test.ts +2 -1
- package/src/tests/openrouter-attribution-call-sites.test.ts +2 -2
- package/src/tests/org-website.test.ts +36 -0
- package/src/tests/pages-frame-ancestors.test.ts +162 -0
- package/src/tests/pi-codemode.test.ts +2 -1
- package/src/tests/pi-mono-adapter.test.ts +54 -0
- package/src/tests/pi-native-mcp.test.ts +13 -1
- package/src/tests/pi-tool-exposure.test.ts +8 -8
- package/src/tests/probe-tool-schema-tokens.test.ts +144 -0
- package/src/tests/prompt-template-session.test.ts +4 -3
- package/src/tests/provider-key-probes.test.ts +181 -0
- package/src/tests/rbac-e2e-helpers.test.ts +88 -1
- package/src/tests/rbac-e2e-helpers.ts +36 -11
- package/src/tests/request-composition-derive.test.ts +329 -0
- package/src/tests/script-connections.test.ts +4 -0
- package/src/tests/script-executor-conformance.test.ts +36 -1
- package/src/tests/script-executor-quickjs-pool.test.ts +50 -0
- package/src/tests/script-executor-quickjs.test.ts +140 -0
- package/src/tests/script-host-validated-args-native.test.ts +196 -0
- package/src/tests/script-per-script-executor.test.ts +415 -0
- package/src/tests/script-runs-retention.test.ts +212 -0
- package/src/tests/scripts-external-api.test.ts +45 -0
- package/src/tests/sdk-allowlist.test.ts +42 -2
- package/src/tests/self-improvement.test.ts +4 -4
- package/src/tests/session-source-backfill.test.ts +429 -0
- package/src/tests/session-source-keys.test.ts +236 -0
- package/src/tests/session-source-labels.test.ts +419 -0
- package/src/tests/sessions-detail-batch.test.ts +2 -0
- package/src/tests/sessions-move.test.ts +131 -0
- package/src/tests/sessions-tree.test.ts +299 -2
- package/src/tests/slack-inbound-files.test.ts +103 -23
- package/src/tests/status.test.ts +101 -18
- package/src/tests/store-progress-learning.test.ts +288 -0
- package/src/tests/tasks-workflow-run-filter.test.ts +146 -0
- package/src/tests/test-net.ts +31 -0
- package/src/tests/transcription.test.ts +8 -0
- package/src/tests/ui-logs-parser.test.ts +180 -0
- package/src/tests/used-by-route.test.ts +11 -0
- package/src/tests/workflow-cascade-cancel.test.ts +172 -0
- package/src/tests/workflow-engine-v2.test.ts +116 -1
- package/src/tests/workflow-http-v2.test.ts +131 -0
- package/src/tests/workflow-mcp-trigger-schema.test.ts +209 -0
- package/src/tests/workflow-run-finished-event.test.ts +263 -0
- package/src/tests/workflow-step-order.test.ts +92 -0
- package/src/tests/workflow-sub-workflow.test.ts +30 -0
- package/src/tests/workflow-swarm-script.test.ts +34 -0
- package/src/tools/get-tasks.ts +8 -0
- package/src/tools/kv/kv-read-auth.ts +4 -2
- package/src/tools/script-common.ts +1 -0
- package/src/tools/script-run.ts +7 -0
- package/src/tools/script-upsert.ts +7 -0
- package/src/tools/store-progress.ts +67 -1
- package/src/tools/task-action.ts +1 -1
- package/src/tools/workflows/get-workflow-run.ts +14 -4
- package/src/tools/workflows/trigger-workflow.ts +104 -10
- package/src/types.ts +36 -1
- package/src/utils/company-profile.ts +51 -1
- package/src/utils/credentials.ts +10 -4
- package/src/utils/internal-ai/complete-structured.ts +5 -0
- package/src/utils/internal-ai/credentials.ts +4 -3
- package/src/utils/provider-key-probes.ts +328 -0
- package/src/utils/transcription.ts +4 -1
- package/src/workflows/engine.ts +18 -16
- package/src/workflows/executors/sub-workflow.ts +8 -1
- package/src/workflows/executors/swarm-script.ts +5 -0
- package/src/workflows/foreach-driver.ts +5 -11
- package/src/workflows/node-timeout.ts +30 -0
- package/templates/ai-toolbox.manifest.json +2 -2
- package/templates/community/linear-delivery-loop/PLAYBOOK.md +323 -0
- package/templates/community/linear-delivery-loop/README.md +38 -0
- package/templates/community/linear-delivery-loop/config.example.json +31 -0
- package/templates/community/linear-delivery-loop/lead-prompt.md +34 -0
- package/templates/community/linear-delivery-loop/schedule.json +10 -0
- package/templates/community/linear-delivery-loop/scripts/linear-delivery-setup.ts +148 -0
- package/templates/community/linear-delivery-loop/scripts/linear-delivery-tick.ts +1781 -0
- package/templates/skills/researching/content.md +2 -0
- package/templates/skills/swarm-docs/config.json +14 -0
- package/templates/skills/swarm-docs/content.md +144 -0
- package/templates/skills/swarm-scripts/SKILL.md +1 -0
- package/templates/skills/swarm-scripts/content.md +1 -0
- package/dist/cli-7421jzsw.js +0 -84
- package/dist/cli-e45e4m6b.js +0 -413
- package/dist/cli-zw88zjba.js +0 -307
- package/dist/handlers-wy1dr6ja.js +0 -41
- package/dist/index-nmm79qgr.js +0 -43
- package/dist/lead-sawnyqxk.js +0 -79
- package/dist/render-v2-pjwxvnkw.js +0 -12
- package/dist/worker-v5e5rxj3.js +0 -79
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test";
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { CodexAdapter } from "../providers/codex-adapter";
|
|
6
|
+
import type { ProviderSessionConfig } from "../providers/types";
|
|
7
|
+
|
|
8
|
+
const keyVars = ["OPENAI_API_KEY", "OPENROUTER_API_KEY", "ANTHROPIC_API_KEY"];
|
|
9
|
+
let savedEnv: Record<string, string | undefined>;
|
|
10
|
+
let home: string;
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
savedEnv = Object.fromEntries(["HOME", ...keyVars].map((key) => [key, process.env[key]]));
|
|
14
|
+
home = await mkdtemp(join(tmpdir(), "credential-validation-codex-"));
|
|
15
|
+
process.env.HOME = home;
|
|
16
|
+
for (const key of keyVars) process.env[key] = "example-first, example-second";
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
for (const [key, value] of Object.entries(savedEnv)) {
|
|
21
|
+
if (value === undefined) delete process.env[key];
|
|
22
|
+
else process.env[key] = value;
|
|
23
|
+
}
|
|
24
|
+
await rm(home, { recursive: true, force: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function config(env?: Record<string, string>): ProviderSessionConfig {
|
|
28
|
+
return {
|
|
29
|
+
agentId: crypto.randomUUID(),
|
|
30
|
+
prompt: "test",
|
|
31
|
+
systemPrompt: "",
|
|
32
|
+
cwd: home,
|
|
33
|
+
logFile: "/dev/null",
|
|
34
|
+
env,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe("Codex credential pools", () => {
|
|
39
|
+
for (const source of ["container", "task", "task-pool"] as const) {
|
|
40
|
+
test(`app-server uses one OpenAI key from ${source}`, async () => {
|
|
41
|
+
let capturedEnv: Record<string, string> | undefined;
|
|
42
|
+
const adapter = new CodexAdapter({
|
|
43
|
+
bypassSubprocess: true,
|
|
44
|
+
appServerFactory: ({ env }) => {
|
|
45
|
+
capturedEnv = env;
|
|
46
|
+
throw new Error("test stops before app-server spawn");
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
const env =
|
|
50
|
+
source === "container"
|
|
51
|
+
? {}
|
|
52
|
+
: {
|
|
53
|
+
OPENAI_API_KEY:
|
|
54
|
+
source === "task" ? "example-task" : "example-task-first, example-task-second",
|
|
55
|
+
};
|
|
56
|
+
await expect(adapter.createSession(config(env))).rejects.toThrow(
|
|
57
|
+
"test stops before app-server spawn",
|
|
58
|
+
);
|
|
59
|
+
expect(
|
|
60
|
+
source === "container"
|
|
61
|
+
? ["example-first", "example-second"]
|
|
62
|
+
: source === "task"
|
|
63
|
+
? ["example-task"]
|
|
64
|
+
: ["example-task-first", "example-task-second"],
|
|
65
|
+
).toContain(capturedEnv?.OPENAI_API_KEY);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test(`subprocess forwards one key per provider from ${source}`, async () => {
|
|
69
|
+
let capturedEnv: Record<string, string | undefined> | undefined;
|
|
70
|
+
const spawn = spyOn(Bun, "spawn").mockImplementation(((
|
|
71
|
+
...args: Parameters<typeof Bun.spawn>
|
|
72
|
+
) => {
|
|
73
|
+
capturedEnv = args[1]?.env;
|
|
74
|
+
throw new Error("test stops before subprocess spawn");
|
|
75
|
+
}) as typeof Bun.spawn);
|
|
76
|
+
try {
|
|
77
|
+
const env =
|
|
78
|
+
source === "container"
|
|
79
|
+
? {}
|
|
80
|
+
: Object.fromEntries(
|
|
81
|
+
keyVars.map((key) => [
|
|
82
|
+
key,
|
|
83
|
+
source === "task" ? "example-task" : "example-task-first, example-task-second",
|
|
84
|
+
]),
|
|
85
|
+
);
|
|
86
|
+
await expect(new CodexAdapter().createSession(config(env))).rejects.toThrow(
|
|
87
|
+
"test stops before subprocess spawn",
|
|
88
|
+
);
|
|
89
|
+
for (const key of keyVars) {
|
|
90
|
+
expect(
|
|
91
|
+
source === "container"
|
|
92
|
+
? ["example-first", "example-second"]
|
|
93
|
+
: source === "task"
|
|
94
|
+
? ["example-task"]
|
|
95
|
+
: ["example-task-first", "example-task-second"],
|
|
96
|
+
).toContain(capturedEnv?.[key]);
|
|
97
|
+
}
|
|
98
|
+
} finally {
|
|
99
|
+
spawn.mockRestore();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
test("chatgpt mode still removes the OpenAI API key", async () => {
|
|
105
|
+
await Bun.write(join(home, ".codex", "auth.json"), JSON.stringify({ auth_mode: "chatgpt" }));
|
|
106
|
+
let capturedEnv: Record<string, string> | undefined;
|
|
107
|
+
const adapter = new CodexAdapter({
|
|
108
|
+
bypassSubprocess: true,
|
|
109
|
+
appServerFactory: ({ env }) => {
|
|
110
|
+
capturedEnv = env;
|
|
111
|
+
throw new Error("test stops before app-server spawn");
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
await expect(adapter.createSession(config({ OPENAI_API_KEY: "example-task" }))).rejects.toThrow(
|
|
115
|
+
"test stops before app-server spawn",
|
|
116
|
+
);
|
|
117
|
+
expect(capturedEnv).toBeDefined();
|
|
118
|
+
expect(capturedEnv?.OPENAI_API_KEY).toBeUndefined();
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex gateway route: `CODEX_GATEWAY_BASE_URL` + `CODEX_GATEWAY_API_KEY`
|
|
3
|
+
* send Codex through an OpenAI Responses-compatible gateway. Covers route
|
|
4
|
+
* selection in the adapter, the presence gate, and the live check.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { afterEach, describe, expect, spyOn, test } from "bun:test";
|
|
8
|
+
import { validateProviderCredentials } from "../commands/provider-credentials";
|
|
9
|
+
import { CodexAdapter, checkCodexCredentials } from "../providers/codex-adapter";
|
|
10
|
+
import type { ProviderSessionConfig } from "../providers/types";
|
|
11
|
+
|
|
12
|
+
const GATEWAY_ENV = {
|
|
13
|
+
CODEX_GATEWAY_BASE_URL: "https://sub2api.example.com/v1",
|
|
14
|
+
CODEX_GATEWAY_API_KEY: "sk-gateway",
|
|
15
|
+
};
|
|
16
|
+
const OAUTH_BLOB = JSON.stringify({
|
|
17
|
+
access: "oauth-access",
|
|
18
|
+
refresh: "r",
|
|
19
|
+
expires: 1,
|
|
20
|
+
accountId: "a",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
function makeFakeThread() {
|
|
24
|
+
return {
|
|
25
|
+
id: null as string | null,
|
|
26
|
+
async runStreamed() {
|
|
27
|
+
async function* generate() {
|
|
28
|
+
yield { type: "turn.completed" as const };
|
|
29
|
+
}
|
|
30
|
+
return { events: generate() };
|
|
31
|
+
},
|
|
32
|
+
async steer() {},
|
|
33
|
+
async interrupt() {},
|
|
34
|
+
async close() {},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function testConfig(overrides: Partial<ProviderSessionConfig> = {}): ProviderSessionConfig {
|
|
39
|
+
return {
|
|
40
|
+
prompt: "hello",
|
|
41
|
+
systemPrompt: "",
|
|
42
|
+
model: "gpt-5.4",
|
|
43
|
+
role: "worker",
|
|
44
|
+
agentId: "11111111-1111-4111-8111-111111111111",
|
|
45
|
+
apiUrl: "http://127.0.0.1:9",
|
|
46
|
+
apiKey: "test",
|
|
47
|
+
cwd: "/tmp",
|
|
48
|
+
logFile: `/tmp/codex-gateway-route-test-${Date.now()}-${Math.random().toString(36).slice(2)}.log`,
|
|
49
|
+
...overrides,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function captureSpawn(
|
|
54
|
+
env: Record<string, string>,
|
|
55
|
+
extra: Partial<ProviderSessionConfig> = {},
|
|
56
|
+
) {
|
|
57
|
+
let captured: { env: Record<string, string>; codexConfig: Record<string, unknown> } | undefined;
|
|
58
|
+
const config = testConfig({ env, ...extra });
|
|
59
|
+
const adapter = new CodexAdapter({
|
|
60
|
+
bypassSubprocess: true,
|
|
61
|
+
threadFactory: ({ env: spawnEnv, codexConfig }) => {
|
|
62
|
+
captured = { env: spawnEnv, codexConfig };
|
|
63
|
+
return makeFakeThread();
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
const session = await adapter.createSession(config);
|
|
67
|
+
await session.waitForCompletion();
|
|
68
|
+
await Bun.file(config.logFile).delete();
|
|
69
|
+
if (!captured) throw new Error("thread factory not called");
|
|
70
|
+
return captured;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const realFetch = globalThis.fetch;
|
|
74
|
+
afterEach(() => {
|
|
75
|
+
globalThis.fetch = realFetch;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("codex gateway route: adapter", () => {
|
|
79
|
+
test("adds a Responses provider block and keeps OAuth and OPENAI_API_KEY out of the CLI env", async () => {
|
|
80
|
+
const log = spyOn(console, "log");
|
|
81
|
+
// codexSlot set: without the gateway route this would revalidate the pool
|
|
82
|
+
// slot against the (unreachable) API and throw.
|
|
83
|
+
const { env, codexConfig } = await captureSpawn(
|
|
84
|
+
{ ...GATEWAY_ENV, CODEX_OAUTH: OAUTH_BLOB, OPENAI_API_KEY: "sk-openai" },
|
|
85
|
+
{ codexSlot: 0 },
|
|
86
|
+
);
|
|
87
|
+
const logged = log.mock.calls.map((c) => String(c[0]));
|
|
88
|
+
log.mockRestore();
|
|
89
|
+
|
|
90
|
+
expect(codexConfig.model_provider).toBe("swarm-gateway");
|
|
91
|
+
expect(codexConfig.model_providers).toEqual({
|
|
92
|
+
"swarm-gateway": {
|
|
93
|
+
name: "Swarm gateway",
|
|
94
|
+
base_url: "https://sub2api.example.com/v1",
|
|
95
|
+
env_key: "CODEX_GATEWAY_API_KEY",
|
|
96
|
+
wire_api: "responses",
|
|
97
|
+
requires_openai_auth: false,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
expect(env.CODEX_GATEWAY_API_KEY).toBe("sk-gateway");
|
|
101
|
+
expect(env.CODEX_OAUTH).toBeUndefined();
|
|
102
|
+
expect(env.OPENAI_API_KEY).toBeUndefined();
|
|
103
|
+
expect(logged.some((line) => line.endsWith("Using credential: gateway"))).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("a gateway URL on api.openai.com is not a gateway: no provider block", async () => {
|
|
107
|
+
const { codexConfig, env } = await captureSpawn({
|
|
108
|
+
CODEX_GATEWAY_BASE_URL: "https://api.openai.com/v1",
|
|
109
|
+
CODEX_GATEWAY_API_KEY: "k",
|
|
110
|
+
OPENAI_API_KEY: "sk-openai",
|
|
111
|
+
});
|
|
112
|
+
expect(codexConfig.model_provider).toBeUndefined();
|
|
113
|
+
expect(codexConfig.model_providers).toBeUndefined();
|
|
114
|
+
expect(env.OPENAI_API_KEY).toBe("sk-openai");
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("codex gateway route: credential gate", () => {
|
|
119
|
+
const noAuthJson = { homeDir: "/nonexistent", fs: { existsSync: () => false } };
|
|
120
|
+
const withAuthJson = { homeDir: "/home/worker", fs: { existsSync: () => true } };
|
|
121
|
+
|
|
122
|
+
test("the gateway URL + key alone is ready", () => {
|
|
123
|
+
expect(checkCodexCredentials(GATEWAY_ENV, noAuthJson)).toMatchObject({
|
|
124
|
+
ready: true,
|
|
125
|
+
satisfiedBy: "env",
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("a gateway URL without its key is not ready, even with auth.json or OAuth", () => {
|
|
130
|
+
const env = {
|
|
131
|
+
CODEX_GATEWAY_BASE_URL: GATEWAY_ENV.CODEX_GATEWAY_BASE_URL,
|
|
132
|
+
CODEX_OAUTH: OAUTH_BLOB,
|
|
133
|
+
};
|
|
134
|
+
expect(checkCodexCredentials(env, withAuthJson)).toMatchObject({
|
|
135
|
+
ready: false,
|
|
136
|
+
missing: ["CODEX_GATEWAY_API_KEY"],
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("live check calls the gateway's /v1/models with the gateway key, never api.openai.com", async () => {
|
|
141
|
+
const calls: Array<{ url: string; auth: string | undefined }> = [];
|
|
142
|
+
globalThis.fetch = (async (url: string | URL | Request, init?: RequestInit) => {
|
|
143
|
+
const headers = init?.headers as Record<string, string> | undefined;
|
|
144
|
+
calls.push({ url: String(url), auth: headers?.Authorization });
|
|
145
|
+
return new Response(JSON.stringify({ data: [{ id: "gpt-5.5" }] }), { status: 200 });
|
|
146
|
+
}) as typeof fetch;
|
|
147
|
+
|
|
148
|
+
const result = await validateProviderCredentials("codex", {
|
|
149
|
+
...GATEWAY_ENV,
|
|
150
|
+
OPENAI_API_KEY: "sk-openai",
|
|
151
|
+
CODEX_OAUTH: OAUTH_BLOB,
|
|
152
|
+
});
|
|
153
|
+
expect(result.ok).toBe(true);
|
|
154
|
+
expect(result.skipped).toBeUndefined();
|
|
155
|
+
expect(calls).toEqual([
|
|
156
|
+
{ url: "https://sub2api.example.com/v1/models", auth: "Bearer sk-gateway" },
|
|
157
|
+
]);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("live check reports the gateway's 401", async () => {
|
|
161
|
+
globalThis.fetch = (async () => new Response("bad key", { status: 401 })) as typeof fetch;
|
|
162
|
+
const result = await validateProviderCredentials("codex", GATEWAY_ENV);
|
|
163
|
+
expect(result).toMatchObject({ ok: false, httpStatus: 401 });
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
3
|
+
import { Readable } from "node:stream";
|
|
4
|
+
import {
|
|
5
|
+
closeDb,
|
|
6
|
+
createAgent,
|
|
7
|
+
createUser,
|
|
8
|
+
getAvailableKeyIndices,
|
|
9
|
+
getDbClient,
|
|
10
|
+
getSwarmConfigs,
|
|
11
|
+
initDb,
|
|
12
|
+
markKeyRateLimited,
|
|
13
|
+
recordKeyAuthFailure,
|
|
14
|
+
recordKeyUsage,
|
|
15
|
+
upsertKv,
|
|
16
|
+
upsertSwarmConfig,
|
|
17
|
+
} from "../be/db";
|
|
18
|
+
import { mintAgentToken, mintToken } from "../be/users";
|
|
19
|
+
import { type CodexAccountsResponse, handleCodexOAuthAccounts } from "../http/codex-oauth-accounts";
|
|
20
|
+
import { _autoReloadStatsForTests, _resetAutoReloadForTests, handleCore } from "../http/core";
|
|
21
|
+
import { getPathSegments, parseQueryParams } from "../http/utils";
|
|
22
|
+
|
|
23
|
+
const API_KEY = "example-codex-accounts-operator";
|
|
24
|
+
let workerToken: string;
|
|
25
|
+
let leadToken: string;
|
|
26
|
+
let workerId: string;
|
|
27
|
+
let leadId: string;
|
|
28
|
+
|
|
29
|
+
function credentials(userId: string, profile?: Record<string, unknown>) {
|
|
30
|
+
const payload = {
|
|
31
|
+
"https://api.openai.com/auth": {
|
|
32
|
+
chatgpt_user_id: userId,
|
|
33
|
+
chatgpt_account_id: "example-shared-workspace",
|
|
34
|
+
chatgpt_plan_type: "team",
|
|
35
|
+
},
|
|
36
|
+
...(profile ? { "https://api.openai.com/profile": profile } : {}),
|
|
37
|
+
};
|
|
38
|
+
return {
|
|
39
|
+
access: `eyJhbGciOiJub25lIn0.${Buffer.from(JSON.stringify(payload)).toString("base64url")}.signature`,
|
|
40
|
+
refresh: `example-refresh-${userId}`,
|
|
41
|
+
accountId: "example-shared-workspace",
|
|
42
|
+
expires: Date.now() + 3_600_000,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function store(key: string, value: unknown = credentials(`user-${key}`)) {
|
|
47
|
+
return await upsertSwarmConfig({
|
|
48
|
+
scope: "global",
|
|
49
|
+
key,
|
|
50
|
+
value: typeof value === "string" ? value : JSON.stringify(value),
|
|
51
|
+
isSecret: true,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function request(path = "", method = "GET", bearer = API_KEY, agentId?: string) {
|
|
56
|
+
const url = `/api/codex-oauth/accounts${path}`;
|
|
57
|
+
const req = Object.assign(Readable.from([]), {
|
|
58
|
+
method,
|
|
59
|
+
url,
|
|
60
|
+
headers: {
|
|
61
|
+
authorization: `Bearer ${bearer}`,
|
|
62
|
+
...(agentId ? { "x-agent-id": agentId } : {}),
|
|
63
|
+
},
|
|
64
|
+
}) as IncomingMessage;
|
|
65
|
+
let status = 200;
|
|
66
|
+
let body = "";
|
|
67
|
+
const res = {
|
|
68
|
+
setHeader: () => {},
|
|
69
|
+
writeHead: (code: number) => {
|
|
70
|
+
status = code;
|
|
71
|
+
},
|
|
72
|
+
end: (chunk?: string) => {
|
|
73
|
+
body += chunk ?? "";
|
|
74
|
+
},
|
|
75
|
+
} as unknown as ServerResponse;
|
|
76
|
+
if (!(await handleCore(req, res, agentId, API_KEY))) {
|
|
77
|
+
const handled = await handleCodexOAuthAccounts(
|
|
78
|
+
req,
|
|
79
|
+
res,
|
|
80
|
+
getPathSegments(url),
|
|
81
|
+
parseQueryParams(url),
|
|
82
|
+
);
|
|
83
|
+
expect(handled).toBe(true);
|
|
84
|
+
}
|
|
85
|
+
return new Response(body, { status });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function list(): Promise<CodexAccountsResponse> {
|
|
89
|
+
const response = await request();
|
|
90
|
+
expect(response.status).toBe(200);
|
|
91
|
+
return (await response.json()) as CodexAccountsResponse;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
beforeAll(async () => {
|
|
95
|
+
initDb(":memory:");
|
|
96
|
+
workerId = (await createAgent({ name: "account-worker", isLead: false, status: "idle" })).id;
|
|
97
|
+
leadId = (await createAgent({ name: "account-lead", isLead: true, status: "idle" })).id;
|
|
98
|
+
workerToken = (await mintAgentToken(workerId)).plaintext;
|
|
99
|
+
leadToken = (await mintAgentToken(leadId)).plaintext;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
beforeEach(async () => {
|
|
103
|
+
await getDbClient().run("DELETE FROM swarm_config");
|
|
104
|
+
await getDbClient().run("DELETE FROM api_key_status");
|
|
105
|
+
await getDbClient().run("DELETE FROM kv_entries");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
afterEach(() => _resetAutoReloadForTests());
|
|
109
|
+
|
|
110
|
+
afterAll(() => {
|
|
111
|
+
_resetAutoReloadForTests();
|
|
112
|
+
closeDb();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("Codex account HTTP routes", () => {
|
|
116
|
+
test("lists slots in numeric order with metadata and no credentials", async () => {
|
|
117
|
+
const account = credentials("grant-first", { email: "first@example.test" });
|
|
118
|
+
await store("codex_oauth_10", credentials("grant-tenth"));
|
|
119
|
+
await store("codex_oauth_2", account);
|
|
120
|
+
await upsertSwarmConfig({
|
|
121
|
+
scope: "agent",
|
|
122
|
+
scopeId: workerId,
|
|
123
|
+
key: "codex_oauth_1",
|
|
124
|
+
value: JSON.stringify(credentials("agent-only")),
|
|
125
|
+
isSecret: true,
|
|
126
|
+
});
|
|
127
|
+
await recordKeyUsage("CODEX_OAUTH", "first", 2, null);
|
|
128
|
+
const response = await request();
|
|
129
|
+
expect(response.status).toBe(200);
|
|
130
|
+
const text = await response.text();
|
|
131
|
+
expect(text).not.toContain(account.access);
|
|
132
|
+
expect(text).not.toContain(account.refresh);
|
|
133
|
+
expect(text).not.toContain(account.accountId);
|
|
134
|
+
expect(JSON.parse(text)).toEqual({
|
|
135
|
+
accounts: [
|
|
136
|
+
{
|
|
137
|
+
slot: 2,
|
|
138
|
+
configKey: "codex_oauth_2",
|
|
139
|
+
legacy: false,
|
|
140
|
+
suffix: "first",
|
|
141
|
+
plan: "team",
|
|
142
|
+
email: "first@example.test",
|
|
143
|
+
status: "ok",
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
slot: 10,
|
|
147
|
+
configKey: "codex_oauth_10",
|
|
148
|
+
legacy: false,
|
|
149
|
+
suffix: "tenth",
|
|
150
|
+
plan: "team",
|
|
151
|
+
email: null,
|
|
152
|
+
status: "unchecked",
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("uses the legacy row only when slot zero does not exist", async () => {
|
|
159
|
+
await store("codex_oauth");
|
|
160
|
+
expect((await list()).accounts).toMatchObject([
|
|
161
|
+
{ slot: 0, legacy: true, configKey: "codex_oauth" },
|
|
162
|
+
]);
|
|
163
|
+
await store("codex_oauth_0");
|
|
164
|
+
expect((await list()).accounts).toMatchObject([
|
|
165
|
+
{ slot: 0, legacy: false, configKey: "codex_oauth_0" },
|
|
166
|
+
]);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("lists malformed credentials as accounts that need login", async () => {
|
|
170
|
+
await store("codex_oauth_0", "not-json");
|
|
171
|
+
await store("codex_oauth_1", { access: 42, accountId: "example" });
|
|
172
|
+
await store("codex_oauth_2", "null");
|
|
173
|
+
const { accounts } = await list();
|
|
174
|
+
expect(accounts).toHaveLength(3);
|
|
175
|
+
for (const account of accounts) {
|
|
176
|
+
expect(account).toMatchObject({
|
|
177
|
+
suffix: null,
|
|
178
|
+
plan: null,
|
|
179
|
+
email: null,
|
|
180
|
+
status: "needs_login",
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test.each([
|
|
186
|
+
{ refresh: undefined },
|
|
187
|
+
{ refresh: "" },
|
|
188
|
+
{ expires: undefined },
|
|
189
|
+
{ expires: "invalid" },
|
|
190
|
+
{ expires: null },
|
|
191
|
+
])("requires a refresh token and numeric expiry: %j", async (invalid) => {
|
|
192
|
+
await store("codex_oauth_0", { ...credentials("grant-invalid"), ...invalid });
|
|
193
|
+
expect((await list()).accounts).toMatchObject([
|
|
194
|
+
{ slot: 0, suffix: null, plan: null, email: null, status: "needs_login" },
|
|
195
|
+
]);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("rejects a non-finite expiry", async () => {
|
|
199
|
+
const value = JSON.stringify(credentials("grant-invalid")).replace(
|
|
200
|
+
/"expires":\d+/,
|
|
201
|
+
'"expires":1e999',
|
|
202
|
+
);
|
|
203
|
+
await store("codex_oauth_0", value);
|
|
204
|
+
expect((await list()).accounts).toMatchObject([
|
|
205
|
+
{ slot: 0, suffix: null, plan: null, email: null, status: "needs_login" },
|
|
206
|
+
]);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("ignores unrelated unreadable secrets and permits removal of an unreadable account", async () => {
|
|
210
|
+
await store("codex_oauth_0");
|
|
211
|
+
for (const key of ["UNRELATED_SECRET", "codex_oauth_1"]) {
|
|
212
|
+
await store(key);
|
|
213
|
+
await getDbClient().run("UPDATE swarm_config SET value = ?, encrypted = 1 WHERE key = ?", [
|
|
214
|
+
"invalid-ciphertext",
|
|
215
|
+
key,
|
|
216
|
+
]);
|
|
217
|
+
}
|
|
218
|
+
const { accounts } = await list();
|
|
219
|
+
expect(accounts).toHaveLength(2);
|
|
220
|
+
expect(accounts[1]).toMatchObject({ slot: 1, suffix: null, status: "needs_login" });
|
|
221
|
+
const response = await request("/1", "DELETE");
|
|
222
|
+
expect(response.status).toBe(200);
|
|
223
|
+
expect((await response.json()).accounts).toHaveLength(1);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("shows active KV and status auth benches without treating ordinary rate limits as auth failures", async () => {
|
|
227
|
+
await store("codex_oauth_0", credentials("grant-bench"));
|
|
228
|
+
await store("codex_oauth_1", credentials("grant-authf"));
|
|
229
|
+
await store("codex_oauth_2", credentials("grant-ratel"));
|
|
230
|
+
await upsertKv({
|
|
231
|
+
namespace: "codex-auth-watch",
|
|
232
|
+
key: "bench:bench",
|
|
233
|
+
valueType: "string",
|
|
234
|
+
value: "1",
|
|
235
|
+
});
|
|
236
|
+
await recordKeyAuthFailure("CODEX_OAUTH", "authf", 1);
|
|
237
|
+
await recordKeyAuthFailure("CODEX_OAUTH", "authf", 1);
|
|
238
|
+
await getDbClient().run("DELETE FROM kv_entries WHERE key = 'bench:authf'");
|
|
239
|
+
await markKeyRateLimited(
|
|
240
|
+
"CODEX_OAUTH",
|
|
241
|
+
"ratel",
|
|
242
|
+
2,
|
|
243
|
+
new Date(Date.now() + 3_600_000).toISOString(),
|
|
244
|
+
);
|
|
245
|
+
expect((await list()).accounts.map((account) => account.status)).toEqual([
|
|
246
|
+
"needs_login",
|
|
247
|
+
"needs_login",
|
|
248
|
+
"ok",
|
|
249
|
+
]);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("ignores expired KV markers and status records from other scopes", async () => {
|
|
253
|
+
await store("codex_oauth_0", credentials("grant-clear"));
|
|
254
|
+
await upsertKv({
|
|
255
|
+
namespace: "codex-auth-watch",
|
|
256
|
+
key: "bench:clear",
|
|
257
|
+
valueType: "string",
|
|
258
|
+
value: "1",
|
|
259
|
+
expiresAt: Date.now() - 1,
|
|
260
|
+
});
|
|
261
|
+
await recordKeyAuthFailure("CODEX_OAUTH", "clear", 0, "agent", workerId);
|
|
262
|
+
expect((await list()).accounts[0]?.status).toBe("unchecked");
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("requires authentication and permission to read all secrets", async () => {
|
|
266
|
+
await store("codex_oauth_0");
|
|
267
|
+
expect((await request("", "GET", "invalid")).status).toBe(401);
|
|
268
|
+
const denied = await request("", "GET", workerToken);
|
|
269
|
+
expect(denied.status).toBe(403);
|
|
270
|
+
expect(await denied.text()).not.toContain("codex_oauth_0");
|
|
271
|
+
expect((await request("", "GET", leadToken)).status).toBe(200);
|
|
272
|
+
expect((await request("", "GET", API_KEY, workerId)).status).toBe(200);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("deletes only the selected slot and schedules config side effects", async () => {
|
|
276
|
+
await store("codex_oauth_0");
|
|
277
|
+
await store("codex_oauth_2");
|
|
278
|
+
const response = await request("/2", "DELETE");
|
|
279
|
+
expect(response.status).toBe(200);
|
|
280
|
+
expect((await response.json()).accounts).toMatchObject([{ slot: 0 }]);
|
|
281
|
+
expect(await getSwarmConfigs({ scope: "global", key: "codex_oauth_2" })).toEqual([]);
|
|
282
|
+
expect(_autoReloadStatsForTests().pending).toBe(true);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("a sign-in in a freed slot does not inherit the removed account's auth bench", async () => {
|
|
286
|
+
await store("codex_oauth_0", credentials("grant-keep0"));
|
|
287
|
+
await store("codex_oauth_1", credentials("grant-gone1"));
|
|
288
|
+
await recordKeyAuthFailure("CODEX_OAUTH", "gone1", 1);
|
|
289
|
+
await recordKeyAuthFailure("CODEX_OAUTH", "gone1", 1);
|
|
290
|
+
expect((await getAvailableKeyIndices("CODEX_OAUTH", 2)).availableIndices).toEqual([0]);
|
|
291
|
+
expect((await request("/1", "DELETE")).status).toBe(200);
|
|
292
|
+
await store("codex_oauth_1", credentials("grant-fresh"));
|
|
293
|
+
expect((await getAvailableKeyIndices("CODEX_OAUTH", 2)).availableIndices).toEqual([0, 1]);
|
|
294
|
+
expect((await list()).accounts.map((account) => account.status)).toEqual([
|
|
295
|
+
"unchecked",
|
|
296
|
+
"unchecked",
|
|
297
|
+
]);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("deletes the legacy slot zero and keeps it when the numbered row exists", async () => {
|
|
301
|
+
await store("codex_oauth");
|
|
302
|
+
expect((await request("/0", "DELETE")).status).toBe(200);
|
|
303
|
+
expect(await getSwarmConfigs({ scope: "global", key: "codex_oauth" })).toEqual([]);
|
|
304
|
+
await store("codex_oauth");
|
|
305
|
+
await store("codex_oauth_0");
|
|
306
|
+
const response = await request("/0", "DELETE");
|
|
307
|
+
expect(response.status).toBe(200);
|
|
308
|
+
expect((await response.json()).accounts).toMatchObject([{ slot: 0, legacy: true }]);
|
|
309
|
+
expect(await getSwarmConfigs({ scope: "global", key: "codex_oauth_0" })).toEqual([]);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test("returns 404 for missing slots and rejects invalid slot numbers", async () => {
|
|
313
|
+
expect((await request("/1", "DELETE")).status).toBe(404);
|
|
314
|
+
for (const slot of ["-1", "1.5", "nope", "101"]) {
|
|
315
|
+
expect((await request(`/${slot}`, "DELETE")).status).toBe(400);
|
|
316
|
+
}
|
|
317
|
+
expect(_autoReloadStatsForTests().pending).toBe(false);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test("a user who may delete config but not read secrets cannot remove an account", async () => {
|
|
321
|
+
await store("codex_oauth_0", credentials("grant-keep0", { email: "keep@example.com" }));
|
|
322
|
+
await store("codex_oauth_1");
|
|
323
|
+
const user = await createUser({ name: "Delete-only user" });
|
|
324
|
+
const token = await mintToken(user.id, "delete-only", { kind: "operator", id: "codex-test" });
|
|
325
|
+
// A new user starts as admin. Replace that with a role that grants only the delete.
|
|
326
|
+
await getDbClient().run("DELETE FROM principal_roles WHERE principalId = ?", [user.id]);
|
|
327
|
+
await getDbClient().run(
|
|
328
|
+
"INSERT INTO roles (id, name) VALUES ('codex-delete-only', 'codex-delete-only')",
|
|
329
|
+
);
|
|
330
|
+
await getDbClient().run(
|
|
331
|
+
"INSERT INTO role_permissions (roleId, verb) VALUES ('codex-delete-only', 'config.delete.any')",
|
|
332
|
+
);
|
|
333
|
+
await getDbClient().run(
|
|
334
|
+
"INSERT INTO principal_roles (principalType, principalId, roleId) VALUES ('user', ?, 'codex-delete-only')",
|
|
335
|
+
[user.id],
|
|
336
|
+
);
|
|
337
|
+
const previous = process.env.RBAC_ENABLED;
|
|
338
|
+
process.env.RBAC_ENABLED = "true";
|
|
339
|
+
try {
|
|
340
|
+
const response = await request("/1", "DELETE", token.plaintext);
|
|
341
|
+
expect(response.status).toBe(403);
|
|
342
|
+
expect(await response.text()).not.toContain("keep@example.com");
|
|
343
|
+
expect((await getSwarmConfigs({ scope: "global", key: "codex_oauth_1" })).length).toBe(1);
|
|
344
|
+
} finally {
|
|
345
|
+
if (previous === undefined) delete process.env.RBAC_ENABLED;
|
|
346
|
+
else process.env.RBAC_ENABLED = previous;
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test("denies worker deletion and identity spoofing while allowing the lead", async () => {
|
|
351
|
+
await store("codex_oauth_0");
|
|
352
|
+
expect((await request("/0", "DELETE", workerToken)).status).toBe(403);
|
|
353
|
+
expect((await request("/0", "DELETE", workerToken, leadId)).status).toBe(403);
|
|
354
|
+
expect((await getSwarmConfigs({ scope: "global", key: "codex_oauth_0" })).length).toBe(1);
|
|
355
|
+
expect((await request("/0", "DELETE", leadToken)).status).toBe(200);
|
|
356
|
+
});
|
|
357
|
+
});
|