@desplega.ai/agent-swarm 2.5.4 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (271) hide show
  1. package/README.md +2 -2
  2. package/dist/{acp-adapter-ek38cwsv.js → acp-adapter-dgfakfpw.js} +9 -9
  3. package/dist/{actions-92mepq03.js → actions-fbqma8aa.js} +21 -21
  4. package/dist/{amp-adapter-9gaxg015.js → amp-adapter-en0jy6hs.js} +4 -4
  5. package/dist/{app-ctn8gsgx.js → app-k8a5cgpp.js} +3 -3
  6. package/dist/{assistant-zyr5kz47.js → assistant-a16433gr.js} +29 -25
  7. package/dist/{boot-reembed-dwzfttdr.js → boot-reembed-6c1sjfnr.js} +1 -1
  8. package/dist/{boot-reembed-w75maga2.js → boot-reembed-f4nn561n.js} +1 -1
  9. package/dist/{boot-scrub-sweep-e9kq43kf.js → boot-scrub-sweep-d4w9gajf.js} +4 -4
  10. package/dist/{boot-seeds-x28v3fhb.js → boot-seeds-9bmkspyw.js} +4 -4
  11. package/dist/{claude-adapter-f0afqnne.js → claude-adapter-nys1kjxa.js} +9 -9
  12. package/dist/{claude-managed-adapter-x81a7r65.js → claude-managed-adapter-55207973.js} +2 -2
  13. package/dist/{claude-managed-setup-bqzwjd9x.js → claude-managed-setup-3cm35gey.js} +10 -3
  14. package/dist/{claude-sdk-session-7b7whkaq.js → claude-sdk-session-nw5yd8cp.js} +11 -11
  15. package/dist/{cli-c61mbdds.js → cli-03gg9pd1.js} +4 -4
  16. package/dist/{cli-kxpfx2xr.js → cli-0z1xjzwa.js} +2 -2
  17. package/dist/cli-1tem6ns9.js +894 -0
  18. package/dist/{cli-jbnn4vxb.js → cli-24j1jeeb.js} +3 -3
  19. package/dist/{cli-pzatvw94.js → cli-2sjhpsns.js} +2 -2
  20. package/dist/{cli-dew6x255.js → cli-39aet655.js} +2 -2
  21. package/dist/{cli-g1cw7xtt.js → cli-39rrw8yp.js} +1 -1
  22. package/dist/{cli-x0f2v69c.js → cli-3aqd4jaa.js} +12 -7
  23. package/dist/{cli-gbfxp48k.js → cli-3fwqy71j.js} +15 -15
  24. package/dist/{cli-7p4jxgtt.js → cli-3gfvmzht.js} +6 -6
  25. package/dist/{cli-03vpq45j.js → cli-3ndmpxxv.js} +2 -2
  26. package/dist/{cli-asq5ydm4.js → cli-4hxrf0p4.js} +131 -50
  27. package/dist/{cli-mz9afxxf.js → cli-4rf0f5vg.js} +15 -3
  28. package/dist/{cli-k4tjsdx9.js → cli-5rj83bam.js} +272 -195
  29. package/dist/{cli-sbwsxvtd.js → cli-5sckpqcx.js} +2 -2
  30. package/dist/{cli-cz5nmdxt.js → cli-683y0b3a.js} +2 -2
  31. package/dist/{cli-ethgy7ha.js → cli-6y51ryva.js} +2 -2
  32. package/dist/{cli-n4c8e3s5.js → cli-708wdy32.js} +1 -1
  33. package/dist/{cli-pasyvjmm.js → cli-737pyqxr.js} +1 -1
  34. package/dist/{cli-gx4rm0qx.js → cli-7enjwya9.js} +1 -1
  35. package/dist/{cli-6x9ma5w5.js → cli-803188pb.js} +1 -1
  36. package/dist/{cli-hh5we352.js → cli-9abdgn5t.js} +7 -7
  37. package/dist/{cli-ff1tm9n9.js → cli-a2rr0fmg.js} +1 -1
  38. package/dist/{cli-c952c7ac.js → cli-a34pr3w1.js} +2 -2
  39. package/dist/{cli-s682zgqz.js → cli-abkm71vr.js} +1 -1
  40. package/dist/{cli-mmhj8vd7.js → cli-ag2yrnt5.js} +11 -2
  41. package/dist/{cli-bvpgr9ny.js → cli-aj8djyqt.js} +5 -5
  42. package/dist/{cli-gwhjmh8g.js → cli-art7c1zh.js} +5 -5
  43. package/dist/{cli-5bnsc0e1.js → cli-az25fn5f.js} +4 -4
  44. package/dist/{cli-9s05422w.js → cli-bqztfyve.js} +1 -1
  45. package/dist/{cli-bn0rewsy.js → cli-c081bj3b.js} +2 -2
  46. package/dist/{cli-1r5ad7j5.js → cli-c2habvsf.js} +12 -12
  47. package/dist/{cli-64mh9mf3.js → cli-ca6banwz.js} +69 -17
  48. package/dist/{cli-25n3mv5d.js → cli-cz37jqss.js} +5 -5
  49. package/dist/{cli-6n8axjv1.js → cli-d2dpf95n.js} +2 -2
  50. package/dist/{cli-gn5af0x5.js → cli-emhjcj2y.js} +11 -11
  51. package/dist/{cli-6a1xg1p7.js → cli-esakngh8.js} +2 -2
  52. package/dist/{cli-ha9rmwgq.js → cli-fb8m699c.js} +2 -2
  53. package/dist/{cli-4b2z6n0m.js → cli-ffvsj2qw.js} +17 -8
  54. package/dist/{cli-abs8ndtr.js → cli-g20jjwt4.js} +26 -26
  55. package/dist/{cli-jb30dzvk.js → cli-g8ka5j4j.js} +446 -164
  56. package/dist/{cli-19202kew.js → cli-gf95q3bm.js} +5 -5
  57. package/dist/{cli-eftzyh9y.js → cli-gnsb1zxa.js} +2 -2
  58. package/dist/{cli-nwyw3zhg.js → cli-gxee0bqa.js} +1 -1
  59. package/dist/{cli-e42vss7b.js → cli-hfeb68z4.js} +5 -5
  60. package/dist/{cli-g9vd9ece.js → cli-k21gx5ht.js} +14 -16
  61. package/dist/{cli-tsma7p8v.js → cli-kf4jree9.js} +2 -2
  62. package/dist/{cli-dabehxhp.js → cli-kt907da6.js} +1 -1
  63. package/dist/{cli-wx2nj19m.js → cli-m5a9m0ak.js} +6 -6
  64. package/dist/{cli-621ce7kw.js → cli-m70bajzr.js} +3 -3
  65. package/dist/{cli-9j129brq.js → cli-mtnj4fwa.js} +6 -6
  66. package/dist/{cli-xzfq4at3.js → cli-mvjz5an3.js} +15 -11
  67. package/dist/{cli-277c31dw.js → cli-n7asmce6.js} +225 -3
  68. package/dist/{cli-xtrz6bzs.js → cli-nn6vx2mq.js} +1 -1
  69. package/dist/{cli-2655k16q.js → cli-nq677csy.js} +1 -1
  70. package/dist/{cli-0shx9j93.js → cli-pfk92eqz.js} +2 -2
  71. package/dist/{cli-wghjkr0e.js → cli-sqma6vw1.js} +10 -7
  72. package/dist/{cli-nzfnt2gs.js → cli-tf1n0r0b.js} +1 -1
  73. package/dist/{cli-qqzmjxkf.js → cli-v49fkghp.js} +11 -6
  74. package/dist/{cli-x77gen2p.js → cli-w2656ka1.js} +7 -7
  75. package/dist/{cli-5kqnemen.js → cli-yvy48785.js} +1 -1
  76. package/dist/{cli-m4ecdxvz.js → cli-zqne1qcj.js} +7 -7
  77. package/dist/cli.js +17 -17
  78. package/dist/{codex-adapter-wch12q99.js → codex-adapter-d95q4qsn.js} +7 -7
  79. package/dist/{codex-login-rn7c5gxt.js → codex-login-8cg0vhf2.js} +1 -1
  80. package/dist/{codex-session-runner-41pfcr95.js → codex-session-runner-gdwd8h0c.js} +8 -8
  81. package/dist/{commands-9849dkj6.js → commands-jqmgk54b.js} +2 -2
  82. package/dist/{controller-ja0sd95p.js → controller-z3jq589f.js} +4 -4
  83. package/dist/{controllers-connect-xp05dj6x.js → controllers-connect-r2za39ef.js} +23 -23
  84. package/dist/{cursor-adapter-623qxqew.js → cursor-adapter-4mr4jgkw.js} +2 -2
  85. package/dist/{db-47vj40dy.js → db-ppv2tze7.js} +3 -3
  86. package/dist/{db-w759qmx0.js → db-w36wwb5g.js} +4 -2
  87. package/dist/{devin-adapter-4ycy4vrf.js → devin-adapter-byen2cxt.js} +1 -1
  88. package/dist/{dispatcher-7b8e4z2j.js → dispatcher-pv5h77vf.js} +20 -20
  89. package/dist/{docker-s84hjs91.js → docker-fdzyewpz.js} +2 -2
  90. package/dist/{dsh-adapter-2ezw3xea.js → dsh-adapter-d4pdpb3r.js} +2 -2
  91. package/dist/{e2b-9gkdhm3c.js → e2b-222hsajc.js} +2 -2
  92. package/dist/{e2b-cjtf8k1z.js → e2b-fn3zacvt.js} +2 -2
  93. package/dist/{easter-egg-3d-e4qv8dxj.js → easter-egg-3d-4sp4dvmc.js} +1 -1
  94. package/dist/{event-bus-3z78ep5n.js → event-bus-pghg8hqy.js} +1 -1
  95. package/dist/{execute-0xpz8950.js → execute-jev839y1.js} +1 -1
  96. package/dist/{external-channel-e2s028da.js → external-channel-y6nkzhnn.js} +2 -2
  97. package/dist/{grok-adapter-ys7gaqee.js → grok-adapter-t08b9ntm.js} +10 -10
  98. package/dist/handlers-eqsnmv95.js +41 -0
  99. package/dist/{heartbeat-w3mntghj.js → heartbeat-vn31xwdk.js} +23 -23
  100. package/dist/{hook-g7kvv6x9.js → hook-6d0zbvvt.js} +6 -6
  101. package/dist/{hook-3jjywnpb.js → hook-cpjs8q51.js} +8 -8
  102. package/dist/{http-tkp7d4kb.js → http-9c6medda.js} +856 -99
  103. package/dist/{index-xdaahthb.js → index-38t35h0n.js} +20 -20
  104. package/dist/{index-3n8m9ea2.js → index-g6k9tctj.js} +1 -1
  105. package/dist/{index-1ef1myq2.js → index-k0yt1tvq.js} +20 -20
  106. package/dist/{index-31ceyf42.js → index-pkma1yf8.js} +1 -1
  107. package/dist/{index-q7m26kvs.js → index-qhjf2htm.js} +13 -10
  108. package/dist/index-yqgbfgaj.js +43 -0
  109. package/dist/{keepalive-v4ta8wgg.js → keepalive-em0117bp.js} +4 -4
  110. package/dist/lead-23e7z59t.js +79 -0
  111. package/dist/{maintenance-84n1ex21.js → maintenance-kbme3qy9.js} +1 -1
  112. package/dist/{oauth-refresh-sweep-vjnnandw.js → oauth-refresh-sweep-xysj4yse.js} +3 -3
  113. package/dist/{onboard-sfmpe4np.js → onboard-p6q0vz97.js} +3 -3
  114. package/dist/{opencode-adapter-jmpst4q4.js → opencode-adapter-esv3k9yn.js} +7 -7
  115. package/dist/{otel-impl-v6cc0zve.js → otel-impl-54qhqh1z.js} +2 -2
  116. package/dist/{pi-mono-adapter-fk8fkcf6.js → pi-mono-adapter-mymbwhj0.js} +10 -10
  117. package/dist/{pricing-refresh-fsk1e1zc.js → pricing-refresh-yb75wv33.js} +3 -3
  118. package/dist/{rbac-roles-prmkdnkw.js → rbac-roles-1vp2mw4p.js} +2 -2
  119. package/dist/{rbac-roles-zxmxqa39.js → rbac-roles-dexhc9x8.js} +1 -1
  120. package/dist/render-v2-tjay5hhf.js +16 -0
  121. package/dist/{resume-frdtgfa0.js → resume-8ayf37g5.js} +9 -9
  122. package/dist/{runtime-bt5pkpm1.js → runtime-hqe5cqcd.js} +1 -1
  123. package/dist/{session-access-fpez4y5q.js → session-access-fdhp987s.js} +4 -4
  124. package/dist/session-source-backfill-qgtc86z1.js +110 -0
  125. package/dist/{setup-mq6m3m8q.js → setup-4rrmqcq3.js} +1 -1
  126. package/dist/{virtual-modules-mnfbh5mr.js → virtual-modules-52km00d6.js} +2 -2
  127. package/dist/{work-objects-kfahhjsq.js → work-objects-p28fgd70.js} +23 -23
  128. package/dist/worker-e3w7vye1.js +79 -0
  129. package/dist/{x-pbpezr0s.js → x-wxrzvzx8.js} +2 -2
  130. package/openapi.json +1274 -32
  131. package/package.json +1 -1
  132. package/src/assets/source-key.ts +95 -0
  133. package/src/be/db/api-keys.ts +80 -0
  134. package/src/be/db/session-labels.ts +28 -0
  135. package/src/be/db/session-moves.ts +5 -6
  136. package/src/be/db/session-tree.ts +153 -14
  137. package/src/be/db/tasks/read.ts +11 -0
  138. package/src/be/db/tasks/write.ts +6 -1
  139. package/src/be/db/workflow-runs.ts +183 -1
  140. package/src/be/db-retention.ts +61 -6
  141. package/src/be/db.ts +59 -114
  142. package/src/be/memory/providers/openai-embedding.ts +5 -1
  143. package/src/be/migrations/244_api_key_validation.sql +4 -0
  144. package/src/be/onboarding.ts +4 -0
  145. package/src/be/scripts/typecheck.ts +1 -1
  146. package/src/be/session-source-backfill.ts +163 -0
  147. package/src/be/swarm-config-guard.ts +1 -0
  148. package/src/comb/review-batch.ts +28 -1
  149. package/src/comb/templates.ts +7 -2
  150. package/src/commands/claude-managed-setup.ts +7 -1
  151. package/src/commands/credential-refresh.ts +76 -1
  152. package/src/commands/credential-wait.ts +2 -2
  153. package/src/commands/provider-credentials.ts +210 -170
  154. package/src/commands/runner.ts +25 -4
  155. package/src/extensions/apply-task-create.ts +8 -1
  156. package/src/extensions/contract-types.generated.ts +1 -1
  157. package/src/extensions/contract.ts +5 -0
  158. package/src/http/all-routes.ts +2 -0
  159. package/src/http/api-keys.ts +53 -0
  160. package/src/http/assets.ts +15 -0
  161. package/src/http/codex-oauth-accounts.ts +191 -0
  162. package/src/http/comb.ts +15 -2
  163. package/src/http/config.ts +1 -1
  164. package/src/http/core.ts +6 -0
  165. package/src/http/fleet-benchmark.ts +5 -2
  166. package/src/http/index.ts +11 -0
  167. package/src/http/integrations.ts +5 -1
  168. package/src/http/kv.ts +8 -3
  169. package/src/http/onboarding.ts +6 -1
  170. package/src/http/pool-keys.ts +354 -0
  171. package/src/http/sessions.ts +37 -12
  172. package/src/http/stats.ts +1 -0
  173. package/src/http/status.ts +26 -12
  174. package/src/http/tasks.ts +3 -0
  175. package/src/http/used-by.ts +7 -2
  176. package/src/http/workflows.ts +78 -9
  177. package/src/integrations/chat-sdk/inbound.ts +27 -0
  178. package/src/kv-reserved-namespaces.ts +19 -0
  179. package/src/providers/acp-targets.ts +1 -1
  180. package/src/providers/amp-adapter.ts +1 -1
  181. package/src/providers/claude-adapter.ts +5 -1
  182. package/src/providers/claude-managed-adapter.ts +16 -9
  183. package/src/providers/codex-adapter.ts +71 -11
  184. package/src/scheduler/scheduler.ts +3 -2
  185. package/src/scripts-runtime/swarm-sdk.ts +7 -1
  186. package/src/scripts-runtime/types/stdlib.d.ts +1 -1
  187. package/src/scripts-runtime/types/swarm-extension.d.ts +17 -8
  188. package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -1
  189. package/src/slack/assistant.ts +7 -0
  190. package/src/slack/channel-labels.ts +74 -0
  191. package/src/slack/handlers.ts +8 -0
  192. package/src/slack/inbound-files.ts +5 -2
  193. package/src/slack/thread-buffer.ts +4 -0
  194. package/src/tasks/sibling-awareness.ts +12 -3
  195. package/src/telemetry-context.ts +5 -6
  196. package/src/telemetry-identity.ts +3 -3
  197. package/src/tests/api-keys-validation.test.ts +170 -0
  198. package/src/tests/claude-managed-adapter.test.ts +46 -0
  199. package/src/tests/claude-routes-e2e.test.ts +7 -2
  200. package/src/tests/codex-credential-pools.test.ts +120 -0
  201. package/src/tests/codex-gateway-route.test.ts +165 -0
  202. package/src/tests/codex-oauth-accounts.test.ts +357 -0
  203. package/src/tests/comb-review-batch.test.ts +63 -2
  204. package/src/tests/config-pool-keys.test.ts +533 -0
  205. package/src/tests/credential-check.test.ts +290 -2
  206. package/src/tests/credential-refresh.test.ts +172 -1
  207. package/src/tests/db-retention.test.ts +9 -3
  208. package/src/tests/db-task-read-contract.test.ts +1 -0
  209. package/src/tests/db-task-write-contract.test.ts +1 -3
  210. package/src/tests/docs-index-footprint.test.js +3 -3
  211. package/src/tests/extensions-pre-task.test.ts +53 -1
  212. package/src/tests/fixtures/extensions/record-task-create/hooks.ts +9 -0
  213. package/src/tests/fixtures/extensions/record-task-create/manifest.json +7 -0
  214. package/src/tests/fleet-benchmark.test.ts +8 -0
  215. package/src/tests/get-workflow-run-tool.test.ts +98 -0
  216. package/src/tests/integrations-http.test.ts +32 -2
  217. package/src/tests/internal-ai/complete-structured.test.ts +37 -1
  218. package/src/tests/internal-ai/credentials.test.ts +23 -0
  219. package/src/tests/kv-http.test.ts +32 -0
  220. package/src/tests/kv-tool.test.ts +25 -0
  221. package/src/tests/multi-runtime-registration.test.ts +12 -12
  222. package/src/tests/onboarding.test.ts +108 -1
  223. package/src/tests/openai-embedding-credentials.test.ts +35 -0
  224. package/src/tests/openrouter-attribution-call-sites.test.ts +2 -2
  225. package/src/tests/org-website.test.ts +36 -0
  226. package/src/tests/provider-key-probes.test.ts +181 -0
  227. package/src/tests/rbac-e2e-helpers.test.ts +88 -1
  228. package/src/tests/rbac-e2e-helpers.ts +36 -11
  229. package/src/tests/script-connections.test.ts +2 -0
  230. package/src/tests/script-runs-retention.test.ts +212 -0
  231. package/src/tests/sdk-allowlist.test.ts +39 -0
  232. package/src/tests/session-source-backfill.test.ts +429 -0
  233. package/src/tests/session-source-keys.test.ts +236 -0
  234. package/src/tests/session-source-labels.test.ts +419 -0
  235. package/src/tests/sessions-detail-batch.test.ts +2 -0
  236. package/src/tests/sessions-move.test.ts +131 -0
  237. package/src/tests/sessions-tree.test.ts +299 -2
  238. package/src/tests/slack-inbound-files.test.ts +103 -23
  239. package/src/tests/status.test.ts +101 -18
  240. package/src/tests/tasks-workflow-run-filter.test.ts +146 -0
  241. package/src/tests/test-net.ts +31 -0
  242. package/src/tests/transcription.test.ts +8 -0
  243. package/src/tests/used-by-route.test.ts +11 -0
  244. package/src/tests/workflow-cascade-cancel.test.ts +172 -0
  245. package/src/tests/workflow-http-v2.test.ts +131 -0
  246. package/src/tests/workflow-step-order.test.ts +92 -0
  247. package/src/tests/workflow-sub-workflow.test.ts +30 -0
  248. package/src/tools/get-tasks.ts +8 -0
  249. package/src/tools/kv/kv-read-auth.ts +4 -2
  250. package/src/tools/task-action.ts +1 -1
  251. package/src/tools/workflows/get-workflow-run.ts +14 -4
  252. package/src/types.ts +24 -0
  253. package/src/utils/company-profile.ts +51 -1
  254. package/src/utils/credentials.ts +5 -2
  255. package/src/utils/internal-ai/complete-structured.ts +5 -0
  256. package/src/utils/internal-ai/credentials.ts +4 -3
  257. package/src/utils/provider-key-probes.ts +328 -0
  258. package/src/utils/transcription.ts +4 -1
  259. package/src/workflows/executors/sub-workflow.ts +8 -1
  260. package/templates/ai-toolbox.manifest.json +2 -2
  261. package/templates/skills/researching/content.md +2 -0
  262. package/templates/skills/swarm-scripts/SKILL.md +1 -0
  263. package/templates/skills/swarm-scripts/content.md +1 -0
  264. package/dist/cli-7421jzsw.js +0 -84
  265. package/dist/cli-e45e4m6b.js +0 -413
  266. package/dist/cli-zw88zjba.js +0 -307
  267. package/dist/handlers-wy1dr6ja.js +0 -41
  268. package/dist/index-nmm79qgr.js +0 -43
  269. package/dist/lead-sawnyqxk.js +0 -79
  270. package/dist/render-v2-pjwxvnkw.js +0 -12
  271. package/dist/worker-v5e5rxj3.js +0 -79
