@desplega.ai/agent-swarm 2.6.0 → 2.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/{acp-adapter-dgfakfpw.js → acp-adapter-8dn5tq56.js} +6 -6
- package/dist/{actions-fbqma8aa.js → actions-3r6gw7dy.js} +18 -18
- package/dist/{amp-adapter-en0jy6hs.js → amp-adapter-wjtq413p.js} +3 -3
- package/dist/{anthropic-messages-txs190dd.js → anthropic-messages-n4cby1rz.js} +4 -4
- package/dist/{app-k8a5cgpp.js → app-d53yhykk.js} +3 -3
- package/dist/{assistant-a16433gr.js → assistant-ksrawj3k.js} +20 -20
- package/dist/{boot-reembed-6c1sjfnr.js → boot-reembed-fxfw17m2.js} +11 -3
- package/dist/{boot-reembed-f4nn561n.js → boot-reembed-k5y5xbj0.js} +15 -8
- package/dist/{boot-scrub-sweep-d4w9gajf.js → boot-scrub-sweep-se41j9fd.js} +3 -3
- package/dist/{boot-seeds-9bmkspyw.js → boot-seeds-r3kwwv8b.js} +4 -4
- package/dist/{claude-adapter-nys1kjxa.js → claude-adapter-ahmyf231.js} +8 -8
- package/dist/{claude-managed-adapter-55207973.js → claude-managed-adapter-5knamegc.js} +3 -3
- package/dist/{claude-managed-setup-3cm35gey.js → claude-managed-setup-mdc7dy6b.js} +3 -3
- package/dist/{claude-sdk-session-nw5yd8cp.js → claude-sdk-session-n5wen9zk.js} +8729 -8600
- package/dist/{cli-683y0b3a.js → cli-0dqtv2jg.js} +2 -2
- package/dist/{cli-a34pr3w1.js → cli-0tww7dnz.js} +2 -2
- package/dist/{cli-w2656ka1.js → cli-0w4e4v58.js} +7 -7
- package/dist/{cli-5sckpqcx.js → cli-2sjm3t4r.js} +1 -1
- package/dist/{cli-gf2850q7.js → cli-3cywb44q.js} +18 -1
- package/dist/{cli-8wkb09q5.js → cli-3k9jej0c.js} +2 -2
- package/dist/{cli-3aqd4jaa.js → cli-3v2cyqb8.js} +3 -3
- package/dist/{cli-emhjcj2y.js → cli-5qt9vzmq.js} +89 -20
- package/dist/{cli-abkm71vr.js → cli-5rp1w4cr.js} +1 -1
- package/dist/{cli-gf95q3bm.js → cli-6vwjsetx.js} +4 -4
- package/dist/{cli-k21gx5ht.js → cli-6w2yrrc0.js} +10 -10
- package/dist/{cli-m70bajzr.js → cli-75tdydgv.js} +1 -1
- package/dist/{cli-5rj83bam.js → cli-8feswf2v.js} +47 -36
- package/dist/{cli-sxgtf5fw.js → cli-8wd3mbyj.js} +1 -1
- package/dist/{cli-ca6banwz.js → cli-9csfthbw.js} +10 -8
- package/dist/{cli-dqygeeh5.js → cli-b41p764a.js} +31 -3
- package/dist/{cli-m5a9m0ak.js → cli-c1t533hh.js} +3 -3
- package/dist/{cli-2sjhpsns.js → cli-c28jxpcv.js} +1 -1
- package/dist/{cli-bqztfyve.js → cli-cb50g9cd.js} +1 -1
- package/dist/{cli-ag2yrnt5.js → cli-d8gaphtr.js} +2 -2
- package/dist/{cli-9abdgn5t.js → cli-dgtfysne.js} +3 -3
- package/dist/{cli-nq677csy.js → cli-dscnhn18.js} +1 -1
- package/dist/{cli-3fwqy71j.js → cli-dv11pgpv.js} +39 -32
- package/dist/{cli-v49fkghp.js → cli-ekptgk2m.js} +6 -6
- package/dist/{cli-c2habvsf.js → cli-f3mzy662.js} +17 -17
- package/dist/{cli-zqne1qcj.js → cli-fk7k8s9x.js} +2 -2
- package/dist/{cli-1tem6ns9.js → cli-gt4h8cxw.js} +3 -2
- package/dist/{cli-vbhgkw9h.js → cli-h3e7bhvn.js} +3 -3
- package/dist/{cli-ffvsj2qw.js → cli-hkxqdmnb.js} +3 -3
- package/dist/{cli-a2rr0fmg.js → cli-hpkvgdad.js} +7 -4
- package/dist/{cli-art7c1zh.js → cli-hq2v4rr5.js} +37 -9
- package/dist/{cli-sqma6vw1.js → cli-k1zb7raw.js} +5 -5
- package/dist/{cli-4rvr7zfx.js → cli-k4awgvzm.js} +5 -0
- package/dist/{cli-3ndmpxxv.js → cli-k7d3hvya.js} +2 -2
- package/dist/{cli-3gfvmzht.js → cli-kbtdftbw.js} +74 -26
- package/dist/{cli-hfeb68z4.js → cli-kj69b9tw.js} +34 -6
- package/dist/{cli-az25fn5f.js → cli-kk9jy4nm.js} +2 -2
- package/dist/{cli-39aet655.js → cli-m52t4cta.js} +1 -1
- package/dist/{cli-cz37jqss.js → cli-mdadewqp.js} +5 -5
- package/dist/{cli-esakngh8.js → cli-nf6k60sn.js} +1 -1
- package/dist/{cli-j6rktc91.js → cli-nzdf09js.js} +1 -1
- package/dist/{cli-rr7zx19j.js → cli-prcyzecg.js} +12 -12
- package/dist/{cli-4hxrf0p4.js → cli-pzafyqtk.js} +393 -62
- package/dist/{cli-g8ka5j4j.js → cli-qvaa1gjp.js} +173 -52
- package/dist/{cli-d2dpf95n.js → cli-rg4b2g6j.js} +1 -1
- package/dist/{cli-kf4jree9.js → cli-rm66vqdz.js} +1 -1
- package/dist/{cli-4rf0f5vg.js → cli-s504c3w7.js} +5 -3
- package/dist/{cli-nn6vx2mq.js → cli-swkx9jm2.js} +7 -5
- package/dist/{cli-g20jjwt4.js → cli-thsaq4pd.js} +16 -16
- package/dist/{cli-aj8djyqt.js → cli-twjnyvs8.js} +2 -2
- package/dist/{cli-03gg9pd1.js → cli-typdpe8g.js} +4 -4
- package/dist/{cli-mtnj4fwa.js → cli-vg8pwmsd.js} +6 -6
- package/dist/{cli-gnsb1zxa.js → cli-vhpckzvp.js} +1 -1
- package/dist/{cli-7enjwya9.js → cli-vz78s5wm.js} +1 -1
- package/dist/{cli-dv97kdzv.js → cli-wdxgsdz5.js} +2 -2
- package/dist/{cli-7a6p4e65.js → cli-wtjg6je3.js} +1 -1
- package/dist/{cli-6y51ryva.js → cli-x3cngb1h.js} +44 -12
- package/dist/{cli-n7asmce6.js → cli-xbz6mcg4.js} +3 -3
- package/dist/{cli-24j1jeeb.js → cli-y4tdxsqb.js} +3 -3
- package/dist/{cli-mvjz5an3.js → cli-yqrsp3ny.js} +7 -7
- package/dist/cli.js +31 -21
- package/dist/{codex-adapter-d95q4qsn.js → codex-adapter-7wepzq15.js} +6 -6
- package/dist/{codex-hook-brpnycn5.js → codex-hook-pbdh4xhx.js} +2 -2
- package/dist/{codex-session-runner-gdwd8h0c.js → codex-session-runner-02sb49e4.js} +6 -6
- package/dist/{commands-jqmgk54b.js → commands-3rcspxct.js} +2 -2
- package/dist/{controller-z3jq589f.js → controller-8ar4avd6.js} +3 -3
- package/dist/{controllers-connect-r2za39ef.js → controllers-connect-ajvh9h0v.js} +19 -19
- package/dist/{cursor-adapter-4mr4jgkw.js → cursor-adapter-gd0sa7bh.js} +1 -1
- package/dist/{db-w36wwb5g.js → db-dv9j7ts5.js} +2 -2
- package/dist/{db-ppv2tze7.js → db-z26vgdvj.js} +3 -3
- package/dist/{dispatcher-pv5h77vf.js → dispatcher-8m0htjq8.js} +17 -17
- package/dist/{dsh-adapter-d4pdpb3r.js → dsh-adapter-y8gyk130.js} +1 -1
- package/dist/{e2b-fn3zacvt.js → e2b-74vg9b7e.js} +1 -1
- package/dist/{e2b-222hsajc.js → e2b-jyay8wzn.js} +1 -1
- package/dist/{easter-egg-3d-4sp4dvmc.js → easter-egg-3d-28f880xx.js} +1 -1
- package/dist/{execute-jev839y1.js → execute-9seb79vh.js} +1 -1
- package/dist/{external-channel-y6nkzhnn.js → external-channel-smz7jyth.js} +2 -2
- package/dist/{getMachineId-bsd-brn0xym3.js → getMachineId-bsd-whfre6gr.js} +3 -3
- package/dist/{getMachineId-darwin-t339sfxm.js → getMachineId-darwin-5zjynb19.js} +3 -3
- package/dist/{getMachineId-linux-5eq95d8b.js → getMachineId-linux-8mksrrsq.js} +3 -3
- package/dist/{getMachineId-unsupported-qavrtc7j.js → getMachineId-unsupported-fxtqsdt2.js} +3 -3
- package/dist/{getMachineId-win-q7fe1wx7.js → getMachineId-win-nzfmz857.js} +3 -3
- package/dist/{grok-adapter-t08b9ntm.js → grok-adapter-z0nnvtbn.js} +7 -7
- package/dist/handlers-pspryh6g.js +41 -0
- package/dist/{heartbeat-vn31xwdk.js → heartbeat-f928aqyp.js} +20 -20
- package/dist/{hook-cpjs8q51.js → hook-d7c605r4.js} +7 -7
- package/dist/{hook-6d0zbvvt.js → hook-q9hv5xf9.js} +5 -5
- package/dist/{http-9c6medda.js → http-q6j4k989.js} +1000 -251
- package/dist/{index-k0yt1tvq.js → index-5nb266jr.js} +17 -17
- package/dist/{index-ysd45kgc.js → index-8fxt5eah.js} +10 -10
- package/dist/{index-zgvqzjsw.js → index-cekzwhez.js} +5 -5
- package/dist/{index-38t35h0n.js → index-ch0c1xff.js} +17 -17
- package/dist/{index-pkma1yf8.js → index-ebnjhxh1.js} +1 -1
- package/dist/{index-yqgbfgaj.js → index-ed8dy8rn.js} +20 -20
- package/dist/{index-qhjf2htm.js → index-ejqdkkqx.js} +262 -96
- package/dist/{index-dr94zbnh.js → index-farm1zsn.js} +32661 -29443
- package/dist/{index-g6k9tctj.js → index-yn1yxg85.js} +1 -1
- package/dist/{keepalive-em0117bp.js → keepalive-zzc3hks1.js} +3 -3
- package/dist/lead-c0pjcaw4.js +79 -0
- package/dist/{maintenance-kbme3qy9.js → maintenance-kqsxzpzx.js} +1 -1
- package/dist/{oauth-refresh-sweep-xysj4yse.js → oauth-refresh-sweep-7qt0g55z.js} +8 -3
- package/dist/{onboard-p6q0vz97.js → onboard-k7x13fqg.js} +121 -8
- package/dist/{opencode-adapter-esv3k9yn.js → opencode-adapter-tqvrtter.js} +5 -5
- package/dist/{otel-impl-54qhqh1z.js → otel-impl-sqkf6k3b.js} +97 -97
- package/dist/{pi-mono-adapter-mymbwhj0.js → pi-mono-adapter-m2qnafxm.js} +89 -14
- package/dist/{pricing-refresh-yb75wv33.js → pricing-refresh-wkpwhgvs.js} +3 -3
- package/dist/{rbac-roles-dexhc9x8.js → rbac-roles-dpn0c0wj.js} +7 -4
- package/dist/{rbac-roles-1vp2mw4p.js → rbac-roles-wk7xd6a6.js} +2 -2
- package/dist/{render-v2-tjay5hhf.js → render-v2-ex2m4g2k.js} +3 -3
- package/dist/{resume-8ayf37g5.js → resume-725g9b0b.js} +7 -7
- package/dist/{runtime-hqe5cqcd.js → runtime-kajas15g.js} +1 -1
- package/dist/{session-access-fdhp987s.js → session-access-0j2ezjje.js} +8 -4
- package/dist/{session-source-backfill-qgtc86z1.js → session-source-backfill-62202awe.js} +2 -2
- package/dist/{setup-4rrmqcq3.js → setup-9sj11ax6.js} +1 -1
- package/dist/{virtual-modules-52km00d6.js → virtual-modules-c6fcq6zr.js} +2 -2
- package/dist/{work-objects-p28fgd70.js → work-objects-0y07495y.js} +20 -20
- package/dist/worker-d4q9sqbc.js +79 -0
- package/openapi.json +1536 -407
- package/package.json +7 -5
- package/src/be/automation-preflight.ts +2 -0
- package/src/be/connector-codes.ts +65 -22
- package/src/be/db/api-keys.ts +9 -0
- package/src/be/db/workflow-runs.ts +36 -5
- package/src/be/db.ts +80 -22
- package/src/be/memory/boot-reembed.ts +4 -1
- package/src/be/memory/constants.ts +6 -0
- package/src/be/memory/embedding-recovery.ts +48 -0
- package/src/be/memory/index-content.ts +5 -2
- package/src/be/memory/providers/openai-embedding.ts +65 -14
- package/src/be/memory/task-learning.ts +257 -0
- package/src/be/memory/types.ts +13 -2
- package/src/be/migrations/245_script_executor.sql +3 -0
- package/src/be/migrations/246_connector_code_kind.sql +11 -0
- package/src/be/oauth-refresh-sweep.ts +2 -1
- package/src/be/onboarding.ts +125 -15
- package/src/be/rbac-roles.ts +4 -1
- package/src/be/scripts/boot-reembed.ts +2 -0
- package/src/be/scripts/db.ts +14 -4
- package/src/be/scripts/embeddings.ts +4 -2
- package/src/be/scripts/run-saved.ts +1 -0
- package/src/be/scripts/typecheck.ts +9 -3
- package/src/be/seed/agent-fs-provision.ts +75 -14
- package/src/be/seed-skills/index.ts +7 -0
- package/src/be/swarm-config-guard.ts +3 -0
- package/src/cli.tsx +9 -0
- package/src/comb/config.ts +6 -1
- package/src/commands/onboard/agent-fs-version.ts +5 -0
- package/src/commands/onboard/compose-generator.ts +117 -4
- package/src/commands/onboard/env-generator.ts +9 -0
- package/src/commands/onboard/tei-image.ts +12 -0
- package/src/commands/onboard/types.ts +7 -0
- package/src/commands/onboard.tsx +11 -1
- package/src/commands/provider-credentials.ts +2 -1
- package/src/commands/runner.ts +14 -1
- package/src/heartbeat/heartbeat.ts +14 -1
- package/src/heartbeat/script-runner-probe.ts +47 -0
- package/src/http/agentfs-proxy.ts +369 -0
- package/src/http/all-routes.ts +1 -0
- package/src/http/api-keys.ts +24 -9
- package/src/http/codex-oauth-device.ts +8 -1
- package/src/http/fs.ts +64 -10
- package/src/http/idempotency-key.ts +1 -1
- package/src/http/inbox-state.ts +27 -1
- package/src/http/index.ts +7 -4
- package/src/http/memory.ts +7 -4
- package/src/http/onboarding.ts +139 -11
- package/src/http/pages-public.ts +38 -5
- package/src/http/scripts.ts +27 -0
- package/src/http/session-access.ts +24 -0
- package/src/http/sessions.ts +10 -6
- package/src/http/tasks.ts +85 -11
- package/src/http/telemetry-ui.ts +1 -1
- package/src/http/user-roles.ts +4 -1
- package/src/http/users.ts +272 -3
- package/src/http/utils.ts +22 -3
- package/src/http/workflows.ts +10 -0
- package/src/http/x.ts +5 -0
- package/src/prompts/base-prompt.ts +45 -10
- package/src/prompts/session-templates.ts +43 -5
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/codex-adapter.ts +6 -1
- package/src/providers/cursor-adapter.ts +1 -1
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/providers/pi-mono-adapter.ts +99 -14
- package/src/rbac/legacy-policy.ts +1 -0
- package/src/rbac/permissions.ts +4 -0
- package/src/scheduler/scheduler.ts +2 -1
- package/src/scripts-runtime/eval-harness.ts +8 -1
- package/src/scripts-runtime/executors/native.ts +22 -16
- package/src/scripts-runtime/executors/quickjs-prelude.ts +103 -1
- package/src/scripts-runtime/executors/quickjs-runner.ts +2 -1
- package/src/scripts-runtime/executors/quickjs.ts +7 -0
- package/src/scripts-runtime/executors/registry.ts +32 -0
- package/src/scripts-runtime/executors/types.ts +7 -0
- package/src/scripts-runtime/host-validated-args.ts +133 -0
- package/src/scripts-runtime/loader.ts +30 -3
- package/src/scripts-runtime/swarm-sdk.ts +9 -2
- package/src/scripts-runtime/types/stdlib.d.ts +9 -1
- package/src/scripts-runtime/types/swarm-sdk.d.ts +9 -1
- package/src/tests/agent-fs-provision-seeder.test.ts +261 -92
- package/src/tests/agentfs-proxy.test.ts +1106 -0
- package/src/tests/api-key-status-by-suffix.test.ts +242 -0
- package/src/tests/base-prompt.test.ts +139 -4
- package/src/tests/codex-adapter.test.ts +46 -8
- package/src/tests/codex-oauth-device.test.ts +12 -1
- package/src/tests/comb-config-guard.test.ts +20 -0
- package/src/tests/comb-links.test.ts +79 -1
- package/src/tests/comb-status.test.ts +44 -0
- package/src/tests/community-linear-delivery-loop.test.ts +2099 -0
- package/src/tests/credential-check.test.ts +4 -2
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/docs-index.test.ts +135 -0
- package/src/tests/dsh-adapter.test.ts +1 -1
- package/src/tests/fetch-resolved-env.test.ts +16 -2
- package/src/tests/fixtures/onboard/baseline-states.ts +68 -0
- package/src/tests/fixtures/onboard/content.compose.yml.txt +190 -0
- package/src/tests/fixtures/onboard/content.env.txt +29 -0
- package/src/tests/fixtures/onboard/dev.compose.yml.txt +155 -0
- package/src/tests/fixtures/onboard/dev.env.txt +28 -0
- package/src/tests/fixtures/onboard/full-all.compose.yml.txt +582 -0
- package/src/tests/fixtures/onboard/full-all.env.txt +59 -0
- package/src/tests/fixtures/onboard/full.compose.yml.txt +435 -0
- package/src/tests/fixtures/onboard/full.env.txt +36 -0
- package/src/tests/fixtures/onboard/research.compose.yml.txt +155 -0
- package/src/tests/fixtures/onboard/research.env.txt +28 -0
- package/src/tests/fixtures/onboard/solo.compose.yml.txt +85 -0
- package/src/tests/fixtures/onboard/solo.env.txt +26 -0
- package/src/tests/heartbeat-checklist.test.ts +89 -0
- package/src/tests/heartbeat-script-runner-probe.test.ts +83 -0
- package/src/tests/http-log-scrubbing.test.ts +34 -1
- package/src/tests/http-task-write-ownership.test.ts +5 -1
- package/src/tests/invites-http.test.ts +464 -0
- package/src/tests/memory-embedding-input-cap.test.ts +122 -1
- package/src/tests/memory-embedding-recovery.test.ts +137 -0
- package/src/tests/oauth-refresh-sweep.test.ts +34 -1
- package/src/tests/onboard-compose.test.ts +162 -0
- package/src/tests/onboard-env.test.ts +24 -0
- package/src/tests/onboard-non-interactive.test.ts +29 -0
- package/src/tests/onboard-tei-image.test.ts +36 -0
- package/src/tests/onboarding.test.ts +451 -4
- package/src/tests/openai-embedding-provider.test.ts +105 -2
- package/src/tests/opencode-adapter.test.ts +2 -1
- package/src/tests/pages-frame-ancestors.test.ts +162 -0
- package/src/tests/pi-codemode.test.ts +2 -1
- package/src/tests/pi-mono-adapter.test.ts +54 -0
- package/src/tests/pi-mono-session-cost-on-abort.test.ts +270 -0
- package/src/tests/pi-native-mcp.test.ts +13 -1
- package/src/tests/pi-tool-exposure.test.ts +8 -8
- package/src/tests/preload-env.test.ts +4 -0
- package/src/tests/preload.ts +4 -0
- package/src/tests/probe-tool-schema-tokens.test.ts +144 -0
- package/src/tests/prompt-template-session.test.ts +20 -3
- package/src/tests/rbac-admission-e2e.test.ts +22 -6
- package/src/tests/rbac-admission.test.ts +342 -4
- package/src/tests/rbac-engine.test.ts +1 -0
- package/src/tests/rbac-roles.test.ts +9 -1
- package/src/tests/request-composition-derive.test.ts +329 -0
- package/src/tests/script-connections.test.ts +2 -0
- package/src/tests/script-executor-conformance.test.ts +36 -1
- package/src/tests/script-executor-quickjs-pool.test.ts +50 -0
- package/src/tests/script-executor-quickjs.test.ts +140 -0
- package/src/tests/script-host-validated-args-native.test.ts +196 -0
- package/src/tests/script-per-script-executor.test.ts +415 -0
- package/src/tests/scripts-external-api.test.ts +45 -0
- package/src/tests/sdk-allowlist.test.ts +3 -2
- package/src/tests/self-improvement.test.ts +4 -4
- package/src/tests/send-task-requester-gate.test.ts +64 -3
- package/src/tests/sessions-move.test.ts +39 -1
- package/src/tests/store-progress-learning.test.ts +381 -0
- package/src/tests/ui-logs-parser.test.ts +180 -0
- package/src/tests/whoami-route.test.ts +41 -0
- package/src/tests/workflow-engine-v2.test.ts +116 -1
- package/src/tests/workflow-mcp-trigger-schema.test.ts +209 -0
- package/src/tests/workflow-run-finished-event.test.ts +263 -0
- package/src/tests/workflow-swarm-script.test.ts +34 -0
- package/src/tools/inject-learning.ts +1 -1
- package/src/tools/memory-edit.ts +1 -1
- package/src/tools/memory-search.ts +1 -1
- package/src/tools/script-common.ts +1 -0
- package/src/tools/script-run.ts +7 -0
- package/src/tools/script-upsert.ts +7 -0
- package/src/tools/store-progress.ts +71 -1
- package/src/tools/utils.ts +12 -1
- package/src/tools/workflows/trigger-workflow.ts +104 -10
- package/src/types.ts +12 -1
- package/src/utils/constants.ts +39 -0
- package/src/utils/credentials.ts +5 -2
- package/src/utils/task-attachment-links.ts +15 -3
- package/src/workflows/engine.ts +18 -16
- package/src/workflows/executors/swarm-script.ts +5 -0
- package/src/workflows/foreach-driver.ts +5 -11
- package/src/workflows/node-timeout.ts +30 -0
- package/templates/community/linear-delivery-loop/PLAYBOOK.md +323 -0
- package/templates/community/linear-delivery-loop/README.md +38 -0
- package/templates/community/linear-delivery-loop/config.example.json +31 -0
- package/templates/community/linear-delivery-loop/lead-prompt.md +34 -0
- package/templates/community/linear-delivery-loop/schedule.json +10 -0
- package/templates/community/linear-delivery-loop/scripts/linear-delivery-setup.ts +148 -0
- package/templates/community/linear-delivery-loop/scripts/linear-delivery-tick.ts +1781 -0
- package/templates/skills/swarm-docs/config.json +14 -0
- package/templates/skills/swarm-docs/content.md +144 -0
- package/dist/handlers-eqsnmv95.js +0 -41
- package/dist/lead-23e7z59t.js +0 -79
- package/dist/worker-e3w7vye1.js +0 -79
- package/dist/{azure-openai-responses-9kfwhmn7.js → azure-openai-responses-43asa23s.js} +0 -0
- package/dist/{node-vxx896fa.js → node-t28cc56b.js} +0 -0
- package/dist/{openai-codex-responses-79s8xnbh.js → openai-codex-responses-jhr3sjtm.js} +3 -3
- package/dist/{openai-completions-tdbyts0x.js → openai-completions-gs6yn37k.js} +3 -3
- /package/dist/{openai-responses-dm3t5vqp.js → openai-responses-jqm9vwwk.js} +0 -0
package/src/commands/runner.ts
CHANGED
|
@@ -1222,6 +1222,7 @@ export async function provisionAgentFsAfterRegistration(opts: {
|
|
|
1222
1222
|
* - TEMPLATE_REGISTRY_URL — read per registry fetch.
|
|
1223
1223
|
* - SLACK_DISABLE — read by the prompt builder to gate the Slack tool section.
|
|
1224
1224
|
* - SWARM_ORG_NAME — read per telemetry event for org identity.
|
|
1225
|
+
* - AGENT_FS_LINK_TARGET — read by the prompt builder for the file-link base.
|
|
1225
1226
|
*
|
|
1226
1227
|
* CLAUDE_TRANSPORT stays in the per-session environment. Caching a scoped
|
|
1227
1228
|
* value here would prevent deletion from restoring the configured default.
|
|
@@ -1241,6 +1242,7 @@ export const RELOADABLE_ENV_KEYS: ReadonlySet<string> = new Set([
|
|
|
1241
1242
|
"TEMPLATE_REGISTRY_URL",
|
|
1242
1243
|
"SLACK_DISABLE",
|
|
1243
1244
|
"SWARM_ORG_NAME",
|
|
1245
|
+
"AGENT_FS_LINK_TARGET",
|
|
1244
1246
|
// Gate the public-ranking `ranking.usage` emit, so clearing the website or
|
|
1245
1247
|
// turning the setting off stops it on running workers.
|
|
1246
1248
|
"SWARM_ORG_WEBSITE",
|
|
@@ -2714,6 +2716,8 @@ export interface RunningTask {
|
|
|
2714
2716
|
assistantText?: { value?: string };
|
|
2715
2717
|
/** Last progress value, retained until the final API writes settle. */
|
|
2716
2718
|
finalProgress?: { value?: string };
|
|
2719
|
+
/** Latest cost snapshot captured from `result` events before completion settles. */
|
|
2720
|
+
lastCost?: { value?: CostData };
|
|
2717
2721
|
/** Last time `reconcileActiveTasks` read this task's server-side status. */
|
|
2718
2722
|
lastReconcileAt?: number;
|
|
2719
2723
|
/** When the runner asked the session to abort (cancel or reconcile). */
|
|
@@ -4553,6 +4557,7 @@ async function spawnProviderProcess(
|
|
|
4553
4557
|
// session promise resolves (see `trackAssistantText`).
|
|
4554
4558
|
const assistantTextHolder: { value?: string } = {};
|
|
4555
4559
|
const finalProgressHolder: { value?: string } = {};
|
|
4560
|
+
const lastCostHolder: { value?: CostData } = {};
|
|
4556
4561
|
|
|
4557
4562
|
// Context usage throttle: max 1 snapshot per 30 seconds
|
|
4558
4563
|
let lastContextPostTime = 0;
|
|
@@ -4761,6 +4766,11 @@ async function spawnProviderProcess(
|
|
|
4761
4766
|
break;
|
|
4762
4767
|
}
|
|
4763
4768
|
case "result":
|
|
4769
|
+
lastCostHolder.value = {
|
|
4770
|
+
...event.cost,
|
|
4771
|
+
taskId: realTaskId,
|
|
4772
|
+
sessionId: opts.runnerSessionId,
|
|
4773
|
+
};
|
|
4764
4774
|
{
|
|
4765
4775
|
// Mid-session snapshot — fires on each streamed "result" event,
|
|
4766
4776
|
// necessarily before `ProviderResult.appliedReasoningEffort` exists
|
|
@@ -4968,6 +4978,7 @@ async function spawnProviderProcess(
|
|
|
4968
4978
|
}
|
|
4969
4979
|
|
|
4970
4980
|
// Retain cost data for the concurrent final API writes.
|
|
4981
|
+
result.cost ??= lastCostHolder.value;
|
|
4971
4982
|
if (result.cost) {
|
|
4972
4983
|
result.cost = { ...result.cost, taskId: realTaskId, sessionId: opts.runnerSessionId };
|
|
4973
4984
|
sessionSpan.setAttributes({
|
|
@@ -5130,6 +5141,7 @@ async function spawnProviderProcess(
|
|
|
5130
5141
|
model: model || undefined,
|
|
5131
5142
|
assistantText: assistantTextHolder,
|
|
5132
5143
|
finalProgress: finalProgressHolder,
|
|
5144
|
+
lastCost: lastCostHolder,
|
|
5133
5145
|
};
|
|
5134
5146
|
runningTaskForSessionInit = runningTask;
|
|
5135
5147
|
if (pendingHarnessVariant !== undefined) {
|
|
@@ -5146,7 +5158,7 @@ async function spawnProviderProcess(
|
|
|
5146
5158
|
runningTask.result ??= r;
|
|
5147
5159
|
})
|
|
5148
5160
|
.catch(() => {
|
|
5149
|
-
runningTask.result ??= { exitCode: 1, isError: true };
|
|
5161
|
+
runningTask.result ??= { exitCode: 1, isError: true, cost: lastCostHolder.value };
|
|
5150
5162
|
});
|
|
5151
5163
|
|
|
5152
5164
|
return runningTask;
|
|
@@ -5316,6 +5328,7 @@ export async function reconcileActiveTasks(
|
|
|
5316
5328
|
exitCode: 1,
|
|
5317
5329
|
isError: true,
|
|
5318
5330
|
sessionId: task.session.sessionId,
|
|
5331
|
+
cost: task.lastCost?.value,
|
|
5319
5332
|
failureReason: "runner exited without result: provider session did not settle after abort",
|
|
5320
5333
|
};
|
|
5321
5334
|
}
|
|
@@ -83,6 +83,7 @@ import { DEFAULT_OPENROUTER_BASE_URL, getOpenRouterBaseUrl } from "../utils/open
|
|
|
83
83
|
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
84
84
|
import { getExecutorRegistry } from "../workflows";
|
|
85
85
|
import { recoverIncompleteRuns } from "../workflows/recovery";
|
|
86
|
+
import { probeScriptRunner } from "./script-runner-probe";
|
|
86
87
|
// Side-effect import: registers heartbeat event templates in the in-memory registry
|
|
87
88
|
import "./templates";
|
|
88
89
|
|
|
@@ -1703,8 +1704,20 @@ export async function defaultChecklistGateRunner(
|
|
|
1703
1704
|
scriptName: string,
|
|
1704
1705
|
leadAgentId: string,
|
|
1705
1706
|
signal: AbortSignal,
|
|
1706
|
-
deps: Parameters<typeof runSavedScriptAsAgent>[1]
|
|
1707
|
+
deps: Parameters<typeof runSavedScriptAsAgent>[1] & {
|
|
1708
|
+
probe?: typeof probeScriptRunner;
|
|
1709
|
+
} = {},
|
|
1707
1710
|
): Promise<unknown> {
|
|
1711
|
+
try {
|
|
1712
|
+
const unhealthy = await (deps.probe ?? probeScriptRunner)(leadAgentId);
|
|
1713
|
+
if (unhealthy) return unhealthy;
|
|
1714
|
+
} catch (error) {
|
|
1715
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1716
|
+
return {
|
|
1717
|
+
quiet: false,
|
|
1718
|
+
reason: `script-runner-unhealthy: ${scrubSecrets(message)}`.slice(0, 300),
|
|
1719
|
+
};
|
|
1720
|
+
}
|
|
1708
1721
|
const script = await getScript({ name: scriptName, scope: "global" });
|
|
1709
1722
|
if (!script) throw new Error(`global script "${scriptName}" not found`);
|
|
1710
1723
|
const output = await runSavedScriptAsAgent(
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { runScriptWithRunCredential } from "../be/scripts/run-credential";
|
|
2
|
+
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
3
|
+
|
|
4
|
+
const PROBE_TIMEOUT_MS = 10_000;
|
|
5
|
+
|
|
6
|
+
/** Exercise the same executor and per-run credential as the gate, with a deadline even if the runner stalls. */
|
|
7
|
+
export async function probeScriptRunner(
|
|
8
|
+
agentId: string,
|
|
9
|
+
runner: typeof runScriptWithRunCredential = runScriptWithRunCredential,
|
|
10
|
+
): Promise<{ quiet: false; reason: string } | null> {
|
|
11
|
+
const controller = new AbortController();
|
|
12
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
13
|
+
try {
|
|
14
|
+
const deadline = new Promise<never>((_, reject) => {
|
|
15
|
+
timer = setTimeout(() => {
|
|
16
|
+
controller.abort();
|
|
17
|
+
reject(new Error("timeout after 10000ms"));
|
|
18
|
+
}, PROBE_TIMEOUT_MS);
|
|
19
|
+
});
|
|
20
|
+
const output = await Promise.race([
|
|
21
|
+
runner({
|
|
22
|
+
source: "export default async function () { return true; }",
|
|
23
|
+
agentId,
|
|
24
|
+
fsMode: "none",
|
|
25
|
+
timeoutMs: PROBE_TIMEOUT_MS,
|
|
26
|
+
signal: controller.signal,
|
|
27
|
+
}),
|
|
28
|
+
deadline,
|
|
29
|
+
]);
|
|
30
|
+
if (output.exitCode !== 0 || output.error || output.runtimeError) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
output.runtimeError?.message ??
|
|
33
|
+
output.error ??
|
|
34
|
+
`script exited with code ${output.exitCode}`,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
} catch (error) {
|
|
39
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
40
|
+
return {
|
|
41
|
+
quiet: false,
|
|
42
|
+
reason: `script-runner-unhealthy: ${scrubSecrets(message)}`.slice(0, 300),
|
|
43
|
+
};
|
|
44
|
+
} finally {
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import { Readable } from "node:stream";
|
|
3
|
+
import { getApiKey } from "../utils/api-key";
|
|
4
|
+
import { isAgentFsProxyEnabled } from "../utils/constants";
|
|
5
|
+
import { resolveHttpRequestAuth } from "./auth";
|
|
6
|
+
import { clientIp, createIpRateLimiter } from "./ip-rate-limit";
|
|
7
|
+
import { route } from "./route-def";
|
|
8
|
+
import { jsonError, parseBody, RequestBodyTooLargeError } from "./utils";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Streaming pass-through to the agent-fs server at AGENT_FS_API_URL, active
|
|
12
|
+
* only when AGENT_FS_PROXY is on:
|
|
13
|
+
*
|
|
14
|
+
* - `/agentfs/*` -> agent-fs `/*` (the API Comb calls from the browser);
|
|
15
|
+
* - `/share/*` and `/site/*` -> agent-fs `/share/*` and `/site/*` unchanged.
|
|
16
|
+
* agent-fs share pages embed root-absolute `/share/<token>/raw` URLs and
|
|
17
|
+
* redirect to `/site/<token>/`, so they 404 behind the `/agentfs` prefix alone.
|
|
18
|
+
*
|
|
19
|
+
* agent-fs enforces its own `af_` bearer. The one swarm check is on
|
|
20
|
+
* `POST /agentfs/auth/register` (see `checkRegister`). Only an
|
|
21
|
+
* `Authorization: Bearer af_...` credential is forwarded (never a swarm API key
|
|
22
|
+
* or an `aswt_` token), plus an allowlist of other request headers (never
|
|
23
|
+
* Cookie or X-Agent-ID). On `/agentfs/*` the upstream CORS headers are dropped
|
|
24
|
+
* (the swarm's own setCorsHeaders applies); on `/share/*` and `/site/*` they
|
|
25
|
+
* are kept, because sandboxed site pages run in an opaque origin and need
|
|
26
|
+
* agent-fs's `Access-Control-Allow-Origin: *`. Both bodies are streamed.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const API_PREFIX = "/agentfs";
|
|
30
|
+
const PUBLIC_PREFIXES = ["/share/", "/site/"];
|
|
31
|
+
|
|
32
|
+
function upstreamBase(): string | null {
|
|
33
|
+
const v = process.env.AGENT_FS_API_URL?.trim().replace(/\/+$/, "");
|
|
34
|
+
return v || null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Request headers forwarded as is. `authorization` is handled separately. */
|
|
38
|
+
const REQUEST_HEADER_ALLOWLIST = [
|
|
39
|
+
"content-type",
|
|
40
|
+
// Framing for streamed uploads: agent-fs checks it against its upload cap.
|
|
41
|
+
"content-length",
|
|
42
|
+
"accept",
|
|
43
|
+
"if-match",
|
|
44
|
+
"if-none-match",
|
|
45
|
+
"range",
|
|
46
|
+
"x-agent-fs-message",
|
|
47
|
+
"x-agent-fs-message-encoding",
|
|
48
|
+
];
|
|
49
|
+
const AGENT_FS_BEARER = /^Bearer\s+af_\S+$/i;
|
|
50
|
+
|
|
51
|
+
/** Response headers copied from agent-fs (plus any `x-agent-fs-*`). */
|
|
52
|
+
const RESPONSE_HEADER_ALLOWLIST = new Set([
|
|
53
|
+
"content-type",
|
|
54
|
+
"content-length",
|
|
55
|
+
"content-disposition",
|
|
56
|
+
"content-range",
|
|
57
|
+
"accept-ranges",
|
|
58
|
+
"etag",
|
|
59
|
+
"last-modified",
|
|
60
|
+
"cache-control",
|
|
61
|
+
"location",
|
|
62
|
+
"retry-after",
|
|
63
|
+
// agent-fs serves user files (HTML included) with a sandbox CSP. Dropping
|
|
64
|
+
// these would run that content in the API's origin.
|
|
65
|
+
"content-security-policy",
|
|
66
|
+
"x-content-type-options",
|
|
67
|
+
"x-frame-options",
|
|
68
|
+
"referrer-policy",
|
|
69
|
+
"x-robots-tag",
|
|
70
|
+
"cross-origin-resource-policy",
|
|
71
|
+
]);
|
|
72
|
+
const EXPOSED_HEADERS = ["X-Agent-FS-Version", "X-Agent-FS-Content-Hash", "Content-Disposition"];
|
|
73
|
+
|
|
74
|
+
const UNGATED =
|
|
75
|
+
"agent-fs enforces its own af_ auth; register needs a swarm credential; swarm credentials are never forwarded";
|
|
76
|
+
const OK_200 = {
|
|
77
|
+
description: "Upstream agent-fs response, streamed verbatim",
|
|
78
|
+
unstructured: "Body is streamed from agent-fs; shape and content-type vary per upstream route",
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
for (const method of ["get", "post", "put", "delete", "patch"] as const) {
|
|
82
|
+
route({
|
|
83
|
+
method,
|
|
84
|
+
path: "/agentfs/{path}",
|
|
85
|
+
pattern: ["agentfs", null],
|
|
86
|
+
exact: false,
|
|
87
|
+
summary: "Streaming pass-through proxy to agent-fs (enabled by AGENT_FS_PROXY)",
|
|
88
|
+
tags: ["Comb"],
|
|
89
|
+
responses: {
|
|
90
|
+
200: OK_200,
|
|
91
|
+
401: {
|
|
92
|
+
description:
|
|
93
|
+
"Returned by agent-fs when credentials are missing or invalid, or by the swarm when POST /agentfs/auth/register has no swarm credential",
|
|
94
|
+
},
|
|
95
|
+
403: {
|
|
96
|
+
description:
|
|
97
|
+
"POST /agentfs/auth/register with a user token for an email that is not the user's",
|
|
98
|
+
},
|
|
99
|
+
413: { description: "POST /agentfs/auth/register body over 16 KB" },
|
|
100
|
+
429: { description: "Too many registrations from this IP (POST /agentfs/auth/register)" },
|
|
101
|
+
502: { description: "agent-fs is unreachable" },
|
|
102
|
+
},
|
|
103
|
+
auth: { apiKey: false },
|
|
104
|
+
...(method === "get" ? {} : { rbac: { ungated: UNGATED } }),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
for (const prefix of ["share", "site"] as const) {
|
|
109
|
+
route({
|
|
110
|
+
method: "get",
|
|
111
|
+
path: `/${prefix}/{path}`,
|
|
112
|
+
pattern: [prefix, null],
|
|
113
|
+
exact: false,
|
|
114
|
+
summary: `Public agent-fs ${prefix} page, served through the API (enabled by AGENT_FS_PROXY)`,
|
|
115
|
+
tags: ["Comb"],
|
|
116
|
+
responses: {
|
|
117
|
+
200: {
|
|
118
|
+
description: `agent-fs /${prefix} response, streamed verbatim`,
|
|
119
|
+
unstructured: "HTML page or raw file streamed from agent-fs; content-type varies",
|
|
120
|
+
},
|
|
121
|
+
404: { description: "Unknown or expired link (from agent-fs), or the proxy is off" },
|
|
122
|
+
502: { description: "agent-fs is unreachable" },
|
|
123
|
+
},
|
|
124
|
+
auth: { apiKey: false },
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** agent-fs `POST /auth/register` is public and unlimited upstream: 5 per minute per IP here. */
|
|
129
|
+
const registerRateLimiter = createIpRateLimiter({ capacity: 5, refillPerMs: 5 / 60_000 });
|
|
130
|
+
|
|
131
|
+
export function _resetAgentFsRegisterRateLimitForTests(): void {
|
|
132
|
+
registerRateLimiter.reset();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const REGISTER_BODY_MAX_BYTES = 16 * 1024;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* agent-fs `POST /auth/register` does not check that the caller owns the
|
|
139
|
+
* email, and the swarm invites its users' emails to the shared drive. An
|
|
140
|
+
* account made first for a teammate's email would get that teammate's seat.
|
|
141
|
+
* So a registration through the proxy needs a swarm credential: the operator
|
|
142
|
+
* key may register any email, a user token only the user's own email or an
|
|
143
|
+
* alias. Returns the JSON body to send upstream, or null after it wrote the
|
|
144
|
+
* error response.
|
|
145
|
+
*/
|
|
146
|
+
async function checkRegister(req: IncomingMessage, res: ServerResponse): Promise<string | null> {
|
|
147
|
+
const auth = await resolveHttpRequestAuth(req, getApiKey());
|
|
148
|
+
if (!auth || auth.page || (auth.kind !== "operator" && auth.kind !== "user")) {
|
|
149
|
+
jsonError(res, "Creating an agent-fs account here needs a swarm API key or user token", 401);
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
let body: unknown;
|
|
153
|
+
try {
|
|
154
|
+
body = await parseBody(req, REGISTER_BODY_MAX_BYTES);
|
|
155
|
+
} catch (err) {
|
|
156
|
+
if (err instanceof RequestBodyTooLargeError) jsonError(res, "Request body too large", 413);
|
|
157
|
+
else jsonError(res, "Invalid JSON body", 400);
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
161
|
+
jsonError(res, "Invalid JSON body", 400);
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
if (auth.kind === "user") {
|
|
165
|
+
const email = (body as { email?: unknown }).email;
|
|
166
|
+
const own = [auth.user.email, ...auth.user.emailAliases]
|
|
167
|
+
.filter((e): e is string => typeof e === "string")
|
|
168
|
+
.map((e) => e.trim().toLowerCase());
|
|
169
|
+
if (typeof email !== "string" || !own.includes(email.trim().toLowerCase())) {
|
|
170
|
+
jsonError(res, "You can create an agent-fs account only for your own email", 403);
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// Re-encoded, so agent-fs reads the same email this check read.
|
|
175
|
+
return JSON.stringify(body);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The path as an upstream router would match it: percent-decoded, repeated
|
|
180
|
+
* slashes collapsed, trailing slash dropped. `/auth/%72egister` and
|
|
181
|
+
* `//auth/register/` must still hit the register limit.
|
|
182
|
+
*/
|
|
183
|
+
function routedPath(pathOnly: string): string {
|
|
184
|
+
let decoded = pathOnly;
|
|
185
|
+
try {
|
|
186
|
+
decoded = decodeURIComponent(pathOnly);
|
|
187
|
+
} catch {
|
|
188
|
+
// A malformed escape: compare the raw bytes.
|
|
189
|
+
}
|
|
190
|
+
return decoded.replace(/\/{2,}/g, "/").replace(/\/+$/, "");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* A path fetch() or the upstream router would rewrite: dot segments (raw or
|
|
195
|
+
* percent-encoded, also behind an encoded `%2F`) and backslashes, which the
|
|
196
|
+
* URL parser treats as `/`. Rejecting them keeps the register limit and the
|
|
197
|
+
* upstream on the same path: `/./auth/register` and `/auth\register` both
|
|
198
|
+
* reach upstream `/auth/register`.
|
|
199
|
+
*/
|
|
200
|
+
function isNonCanonical(pathOnly: string): boolean {
|
|
201
|
+
let decoded = pathOnly;
|
|
202
|
+
try {
|
|
203
|
+
decoded = decodeURIComponent(pathOnly);
|
|
204
|
+
} catch {
|
|
205
|
+
// A malformed escape: check the raw bytes only.
|
|
206
|
+
}
|
|
207
|
+
for (const p of [pathOnly, decoded]) {
|
|
208
|
+
if (p.includes("\\")) return true;
|
|
209
|
+
if (p.split("/").some((s) => s === "." || s === "..")) return true;
|
|
210
|
+
}
|
|
211
|
+
// `.%2e` and the like, in case the decode above failed on a malformed escape.
|
|
212
|
+
return pathOnly.split("/").some((s) => /^(\.|%2e){1,2}$/i.test(s));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export async function handleAgentFsProxy(
|
|
216
|
+
req: IncomingMessage,
|
|
217
|
+
res: ServerResponse,
|
|
218
|
+
): Promise<boolean> {
|
|
219
|
+
const url = req.url ?? "";
|
|
220
|
+
const isApi = url.startsWith(`${API_PREFIX}/`);
|
|
221
|
+
const isPublicPage = !isApi && PUBLIC_PREFIXES.some((p) => url.startsWith(p));
|
|
222
|
+
if (!isApi && !isPublicPage) return false;
|
|
223
|
+
const base = upstreamBase();
|
|
224
|
+
if (!base || !isAgentFsProxyEnabled()) return false;
|
|
225
|
+
const method = req.method ?? "GET";
|
|
226
|
+
const allowed = isApi ? ["GET", "POST", "PUT", "DELETE", "PATCH"] : ["GET"];
|
|
227
|
+
if (!allowed.includes(method)) return false;
|
|
228
|
+
|
|
229
|
+
// Keep the path and query bytes as sent, so an encoded `%2F` survives.
|
|
230
|
+
const upstreamPath = isApi ? url.slice(API_PREFIX.length) : url;
|
|
231
|
+
const queryStart = upstreamPath.indexOf("?");
|
|
232
|
+
const pathOnly = queryStart === -1 ? upstreamPath : upstreamPath.slice(0, queryStart);
|
|
233
|
+
if (isNonCanonical(pathOnly)) {
|
|
234
|
+
jsonError(res, "Invalid path", 400);
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
// Limit on the path fetch() will send, not on our own parse of the raw
|
|
238
|
+
// target: fetch drops a raw `#fragment`, so `/auth/register#x` reaches
|
|
239
|
+
// upstream `/auth/register`.
|
|
240
|
+
const upstreamUrl = `${base}${upstreamPath}`;
|
|
241
|
+
let sentPath: string;
|
|
242
|
+
try {
|
|
243
|
+
// Strip the base URL's own path prefix, if AGENT_FS_API_URL carries one.
|
|
244
|
+
const basePath = new URL(base).pathname.replace(/\/+$/, "");
|
|
245
|
+
sentPath = new URL(upstreamUrl).pathname.slice(basePath.length);
|
|
246
|
+
} catch {
|
|
247
|
+
jsonError(res, "Invalid path", 400);
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
const isRegister = isApi && method === "POST" && routedPath(sentPath) === "/auth/register";
|
|
251
|
+
if (isRegister && !registerRateLimiter.take(clientIp(req))) {
|
|
252
|
+
res.setHeader("Retry-After", "12");
|
|
253
|
+
jsonError(res, "rate_limited", 429);
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
const registerBody = isRegister ? await checkRegister(req, res) : null;
|
|
257
|
+
if (isRegister && registerBody === null) return true;
|
|
258
|
+
|
|
259
|
+
const headers = new Headers();
|
|
260
|
+
const authorization = req.headers.authorization?.trim();
|
|
261
|
+
// Only an agent-fs key goes upstream: never on register (the swarm credential
|
|
262
|
+
// authorized it), and never the swarm API key, even when it looks like one.
|
|
263
|
+
if (
|
|
264
|
+
registerBody === null &&
|
|
265
|
+
authorization &&
|
|
266
|
+
AGENT_FS_BEARER.test(authorization) &&
|
|
267
|
+
authorization.replace(/^bearer\s+/i, "") !== getApiKey()
|
|
268
|
+
) {
|
|
269
|
+
headers.set("authorization", authorization);
|
|
270
|
+
}
|
|
271
|
+
for (const name of REQUEST_HEADER_ALLOWLIST) {
|
|
272
|
+
const v = req.headers[name];
|
|
273
|
+
if (typeof v === "string") headers.set(name, v);
|
|
274
|
+
}
|
|
275
|
+
if (registerBody !== null) {
|
|
276
|
+
// The body was read and re-encoded: fetch sets the new length.
|
|
277
|
+
headers.delete("content-length");
|
|
278
|
+
headers.set("content-type", "application/json");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const ac = new AbortController();
|
|
282
|
+
res.on("close", () => {
|
|
283
|
+
if (!res.writableEnded) ac.abort();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const hasBody = method !== "GET" && method !== "DELETE";
|
|
287
|
+
let upstream: Response;
|
|
288
|
+
try {
|
|
289
|
+
const init: RequestInit & { duplex?: "half" } = {
|
|
290
|
+
method,
|
|
291
|
+
headers,
|
|
292
|
+
redirect: "manual",
|
|
293
|
+
signal: ac.signal,
|
|
294
|
+
};
|
|
295
|
+
if (registerBody !== null) {
|
|
296
|
+
init.body = registerBody;
|
|
297
|
+
} else if (hasBody) {
|
|
298
|
+
// Stream the request body; Bun and undici need duplex "half" for that.
|
|
299
|
+
init.body = Readable.toWeb(req) as RequestInit["body"];
|
|
300
|
+
init.duplex = "half";
|
|
301
|
+
}
|
|
302
|
+
upstream = await fetch(upstreamUrl, init);
|
|
303
|
+
} catch {
|
|
304
|
+
if (!res.headersSent && !res.destroyed) jsonError(res, "agent-fs upstream unavailable", 502);
|
|
305
|
+
else res.destroy();
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (isPublicPage) {
|
|
310
|
+
// agent-fs owns CORS on public pages: drop the swarm's echo before copying its headers.
|
|
311
|
+
for (const name of res.getHeaderNames()) {
|
|
312
|
+
if (name.startsWith("access-control-")) res.removeHeader(name);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
const encoded = upstream.headers.has("content-encoding");
|
|
316
|
+
upstream.headers.forEach((value, name) => {
|
|
317
|
+
const n = name.toLowerCase();
|
|
318
|
+
const keep =
|
|
319
|
+
RESPONSE_HEADER_ALLOWLIST.has(n) ||
|
|
320
|
+
n.startsWith("x-agent-fs-") ||
|
|
321
|
+
(isPublicPage && n.startsWith("access-control-"));
|
|
322
|
+
if (!keep) return;
|
|
323
|
+
// fetch() already decoded the body, so the encoded length no longer applies.
|
|
324
|
+
if (encoded && n === "content-length") return;
|
|
325
|
+
res.setHeader(name, value);
|
|
326
|
+
});
|
|
327
|
+
if (isApi) {
|
|
328
|
+
// Let allowlisted browser origins read the agent-fs response headers (the base
|
|
329
|
+
// list in setCorsHeaders only covers Content-Type, Content-Length, ETag, Location).
|
|
330
|
+
const expose = res.getHeader("Access-Control-Expose-Headers");
|
|
331
|
+
if (typeof expose === "string" && expose !== "*") {
|
|
332
|
+
res.setHeader("Access-Control-Expose-Headers", `${expose}, ${EXPOSED_HEADERS.join(", ")}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
res.writeHead(upstream.status);
|
|
336
|
+
if (!upstream.body) {
|
|
337
|
+
res.end();
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
await pump(upstream.body, res);
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** Copy the upstream body to `res`, waiting for `drain` so a slow client never buffers the file. */
|
|
345
|
+
async function pump(body: ReadableStream<Uint8Array>, res: ServerResponse): Promise<void> {
|
|
346
|
+
const reader = body.getReader();
|
|
347
|
+
const closed = new Promise<void>((resolve) => {
|
|
348
|
+
if (res.destroyed) resolve();
|
|
349
|
+
else res.once("close", () => resolve());
|
|
350
|
+
});
|
|
351
|
+
try {
|
|
352
|
+
for (;;) {
|
|
353
|
+
if (res.destroyed) break;
|
|
354
|
+
const { done, value } = await reader.read();
|
|
355
|
+
if (done) {
|
|
356
|
+
res.end();
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
if (!res.write(value)) {
|
|
360
|
+
await Promise.race([new Promise((resolve) => res.once("drain", resolve)), closed]);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
} catch {
|
|
364
|
+
res.destroy();
|
|
365
|
+
} finally {
|
|
366
|
+
reader.cancel().catch(() => {});
|
|
367
|
+
}
|
|
368
|
+
await closed;
|
|
369
|
+
}
|
package/src/http/all-routes.ts
CHANGED
package/src/http/api-keys.ts
CHANGED
|
@@ -243,14 +243,28 @@ const getAvailable = route({
|
|
|
243
243
|
pattern: ["api", "keys", "available"],
|
|
244
244
|
summary: "Get available (non-rate-limited) key indices for a credential type",
|
|
245
245
|
tags: ["API Keys"],
|
|
246
|
-
query: z
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
246
|
+
query: z
|
|
247
|
+
.object({
|
|
248
|
+
keyType: z.string(),
|
|
249
|
+
totalKeys: z.coerce.number().int().min(1),
|
|
250
|
+
scope: z.string().optional(),
|
|
251
|
+
scopeId: z.string().optional(),
|
|
252
|
+
/** Model family the caller is about to run. Filters out keys with an active weekly window block for that family. */
|
|
253
|
+
model: z.enum(["fable", "opus", "sonnet", "haiku"]).optional(),
|
|
254
|
+
/**
|
|
255
|
+
* Comma-separated key suffixes of the caller's pool, in pool order. When set,
|
|
256
|
+
* key status is matched by suffix across all scopes (not by stored index or
|
|
257
|
+
* `scope`), and returned indices are positions in this list.
|
|
258
|
+
*/
|
|
259
|
+
keySuffixes: z
|
|
260
|
+
.string()
|
|
261
|
+
.transform((value) => value.split(","))
|
|
262
|
+
.optional(),
|
|
263
|
+
})
|
|
264
|
+
.refine((query) => !query.keySuffixes || query.keySuffixes.length === query.totalKeys, {
|
|
265
|
+
message: "keySuffixes must list exactly totalKeys suffixes",
|
|
266
|
+
path: ["keySuffixes"],
|
|
267
|
+
}),
|
|
254
268
|
responses: {
|
|
255
269
|
200: {
|
|
256
270
|
description: "List of available key indices",
|
|
@@ -690,7 +704,7 @@ export async function handleApiKeys(
|
|
|
690
704
|
const parsed = await getAvailable.parse(req, res, pathSegments, queryParams);
|
|
691
705
|
if (!parsed) return true;
|
|
692
706
|
|
|
693
|
-
const { keyType, totalKeys, scope, scopeId, model } = parsed.query;
|
|
707
|
+
const { keyType, totalKeys, scope, scopeId, model, keySuffixes } = parsed.query;
|
|
694
708
|
try {
|
|
695
709
|
const result = await getAvailableKeyIndices(
|
|
696
710
|
keyType,
|
|
@@ -698,6 +712,7 @@ export async function handleApiKeys(
|
|
|
698
712
|
scope,
|
|
699
713
|
scopeId ?? null,
|
|
700
714
|
model,
|
|
715
|
+
keySuffixes,
|
|
701
716
|
);
|
|
702
717
|
getAvailable.respond(res, 200, {
|
|
703
718
|
success: true,
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { decryptSecret, encryptSecret, getEncryptionKey } from "../be/crypto";
|
|
4
|
-
import { getDbClient, upsertSwarmConfig } from "../be/db";
|
|
4
|
+
import { getDbClient, recordKeyValidation, upsertSwarmConfig } from "../be/db";
|
|
5
5
|
import { getCodexDeviceFlow, upsertCodexDeviceFlow } from "../be/db/codex-oauth-device-flows";
|
|
6
6
|
import { updateOnboardingAiFromCodexDevice } from "../be/onboarding";
|
|
7
|
+
import { deriveCodexKeySuffix } from "../providers/codex-oauth/auth-json";
|
|
7
8
|
import {
|
|
8
9
|
CODEX_DEVICE_VERIFICATION_URL,
|
|
9
10
|
DeviceCodeNotEnabledError,
|
|
@@ -214,6 +215,12 @@ async function completeFlow(
|
|
|
214
215
|
isSecret: true,
|
|
215
216
|
description: `Codex ChatGPT OAuth credentials slot ${slot} (stored by dashboard device login)`,
|
|
216
217
|
});
|
|
218
|
+
// OpenAI just issued this token. Record the account as checked, so the
|
|
219
|
+
// account list says "Signed in" now: a worker's Codex check is a presence
|
|
220
|
+
// check and reports no per-account result.
|
|
221
|
+
const keySuffix = deriveCodexKeySuffix(credentials.access, credentials.accountId);
|
|
222
|
+
const checked = [{ keyIndex: slot, keySuffix, ok: true }];
|
|
223
|
+
await recordKeyValidation("CODEX_OAUTH", checked, "global", null, { retireAbsent: false });
|
|
217
224
|
await updateOnboardingAiFromCodexDevice({ status: "complete" });
|
|
218
225
|
state.status = "complete";
|
|
219
226
|
state.slot = slot;
|