@@ -19,7 +19,6 @@
19
19
  */
20
20
 
21
21
  import {
22
- type OpenAICompatibleProvider,
23
22
  openAICompatibleKeyVars,
24
23
  openAICompatibleProviders,
25
24
  providerBaseUrl,
@@ -45,13 +44,35 @@ import type {
45
44
  ProviderName,
46
45
  ReasoningEffort,
47
46
  } from "../types";
48
- import { CLAUDE_CREDENTIALS_HINT } from "../utils/credentials";
49
47
  import {
50
- getOpenRouterAttributionHeaders,
51
- getOpenRouterBaseUrl,
52
- } from "../utils/openrouter-base-url";
48
+ CLAUDE_CREDENTIALS_HINT,
49
+ CREDENTIAL_POOL_VARS,
50
+ splitCredentialPool,
51
+ } from "../utils/credentials";
52
+ import {
53
+ checkAnthropicApiKey,
54
+ checkDeepSeekApiKey,
55
+ checkOpenAICompatibleProvider,
56
+ checkOpenAiApiKey,
57
+ checkOpenRouter,
58
+ checkXaiApiKey,
59
+ type LiveKeyResult,
60
+ type LiveValidationResult,
61
+ timedFetch,
62
+ } from "../utils/provider-key-probes";
53
63
  import { scrubSecrets } from "../utils/secret-scrubber";
54
64
 
65
+ export {
66
+ checkAnthropicApiKey,
67
+ checkDeepSeekApiKey,
68
+ checkOpenAICompatibleProvider,
69
+ checkOpenAiApiKey,
70
+ checkOpenRouter,
71
+ checkXaiApiKey,
72
+ type LiveKeyResult,
73
+ type LiveValidationResult,
74
+ } from "../utils/provider-key-probes";
75
+
55
76
  export type SupportedProvider =
56
77
  | "claude"
57
78
  | "claude-managed"
@@ -215,46 +236,8 @@ export async function checkProviderCredentials(
215
236
  // Mirrors `checkProviderCredentials` but issues a real (cheapest possible)
216
237
  // upstream call so the user can flip the `harness` milestone to `verified`.
217
238
  //
218
- // Pure function — no DB writes. Lives here (not on `ProviderAdapter`) because
219
- // adapters are runtime-loaded by workers, while this dispatcher is API-server
220
- // safe. All errors run through `scrubSecrets` so we never leak the user's
221
- // own key shape back through the JSON response.
222
-
223
- const LIVE_TEST_TIMEOUT_MS = 5_000;
224
-
225
- export interface LiveValidationResult {
226
- ok: boolean;
227
- error?: string;
228
- latency_ms: number;
229
- /** No free live check exists for this credential (cloud routes): report presence only. */
230
- skipped?: boolean;
231
- }
232
-
233
- async function timedFetch(
234
- url: string,
235
- init: RequestInit,
236
- ): Promise<{ ok: boolean; status: number; bodyText: string; latency_ms: number }> {
237
- const controller = new AbortController();
238
- const timer = setTimeout(() => controller.abort(), LIVE_TEST_TIMEOUT_MS);
239
- const start = Date.now();
240
- try {
241
- const res = await fetch(url, { ...init, signal: controller.signal });
242
- let bodyText = "";
243
- try {
244
- bodyText = await res.text();
245
- } catch {
246
- // ignore body read errors — status alone is enough for ok/not-ok
247
- }
248
- return {
249
- ok: res.ok,
250
- status: res.status,
251
- bodyText,
252
- latency_ms: Date.now() - start,
253
- };
254
- } finally {
255
- clearTimeout(timer);
256
- }
257
- }
239
+ // Worker-only dispatch uses the shared probes in `provider-key-probes.ts`.
240
+ // The API imports those probes directly to avoid loading worker harnesses.
258
241
 
259
242
  function asLiveError(err: unknown, latency_ms: number): LiveValidationResult {
260
243
  const raw = err instanceof Error ? err.message : String(err);
@@ -265,6 +248,69 @@ function asLiveError(err: unknown, latency_ms: number): LiveValidationResult {
265
248
  };
266
249
  }
267
250
 
251
+ /**
252
+ * Run `check` on every member of a comma pool in parallel. The pool passes
253
+ * when one key works (the runner skips a rejected key); `warning` names the
254
+ * keys that failed, and the check fails only when every key fails.
255
+ */
256
+ export async function checkEachKey(
257
+ keyType: string,
258
+ value: string,
259
+ check: (key: string) => Promise<LiveValidationResult>,
260
+ ): Promise<LiveValidationResult> {
261
+ const members = splitCredentialPool(value);
262
+ if (members.length === 0) members.push(value.trim());
263
+ const results = await Promise.all(
264
+ members.map(async (member) => {
265
+ const startedAt = Date.now();
266
+ try {
267
+ return await check(member);
268
+ } catch (err) {
269
+ return asLiveError(err, Date.now() - startedAt);
270
+ }
271
+ }),
272
+ );
273
+ const latency_ms = Math.max(...results.map((result) => result.latency_ms));
274
+ if (results.every((result) => result.skipped)) return { ok: true, skipped: true, latency_ms };
275
+ const keys = results.map(
276
+ (result, index): LiveKeyResult => ({
277
+ keyType,
278
+ index,
279
+ suffix: members[index]!.slice(-5),
280
+ ok: result.ok,
281
+ ...(result.error !== undefined ? { error: result.error } : {}),
282
+ ...(result.httpStatus !== undefined ? { httpStatus: result.httpStatus } : {}),
283
+ latency_ms: result.latency_ms,
284
+ }),
285
+ );
286
+ const failed = keys.filter((key) => !key.ok);
287
+ if (failed.length === 0) return { ok: true, latency_ms, keys };
288
+ const failures = failed
289
+ .map(
290
+ (key) =>
291
+ `key ending ${key.suffix} (${key.httpStatus !== undefined ? `HTTP ${key.httpStatus}` : (key.error ?? "Unknown error")})`,
292
+ )
293
+ .join(", ");
294
+ if (failed.length < keys.length) {
295
+ return {
296
+ ok: true,
297
+ latency_ms,
298
+ keys,
299
+ warning: `${failed.length} of ${keys.length} keys failed: ${failures}.`,
300
+ };
301
+ }
302
+ if (keys.length === 1) {
303
+ return {
304
+ ok: false,
305
+ error: failed[0]!.error,
306
+ httpStatus: failed[0]!.httpStatus,
307
+ latency_ms,
308
+ keys,
309
+ };
310
+ }
311
+ return { ok: false, error: `All ${keys.length} keys failed: ${failures}.`, latency_ms, keys };
312
+ }
313
+
268
314
  // ─── Per-endpoint live-call helpers ──────────────────────────────────────────
269
315
  // Each helper accepts the auth material it needs and returns a normalized
270
316
  // `LiveValidationResult`. The harness-level dispatcher below picks which
@@ -272,22 +318,10 @@ function asLiveError(err: unknown, latency_ms: number): LiveValidationResult {
272
318
  // users no longer see "ANTHROPIC_API_KEY is not set" when their adapter is
273
319
  // happily running off `CLAUDE_CODE_OAUTH_TOKEN`.
274
320
 
275
- async function checkAnthropicApiKey(apiKey: string): Promise<LiveValidationResult> {
276
- const r = await timedFetch("https://api.anthropic.com/v1/models", {
277
- method: "GET",
278
- headers: { "x-api-key": apiKey, "anthropic-version": "2023-06-01" },
279
- });
280
- if (r.ok) return { ok: true, latency_ms: r.latency_ms };
281
- return {
282
- ok: false,
283
- error: scrubSecrets(`HTTP ${r.status}: ${r.bodyText.slice(0, 200)}`),
284
- latency_ms: r.latency_ms,
285
- };
286
- }
287
-
288
321
  /**
289
- * OAuth credentials (Claude Pro/Max via `claude` CLI login, ChatGPT via Codex
290
- * OAuth) are treated as a presence check — we don't issue a live upstream call.
322
+ * Codex OAuth credentials (ChatGPT login) are treated as a presence check — we
323
+ * don't issue a live upstream call. (Claude subscription tokens get a real
324
+ * check through the route: see `claudeSubscriptionProvider`.)
291
325
  *
292
326
  * Why: OAuth flows include their own refresh logic (handled at adapter boot,
293
327
  * not here), the OAuth-bearer-with-/v1/models contract isn't a stable public
@@ -299,70 +333,6 @@ function presenceCheckOk(): LiveValidationResult {
299
333
  return { ok: true, latency_ms: 0 };
300
334
  }
301
335
 
302
- async function checkOpenAiApiKey(apiKey: string): Promise<LiveValidationResult> {
303
- const r = await timedFetch("https://api.openai.com/v1/models", {
304
- method: "GET",
305
- headers: { Authorization: `Bearer ${apiKey}` },
306
- });
307
- if (r.ok) return { ok: true, latency_ms: r.latency_ms };
308
- return {
309
- ok: false,
310
- error: scrubSecrets(`HTTP ${r.status}: ${r.bodyText.slice(0, 200)}`),
311
- latency_ms: r.latency_ms,
312
- };
313
- }
314
-
315
- /** `GET /v1/models` is the call the Grok CLI makes first; it runs no inference. */
316
- async function checkXaiApiKey(apiKey: string): Promise<LiveValidationResult> {
317
- const r = await timedFetch("https://api.x.ai/v1/models", {
318
- method: "GET",
319
- headers: { Authorization: `Bearer ${apiKey}` },
320
- });
321
- if (r.ok) return { ok: true, latency_ms: r.latency_ms };
322
- return {
323
- ok: false,
324
- error: scrubSecrets(`HTTP ${r.status}: ${r.bodyText.slice(0, 200)}`),
325
- latency_ms: r.latency_ms,
326
- };
327
- }
328
-
329
- async function checkOpenRouter(apiKey: string): Promise<LiveValidationResult> {
330
- const url = `${getOpenRouterBaseUrl()}/models`;
331
- const r = await timedFetch(url, {
332
- method: "GET",
333
- headers: { ...getOpenRouterAttributionHeaders(url), Authorization: `Bearer ${apiKey}` },
334
- });
335
- if (r.ok) return { ok: true, latency_ms: r.latency_ms };
336
- return {
337
- ok: false,
338
- error: scrubSecrets(`HTTP ${r.status}: ${r.bodyText.slice(0, 200)}`),
339
- latency_ms: r.latency_ms,
340
- };
341
- }
342
-
343
- /**
344
- * `GET <baseUrl>/models` with a Bearer key against an OpenAI-compatible
345
- * registry provider (NaN, Helmcode). Lists models, runs no inference.
346
- * `baseUrl` defaults to the registry entry; the probe passes the
347
- * `<ID>_BASE_URL` override, and tests inject a fake server.
348
- */
349
- export async function checkOpenAICompatibleProvider(
350
- provider: OpenAICompatibleProvider,
351
- apiKey: string,
352
- baseUrl: string = provider.baseUrl,
353
- ): Promise<LiveValidationResult> {
354
- const r = await timedFetch(`${baseUrl.replace(/\/+$/, "")}/models`, {
355
- method: "GET",
356
- headers: { Authorization: `Bearer ${apiKey}` },
357
- });
358
- if (r.ok) return { ok: true, latency_ms: r.latency_ms };
359
- return {
360
- ok: false,
361
- error: scrubSecrets(`HTTP ${r.status}: ${r.bodyText.slice(0, 200)}`),
362
- latency_ms: r.latency_ms,
363
- };
364
- }
365
-
366
336
  /**
367
337
  * Mirror of the disk check in `checkCodexCredentials` — true when the worker
368
338
  * has a materialised `~/.codex/auth.json`, which is the canonical "logged in"
@@ -402,14 +372,15 @@ function parseCodexOAuthAccess(blob: string | undefined): string | null {
402
372
  *
403
373
  * | Harness | Accepted credentials (in resolution order) | Endpoint |
404
374
  * |------------------|-------------------------------------------------------------------------|--------------------------------|
405
- * | `claude` | default route: Foundry / Bedrock / Vertex → gateway (`ANTHROPIC_BASE_URL` + `ANTHROPIC_AUTH_TOKEN` or `ANTHROPIC_API_KEY`) → `CLAUDE_CODE_OAUTH_TOKEN` → `ANTHROPIC_API_KEY` | the route's own `/v1/models`; OAuth presence-only; cloud routes skipped |
375
+ * | `claude` | default route: Foundry / Bedrock / Vertex → gateway (`ANTHROPIC_BASE_URL` + `ANTHROPIC_AUTH_TOKEN` or `ANTHROPIC_API_KEY`) → `CLAUDE_CODE_OAUTH_TOKEN` → `ANTHROPIC_API_KEY` | the route's own `/v1/models`; OAuth: `/api/oauth/profile` (403 scope = ok); cloud routes skipped |
406
376
  * | `claude-managed` | `ANTHROPIC_API_KEY` (managed agents always use API key + managed envs) | Anthropic `/v1/models` |
407
377
  * | `codex` | `~/.codex/auth.json` (file) → `CODEX_OAUTH` (env OAuth) → `OPENAI_API_KEY` | OpenAI `/v1/models` (api-key path only) |
408
- * | `opencode` | auth.json (file) → `OPENROUTER_API_KEY` → `ANTHROPIC_API_KEY` → `OPENAI_API_KEY` → each registry provider key (`NAN_API_KEY` or `HELMCODE_API_NAN`, `HELMCODE_API_KEY`) | file presence-only, otherwise provider's `/v1/models` |
378
+ * | `opencode` | auth.json (file) → `OPENROUTER_API_KEY` → `ANTHROPIC_API_KEY` → `OPENAI_API_KEY` → each registry provider key (`NAN_API_KEY` or `HELMCODE_API_NAN`, `HELMCODE_API_KEY`) | file presence-only, otherwise OpenRouter `/key` or the provider's `/v1/models` |
409
379
  * | `pi` | auth.json (file) → provider API keys, including model-specific keys | file/model-specific key presence-only, otherwise provider's `/v1/models` |
410
380
  * | `acp` | target-specific (the ACP target process owns its own auth) | presence-only (validated by the target process) |
411
381
  * | `pi` (bedrock) | `MODEL_OVERRIDE=amazon-bedrock/*` → AWS SDK default credential chain | presence-only (real check is the worker-side Bedrock enumeration) |
412
382
  * | `devin` | `DEVIN_API_KEY` (+ `DEVIN_API_BASE_URL` override) | `${baseUrl}/v3/self` |
383
+ * | `dsh` | `DEEPSEEK_API_KEY` → `OPENROUTER_API_KEY` | DeepSeek `/models`, or OpenRouter `/key` |
413
384
  * | `amp` | `AMP_API_KEY` | `amp usage` (credit balance, no inference; 5s timeout) |
414
385
  *
415
386
  * Returns `{ok: true, latency_ms}` on 2xx, `{ok: false, error, latency_ms}`
@@ -429,8 +400,9 @@ export async function validateProviderCredentials(
429
400
  case "claude": {
430
401
  // The default route decides where the check goes: a gateway key is
431
402
  // checked against the gateway only (scoped fetch), never against
432
- // api.anthropic.com. OAuth stays presence-only; cloud routes have no
433
- // free check and report `configured` (no live test).
403
+ // api.anthropic.com. A subscription token is checked against
404
+ // `/api/oauth/profile`; cloud routes have no free check and report
405
+ // `configured` (no live test).
434
406
  const route = deriveDefaultRoute("claude", env);
435
407
  if (!route) {
436
408
  return {
@@ -439,16 +411,40 @@ export async function validateProviderCredentials(
439
411
  latency_ms: Date.now() - startedAt,
440
412
  };
441
413
  }
442
- const result = await validateRoute(route, env);
443
- const latency_ms = Date.now() - startedAt;
444
- if (result.status === "verified") return { ok: true, latency_ms };
445
- if (result.status === "configured") return { ok: true, skipped: true, latency_ms };
446
- return { ok: false, error: scrubSecrets(result.reason), latency_ms };
414
+ const checkRoute = async (
415
+ routeEnv: Record<string, string | undefined>,
416
+ ): Promise<LiveValidationResult> => {
417
+ const start = Date.now();
418
+ const result = await validateRoute(route, routeEnv);
419
+ const latency_ms = Date.now() - start;
420
+ if (result.status === "verified") return { ok: true, latency_ms };
421
+ if (result.status === "configured") return { ok: true, skipped: true, latency_ms };
422
+ return {
423
+ ok: false,
424
+ error: scrubSecrets(result.reason),
425
+ httpStatus: result.httpStatus,
426
+ latency_ms,
427
+ };
428
+ };
429
+ const keyVar =
430
+ route.auth.kind === "subscription"
431
+ ? "CLAUDE_CODE_OAUTH_TOKEN"
432
+ : "secretKey" in route.auth
433
+ ? route.auth.secretKey
434
+ : undefined;
435
+ const value = keyVar ? env[keyVar] : undefined;
436
+ // Only pool vars are split: the runner never splits a gateway token.
437
+ if (keyVar && value && (CREDENTIAL_POOL_VARS as readonly string[]).includes(keyVar)) {
438
+ return checkEachKey(keyVar, value, (member) => checkRoute({ ...env, [keyVar]: member }));
439
+ }
440
+ return checkRoute(env);
447
441
  }
448
442
  case "claude-managed": {
449
443
  // Managed agents always run with an API key — OAuth not supported on
450
444
  // the managed-agents path today.
451
- if (env.ANTHROPIC_API_KEY) return checkAnthropicApiKey(env.ANTHROPIC_API_KEY);
445
+ if (env.ANTHROPIC_API_KEY) {
446
+ return checkEachKey("ANTHROPIC_API_KEY", env.ANTHROPIC_API_KEY, checkAnthropicApiKey);
447
+ }
452
448
  return {
453
449
  ok: false,
454
450
  error: "ANTHROPIC_API_KEY is not set.",
@@ -456,6 +452,22 @@ export async function validateProviderCredentials(
456
452
  };
457
453
  }
458
454
  case "codex": {
455
+ // A gateway route is checked against the gateway only (scoped fetch
456
+ // to its own `/models`), never against api.openai.com, and wins over
457
+ // every OAuth / auth.json / OPENAI_API_KEY check below.
458
+ const gateway = deriveDefaultRoute("codex", env);
459
+ if (gateway) {
460
+ const result = await validateRoute(gateway, env);
461
+ const latency_ms = Date.now() - startedAt;
462
+ if (result.status === "verified") return { ok: true, latency_ms };
463
+ if (result.status === "configured") return { ok: true, skipped: true, latency_ms };
464
+ return {
465
+ ok: false,
466
+ error: scrubSecrets(result.reason),
467
+ httpStatus: result.httpStatus,
468
+ latency_ms,
469
+ };
470
+ }
459
471
  // Resolution order matches `checkCodexCredentials`:
460
472
  // 1) `~/.codex/auth.json` on disk (canonical state once `codex login`
461
473
  // has run, or when the entrypoint pre-materialised it from
@@ -474,7 +486,9 @@ export async function validateProviderCredentials(
474
486
  if (codexAuthFileExists(env)) return presenceCheckOk();
475
487
  if (parseCodexOAuthAccess(env.CODEX_OAUTH)) return presenceCheckOk();
476
488
  if (hasCodexOAuthPoolSlots(env)) return presenceCheckOk();
477
- if (env.OPENAI_API_KEY) return checkOpenAiApiKey(env.OPENAI_API_KEY);
489
+ if (env.OPENAI_API_KEY) {
490
+ return checkEachKey("OPENAI_API_KEY", env.OPENAI_API_KEY, checkOpenAiApiKey);
491
+ }
478
492
  return {
479
493
  ok: false,
480
494
  error:
@@ -503,13 +517,21 @@ export async function validateProviderCredentials(
503
517
  // registry providers in declaration order (NaN, canonical key or
504
518
  // alias → Helmcode) at their `<ID>_BASE_URL`, so existing env
505
519
  // combinations keep their probe.
506
- if (env.OPENROUTER_API_KEY) return checkOpenRouter(env.OPENROUTER_API_KEY);
507
- if (env.ANTHROPIC_API_KEY) return checkAnthropicApiKey(env.ANTHROPIC_API_KEY);
508
- if (env.OPENAI_API_KEY) return checkOpenAiApiKey(env.OPENAI_API_KEY);
520
+ if (env.OPENROUTER_API_KEY) {
521
+ return checkEachKey("OPENROUTER_API_KEY", env.OPENROUTER_API_KEY, checkOpenRouter);
522
+ }
523
+ if (env.ANTHROPIC_API_KEY) {
524
+ return checkEachKey("ANTHROPIC_API_KEY", env.ANTHROPIC_API_KEY, checkAnthropicApiKey);
525
+ }
526
+ if (env.OPENAI_API_KEY) {
527
+ return checkEachKey("OPENAI_API_KEY", env.OPENAI_API_KEY, checkOpenAiApiKey);
528
+ }
509
529
  for (const direct of openAICompatibleProviders()) {
510
530
  const key = providerKeyFromEnv(direct, env);
511
531
  if (key) {
512
- return checkOpenAICompatibleProvider(direct, key, providerBaseUrl(direct, env));
532
+ return checkEachKey(direct.envVar, key, (member) =>
533
+ checkOpenAICompatibleProvider(direct, member, providerBaseUrl(direct, env)),
534
+ );
513
535
  }
514
536
  }
515
537
  // Model-specific keys (e.g. GEMINI_API_KEY) can satisfy readiness
@@ -534,28 +556,36 @@ export async function validateProviderCredentials(
534
556
  };
535
557
  }
536
558
  const baseUrl = env.DEVIN_API_BASE_URL ?? "https://api.devin.ai";
537
- const r = await timedFetch(`${baseUrl.replace(/\/+$/, "")}/v3/self`, {
538
- method: "GET",
539
- headers: {
540
- Authorization: `Bearer ${apiKey}`,
541
- "Content-Type": "application/json",
542
- },
559
+ return checkEachKey("DEVIN_API_KEY", apiKey, async (member) => {
560
+ const r = await timedFetch(`${baseUrl.replace(/\/+$/, "")}/v3/self`, {
561
+ method: "GET",
562
+ headers: {
563
+ Authorization: `Bearer ${member}`,
564
+ "Content-Type": "application/json",
565
+ },
566
+ });
567
+ if (r.ok) return { ok: true, latency_ms: r.latency_ms };
568
+ return {
569
+ ok: false,
570
+ error: scrubSecrets(`HTTP ${r.status}: ${r.bodyText.slice(0, 200)}`),
571
+ httpStatus: r.status,
572
+ latency_ms: r.latency_ms,
573
+ };
543
574
  });
544
- if (r.ok) return { ok: true, latency_ms: r.latency_ms };
575
+ }
576
+ case "dsh": {
577
+ // Same precedence as `checkDshCredentials`: the DeepSeek key, then OpenRouter.
578
+ const deepseekKey = env.DEEPSEEK_API_KEY?.trim();
579
+ if (deepseekKey) return checkEachKey("DEEPSEEK_API_KEY", deepseekKey, checkDeepSeekApiKey);
580
+ const openRouterKey = env.OPENROUTER_API_KEY?.trim();
581
+ if (openRouterKey)
582
+ return checkEachKey("OPENROUTER_API_KEY", openRouterKey, checkOpenRouter);
545
583
  return {
546
584
  ok: false,
547
- error: scrubSecrets(`HTTP ${r.status}: ${r.bodyText.slice(0, 200)}`),
548
- latency_ms: r.latency_ms,
585
+ error: "Set DEEPSEEK_API_KEY or OPENROUTER_API_KEY for dsh.",
586
+ latency_ms: Date.now() - startedAt,
549
587
  };
550
588
  }
551
- case "dsh":
552
- return checkDshCredentials(env).ready
553
- ? presenceCheckOk()
554
- : {
555
- ok: false,
556
- error: "Set DEEPSEEK_API_KEY or OPENROUTER_API_KEY for dsh.",
557
- latency_ms: Date.now() - startedAt,
558
- };
559
589
  case "amp": {
560
590
  if (!checkAmpCredentials(env).ready) {
561
591
  return {
@@ -565,14 +595,17 @@ export async function validateProviderCredentials(
565
595
  };
566
596
  }
567
597
  // `amp usage` reads the credit balance and runs no inference.
568
- const live = await liveTestAmpCredentials(env as Record<string, string | undefined>);
569
- return live.ok
570
- ? { ok: true, latency_ms: Date.now() - startedAt }
571
- : {
572
- ok: false,
573
- error: live.error ?? "amp usage failed",
574
- latency_ms: Date.now() - startedAt,
575
- };
598
+ return checkEachKey("AMP_API_KEY", env.AMP_API_KEY!, async (member) => {
599
+ const start = Date.now();
600
+ const live = await liveTestAmpCredentials({ ...env, AMP_API_KEY: member });
601
+ return live.ok
602
+ ? { ok: true, latency_ms: Date.now() - start }
603
+ : {
604
+ ok: false,
605
+ error: live.error ?? "amp usage failed",
606
+ latency_ms: Date.now() - start,
607
+ };
608
+ });
576
609
  }
577
610
  case "cursor": {
578
611
  const apiKey = env.CURSOR_API_KEY?.trim();
@@ -584,16 +617,21 @@ export async function validateProviderCredentials(
584
617
  };
585
618
  }
586
619
  const { liveTestCursorKey } = await import("../providers/cursor-adapter");
587
- const result = await liveTestCursorKey(apiKey);
588
- return result.ok
589
- ? { ok: true, latency_ms: Date.now() - startedAt }
590
- : { ok: false, error: result.error, latency_ms: Date.now() - startedAt };
620
+ return checkEachKey("CURSOR_API_KEY", apiKey, async (member) => {
621
+ const start = Date.now();
622
+ const result = await liveTestCursorKey(member);
623
+ return result.ok
624
+ ? { ok: true, latency_ms: Date.now() - start }
625
+ : { ok: false, error: result.error, latency_ms: Date.now() - start };
626
+ });
591
627
  }
592
628
  case "grok": {
593
629
  const apiKey = env.XAI_API_KEY?.trim();
594
- if (apiKey) return checkXaiApiKey(apiKey);
630
+ if (apiKey) return checkEachKey("XAI_API_KEY", apiKey, checkXaiApiKey);
595
631
  const openRouterKey = env.OPENROUTER_API_KEY?.trim();
596
- if (openRouterKey) return checkOpenRouter(openRouterKey);
632
+ if (openRouterKey) {
633
+ return checkEachKey("OPENROUTER_API_KEY", openRouterKey, checkOpenRouter);
634
+ }
597
635
  return {
598
636
  ok: false,
599
637
  error: "Set XAI_API_KEY or OPENROUTER_API_KEY for grok.",
@@ -653,6 +691,8 @@ export async function buildCredStatusReport(
653
691
  error: live.error ?? null,
654
692
  latency_ms: live.latency_ms,
655
693
  testedAt: Date.now(),
694
+ ...(live.warning !== undefined ? { warning: live.warning } : {}),
695
+ ...(live.keys !== undefined ? { keys: live.keys } : {}),
656
696
  };
657
697
  }
658
698
  // Include the Bedrock enumeration block when the pi probe ran in Bedrock SDK
@@ -1,5 +1,6 @@
1
1
  import { existsSync, statSync } from "node:fs";
2
2
  import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
3
+ import { deriveDefaultRoute } from "@desplega/model-routing";
3
4
  import type { TemplateResponse } from "../../templates/schema.ts";
4
5
  import {
5
6
  type Attributes,
@@ -118,7 +119,13 @@ import {
118
119
  prependContextPreamble,
119
120
  } from "./context-preamble.ts";
120
121
  import { reportCredentialOutcomeThenFinish } from "./credential-outcome-report.ts";
121
- import { type CredentialRefreshState, refreshCredentialStatus } from "./credential-refresh.ts";
122
+ import {
123
+ type CredentialRefreshState,
124
+ credentialFingerprint,
125
+ recheckableFailures,
126
+ refreshCredentialStatus,
127
+ reportKeyValidation,
128
+ } from "./credential-refresh.ts";
122
129
  import {
123
130
  awaitCredentials,
124
131
  BootMaxWaitExceededError,
@@ -831,6 +838,8 @@ export async function exitAfterTasksIfFinished(
831
838
  */
832
839
  export interface ResolvedEnvResult {
833
840
  env: Record<string, string | undefined>;
841
+ /** Full credential pools for live validation. Keep this environment in process only. */
842
+ credentialEnv: Record<string, string | undefined>;
834
843
  credentialSelections: CredentialSelection[];
835
844
  /** Raw config-row value, kept separate from the merged environment so an
836
845
  * actual worker environment variable retains precedence. */
@@ -1049,6 +1058,7 @@ export async function fetchResolvedEnv(
1049
1058
  });
1050
1059
  const effectiveModel = modelSelection.model || (env.MODEL_OVERRIDE as string | undefined) || "";
1051
1060
 
1061
+ const credentialEnv = { ...env };
1052
1062
  const credentialSelections = await resolveCredentialPools(env, {
1053
1063
  apiUrl,
1054
1064
  apiKey,
@@ -1065,7 +1075,7 @@ export async function fetchResolvedEnv(
1065
1075
  enforceModelCapacity: sessionContext?.enforceModelCapacity,
1066
1076
  });
1067
1077
 
1068
- return { env, credentialSelections, resolvedProvider, scriptsOnlyConfigValue };
1078
+ return { env, credentialEnv, credentialSelections, resolvedProvider, scriptsOnlyConfigValue };
1069
1079
  }
1070
1080
 
1071
1081
  /**
@@ -4319,7 +4329,9 @@ async function spawnProviderProcess(
4319
4329
  // config-store slot — gates whether `codexSlot` is set below (see
4320
4330
  // `CodexOAuthCredentialInfo`).
4321
4331
  let oauthIsPoolBacked = false;
4322
- if (adapter.name === "codex") {
4332
+ // A gateway route uses no OAuth: no pool slot is picked, so no auth.json is
4333
+ // materialised for a session whose traffic goes to the gateway.
4334
+ if (adapter.name === "codex" && !deriveDefaultRoute("codex", freshEnv)) {
4323
4335
  const oauthInfo = await resolveCodexOAuthCredentialInfo(opts.apiUrl, opts.apiKey);
4324
4336
  oauthSelection = oauthInfo?.selection;
4325
4337
  oauthIsPoolBacked = oauthInfo?.isPoolBacked ?? false;
@@ -6019,7 +6031,10 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
6019
6031
  ready: null,
6020
6032
  lastRefreshAt: 0,
6021
6033
  inFlight: false,
6034
+ fingerprint: null,
6035
+ rejectedKeys: 0,
6022
6036
  };
6037
+ let latestCredentialEnv: Record<string, string | undefined> | undefined;
6023
6038
 
6024
6039
  // Throttle for live HARNESS_PROVIDER reconciliation. Each reconciliation
6025
6040
  // calls `fetchResolvedEnv` which also re-resolves credential pools — we
@@ -6413,6 +6428,10 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
6413
6428
  credentialRefreshState.harnessProvider = state.harnessProvider;
6414
6429
  credentialRefreshState.ready = bootCredSnapshot?.ready ?? true;
6415
6430
  credentialRefreshState.lastRefreshAt = Date.now();
6431
+ credentialRefreshState.fingerprint = credentialFingerprint(process.env);
6432
+ credentialRefreshState.rejectedKeys = recheckableFailures(bootCredSnapshot?.liveTest);
6433
+ const keys = bootCredSnapshot?.liveTest?.keys;
6434
+ if (keys?.length) await reportKeyValidation(apiConfig, keys);
6416
6435
  } catch (error) {
6417
6436
  console.error(`[${role}] Failed to report credential readiness after recovery: ${error}`);
6418
6437
  process.exit(1);
@@ -7015,9 +7034,11 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
7015
7034
  try {
7016
7035
  const {
7017
7036
  env: freshEnv,
7037
+ credentialEnv,
7018
7038
  resolvedProvider,
7019
7039
  scriptsOnlyConfigValue,
7020
7040
  } = await fetchResolvedEnv(apiUrl, apiKey, agentId);
7041
+ latestCredentialEnv = credentialEnv;
7021
7042
  const nextScriptsOnly = resolveScriptsOnlyMode({
7022
7043
  env: process.env.SCRIPTS_ONLY_MCP,
7023
7044
  configValue: scriptsOnlyConfigValue,
@@ -7049,7 +7070,7 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
7049
7070
  apiConfig,
7050
7071
  credentialRefreshState,
7051
7072
  state.harnessProvider,
7052
- process.env,
7073
+ latestCredentialEnv ?? process.env,
7053
7074
  ).catch((err) =>
7054
7075
  console.warn(`[${role}] cred_status post_task refresh failed (non-fatal): ${err}`),
7055
7076
  );
@@ -3,7 +3,12 @@ import type { RequestInfo } from "../tools/utils";
3
3
  import { type CreateTaskOptions, CreateTaskOptionsSchema } from "../types";
4
4
  import { scrubSecrets } from "../utils/secret-scrubber";
5
5
  import { workflowEventBus } from "../workflows/event-bus";
6
- import type { TaskBlockedEvent, TaskCreateModify, TaskCreateOrigin } from "./contract";
6
+ import type {
7
+ TaskBlockedEvent,
8
+ TaskCreateEvent,
9
+ TaskCreateModify,
10
+ TaskCreateOrigin,
11
+ } from "./contract";
7
12
  import { dispatchPre, extensionIdForAgent, listRegistered } from "./dispatcher";
8
13
 
9
14
  const MODIFIABLE_OPTION_KEYS = [
@@ -50,6 +55,7 @@ type ApplyPreTaskCreateArgs = {
50
55
  options: CreateTaskOptions;
51
56
  origin: TaskCreateOrigin;
52
57
  requestInfo?: RequestInfo;
58
+ slack?: TaskCreateEvent["slack"];
53
59
  /** The caller's escape hatch for an uncatalogued `model`; the harness check ignores it. */
54
60
  allowCustomModel?: boolean;
55
61
  };
@@ -141,6 +147,7 @@ export async function applyPreTaskCreate(
141
147
  options: args.options,
142
148
  origin,
143
149
  requestInfo: args.requestInfo,
150
+ ...(args.slack ? { slack: args.slack } : {}),
144
151
  },
145
152
  {
146
153
  skipExtensionId: creatingExtensionId,