@desplega.ai/agent-swarm 2.5.3 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (349) hide show
  1. package/README.md +2 -2
  2. package/dist/{acp-adapter-gr2fcyyj.js → acp-adapter-dgfakfpw.js} +9 -9
  3. package/dist/{actions-wv766ghf.js → actions-fbqma8aa.js} +21 -21
  4. package/dist/{amp-adapter-speb906t.js → amp-adapter-en0jy6hs.js} +4 -4
  5. package/dist/{app-ag29n14f.js → app-k8a5cgpp.js} +3 -3
  6. package/dist/{assistant-3x8bz3eg.js → assistant-a16433gr.js} +30 -26
  7. package/dist/{boot-reembed-jsmfrpzf.js → boot-reembed-6c1sjfnr.js} +1 -1
  8. package/dist/{boot-reembed-5b3d2eea.js → boot-reembed-f4nn561n.js} +1 -1
  9. package/dist/{boot-scrub-sweep-s73gerxy.js → boot-scrub-sweep-d4w9gajf.js} +4 -4
  10. package/dist/{boot-seeds-5zmax2wr.js → boot-seeds-9bmkspyw.js} +4 -4
  11. package/dist/{claude-adapter-hswdnm3t.js → claude-adapter-nys1kjxa.js} +9 -9
  12. package/dist/{claude-managed-adapter-cvbzj1h6.js → claude-managed-adapter-55207973.js} +3 -3
  13. package/dist/{claude-managed-setup-3k8m3hhc.js → claude-managed-setup-3cm35gey.js} +11 -4
  14. package/dist/{claude-sdk-session-fkbyk66x.js → claude-sdk-session-nw5yd8cp.js} +9313 -9260
  15. package/dist/{cli-210xd1f4.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-25j01dpj.js → cli-24j1jeeb.js} +3 -3
  19. package/dist/{cli-ta8bd7th.js → cli-2sjhpsns.js} +2 -2
  20. package/dist/{cli-p859hcc8.js → cli-39aet655.js} +2 -2
  21. package/dist/{cli-g1cw7xtt.js → cli-39rrw8yp.js} +1 -1
  22. package/dist/{cli-e2ar4bp1.js → cli-3aqd4jaa.js} +121 -47
  23. package/dist/{cli-jjznk3f7.js → cli-3fwqy71j.js} +69 -35
  24. package/dist/{cli-p9rafdrb.js → cli-3gfvmzht.js} +7 -7
  25. package/dist/{cli-03vpq45j.js → cli-3ndmpxxv.js} +2 -2
  26. package/dist/{cli-aecdcr77.js → cli-4hxrf0p4.js} +228 -74
  27. package/dist/{cli-mz9afxxf.js → cli-4rf0f5vg.js} +15 -3
  28. package/dist/{cli-qzme5949.js → cli-5rj83bam.js} +284 -202
  29. package/dist/{cli-fsdprwrg.js → cli-5sckpqcx.js} +2 -2
  30. package/dist/{cli-cz5nmdxt.js → cli-683y0b3a.js} +2 -2
  31. package/dist/{cli-3ngdk5te.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-avdvgcay.js → cli-9abdgn5t.js} +28 -9
  37. package/dist/{cli-99xgw26r.js → cli-a2rr0fmg.js} +1 -1
  38. package/dist/{cli-e5bxnjps.js → cli-a34pr3w1.js} +7 -7
  39. package/dist/{cli-502eqv93.js → cli-abkm71vr.js} +1 -1
  40. package/dist/{cli-mmhj8vd7.js → cli-ag2yrnt5.js} +11 -2
  41. package/dist/{cli-n45mr7zh.js → cli-aj8djyqt.js} +5 -5
  42. package/dist/{cli-1fc85mvt.js → cli-art7c1zh.js} +5 -5
  43. package/dist/{cli-40jsnrjm.js → cli-az25fn5f.js} +10 -8
  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-svrj71pg.js → cli-c2habvsf.js} +12 -12
  47. package/dist/{cli-s49hpn37.js → cli-ca6banwz.js} +85 -19
  48. package/dist/{cli-hhxjnjzs.js → cli-cz37jqss.js} +12 -10
  49. package/dist/{cli-6n8axjv1.js → cli-d2dpf95n.js} +2 -2
  50. package/dist/{cli-tx00c0h6.js → cli-dqygeeh5.js} +17 -1
  51. package/dist/{cli-z21whrrg.js → cli-emhjcj2y.js} +17 -17
  52. package/dist/{cli-e8jyq7d8.js → cli-esakngh8.js} +7 -3
  53. package/dist/{cli-ha9rmwgq.js → cli-fb8m699c.js} +2 -2
  54. package/dist/{cli-35gtk1p6.js → cli-ffvsj2qw.js} +44 -13
  55. package/dist/{cli-ce0ak9ms.js → cli-g20jjwt4.js} +26 -26
  56. package/dist/{cli-kxawjrxg.js → cli-g8ka5j4j.js} +772 -194
  57. package/dist/{cli-vwn81jbs.js → cli-gf95q3bm.js} +5 -5
  58. package/dist/{cli-s53vp709.js → cli-gnsb1zxa.js} +2 -2
  59. package/dist/{cli-nwyw3zhg.js → cli-gxee0bqa.js} +1 -1
  60. package/dist/{cli-gpv60278.js → cli-hfeb68z4.js} +7 -7
  61. package/dist/{cli-0ebey9hg.js → cli-j6rktc91.js} +1 -1
  62. package/dist/{cli-ewm3pwbq.js → cli-k21gx5ht.js} +46 -23
  63. package/dist/{cli-egjggehm.js → cli-kf4jree9.js} +2 -2
  64. package/dist/{cli-dabehxhp.js → cli-kt907da6.js} +1 -1
  65. package/dist/{cli-p0rzjkya.js → cli-m5a9m0ak.js} +32 -13
  66. package/dist/{cli-fzcn3d87.js → cli-m70bajzr.js} +3 -3
  67. package/dist/{cli-w1zwctj2.js → cli-mtnj4fwa.js} +11 -5
  68. package/dist/{cli-gpsz7g88.js → cli-mvjz5an3.js} +16 -12
  69. package/dist/{cli-hcpj2xqz.js → cli-n7asmce6.js} +225 -3
  70. package/dist/{cli-y561h5a9.js → cli-nn6vx2mq.js} +5 -5
  71. package/dist/cli-nq677csy.js +140 -0
  72. package/dist/{cli-a5ebc95n.js → cli-pfk92eqz.js} +18 -15
  73. package/dist/{cli-p6zs9h4y.js → cli-rr7zx19j.js} +767 -249
  74. package/dist/{cli-qgf8emrn.js → cli-sqma6vw1.js} +10 -7
  75. package/dist/{cli-0xk505e8.js → cli-sxgtf5fw.js} +1 -1
  76. package/dist/{cli-nzfnt2gs.js → cli-tf1n0r0b.js} +1 -1
  77. package/dist/{cli-qqzmjxkf.js → cli-v49fkghp.js} +11 -6
  78. package/dist/{cli-x77gen2p.js → cli-w2656ka1.js} +7 -7
  79. package/dist/{cli-5kqnemen.js → cli-yvy48785.js} +1 -1
  80. package/dist/{cli-8s5wx3nz.js → cli-zqne1qcj.js} +44 -17
  81. package/dist/cli.js +22 -22
  82. package/dist/{codex-adapter-n8ka66vf.js → codex-adapter-d95q4qsn.js} +7 -7
  83. package/dist/{codex-hook-e2he7aca.js → codex-hook-brpnycn5.js} +1 -1
  84. package/dist/{codex-login-rn7c5gxt.js → codex-login-8cg0vhf2.js} +1 -1
  85. package/dist/{codex-session-runner-fsebts37.js → codex-session-runner-gdwd8h0c.js} +8 -8
  86. package/dist/{commands-t04rt2pc.js → commands-jqmgk54b.js} +2 -2
  87. package/dist/{controller-rgna04r2.js → controller-z3jq589f.js} +30 -4
  88. package/dist/{controllers-connect-bqm1rtft.js → controllers-connect-r2za39ef.js} +23 -23
  89. package/dist/{cursor-adapter-swr2k95y.js → cursor-adapter-4mr4jgkw.js} +2 -3
  90. package/dist/{db-a4m7haw0.js → db-ppv2tze7.js} +3 -3
  91. package/dist/{db-24ck09xy.js → db-w36wwb5g.js} +4 -2
  92. package/dist/{devin-adapter-4ycy4vrf.js → devin-adapter-byen2cxt.js} +1 -1
  93. package/dist/{dispatcher-nwf9ss0q.js → dispatcher-pv5h77vf.js} +20 -20
  94. package/dist/{docker-nfy2gc8t.js → docker-fdzyewpz.js} +4 -4
  95. package/dist/{dsh-adapter-kpdnhjst.js → dsh-adapter-d4pdpb3r.js} +2 -3
  96. package/dist/{e2b-9gkdhm3c.js → e2b-222hsajc.js} +2 -2
  97. package/dist/{e2b-cjtf8k1z.js → e2b-fn3zacvt.js} +2 -2
  98. package/dist/{easter-egg-3d-e4qv8dxj.js → easter-egg-3d-4sp4dvmc.js} +1 -1
  99. package/dist/{event-bus-3z78ep5n.js → event-bus-pghg8hqy.js} +1 -1
  100. package/dist/{execute-0xpz8950.js → execute-jev839y1.js} +1 -1
  101. package/dist/{external-channel-j1e2p34p.js → external-channel-y6nkzhnn.js} +2 -2
  102. package/dist/{grok-adapter-qtjz0whk.js → grok-adapter-t08b9ntm.js} +10 -10
  103. package/dist/handlers-eqsnmv95.js +41 -0
  104. package/dist/{heartbeat-vtesda5d.js → heartbeat-vn31xwdk.js} +23 -23
  105. package/dist/{hook-b2hhj6rd.js → hook-6d0zbvvt.js} +6 -5
  106. package/dist/{hook-8wqaszda.js → hook-cpjs8q51.js} +13 -7
  107. package/dist/{http-mxcqhhcs.js → http-9c6medda.js} +1085 -136
  108. package/dist/{index-spwxjgay.js → index-38t35h0n.js} +20 -20
  109. package/dist/{index-1r8wfpzs.js → index-g6k9tctj.js} +1 -1
  110. package/dist/{index-srg522m6.js → index-k0yt1tvq.js} +20 -20
  111. package/dist/{index-31ceyf42.js → index-pkma1yf8.js} +1 -1
  112. package/dist/{index-n1vbvta9.js → index-qhjf2htm.js} +29 -24
  113. package/dist/index-yqgbfgaj.js +43 -0
  114. package/dist/{keepalive-dsd8rtgx.js → keepalive-em0117bp.js} +4 -4
  115. package/dist/lead-23e7z59t.js +79 -0
  116. package/dist/{maintenance-jqmqwnet.js → maintenance-kbme3qy9.js} +1 -1
  117. package/dist/{oauth-refresh-sweep-q2c3bnvm.js → oauth-refresh-sweep-xysj4yse.js} +3 -3
  118. package/dist/{onboard-w4yd7s8t.js → onboard-p6q0vz97.js} +3 -3
  119. package/dist/{opencode-adapter-9tppvxar.js → opencode-adapter-esv3k9yn.js} +7 -7
  120. package/dist/{otel-impl-f36byzbd.js → otel-impl-54qhqh1z.js} +2 -2
  121. package/dist/{pi-mono-adapter-tqsge1ay.js → pi-mono-adapter-mymbwhj0.js} +41 -14
  122. package/dist/{pricing-refresh-xvak5p4v.js → pricing-refresh-yb75wv33.js} +3 -3
  123. package/dist/{rbac-roles-gfn30fb8.js → rbac-roles-1vp2mw4p.js} +2 -2
  124. package/dist/{rbac-roles-682k2mt5.js → rbac-roles-dexhc9x8.js} +1 -1
  125. package/dist/render-v2-tjay5hhf.js +16 -0
  126. package/dist/{resume-2dvq348g.js → resume-8ayf37g5.js} +11 -9
  127. package/dist/{runtime-bt5pkpm1.js → runtime-hqe5cqcd.js} +1 -1
  128. package/dist/{session-access-7f21ekq9.js → session-access-fdhp987s.js} +4 -4
  129. package/dist/session-source-backfill-qgtc86z1.js +110 -0
  130. package/dist/{setup-epffxtbk.js → setup-4rrmqcq3.js} +1 -1
  131. package/dist/{virtual-modules-mnfbh5mr.js → virtual-modules-52km00d6.js} +2 -2
  132. package/dist/{work-objects-xz1a5rqk.js → work-objects-p28fgd70.js} +23 -23
  133. package/dist/worker-e3w7vye1.js +79 -0
  134. package/dist/{x-pbpezr0s.js → x-wxrzvzx8.js} +2 -2
  135. package/openapi.json +1323 -16
  136. package/package.json +5 -5
  137. package/plugin/commands/create-pr.md +2 -2
  138. package/plugin/pi-skills/create-pr/SKILL.md +2 -2
  139. package/src/assets/source-key.ts +95 -0
  140. package/src/be/db/api-keys.ts +80 -0
  141. package/src/be/db/approvals.ts +48 -0
  142. package/src/be/db/session-labels.ts +28 -0
  143. package/src/be/db/session-moves.ts +5 -6
  144. package/src/be/db/session-tree.ts +153 -14
  145. package/src/be/db/tasks/read.ts +11 -0
  146. package/src/be/db/tasks/write.ts +21 -6
  147. package/src/be/db/workflow-idempotency.ts +183 -0
  148. package/src/be/db/workflow-runs.ts +500 -7
  149. package/src/be/db-retention.ts +61 -6
  150. package/src/be/db.ts +126 -131
  151. package/src/be/memory/providers/openai-embedding.ts +5 -1
  152. package/src/be/migrations/242_workflow_idempotency_keys.sql +40 -0
  153. package/src/be/migrations/244_api_key_validation.sql +4 -0
  154. package/src/be/onboarding.ts +4 -0
  155. package/src/be/scripts/typecheck.ts +1 -1
  156. package/src/be/sealed-json.ts +1 -0
  157. package/src/be/seed-scripts/catalog/compound-insights.inline.ts +2 -2
  158. package/src/be/seed-scripts/catalog/compound-insights.ts +2 -2
  159. package/src/be/seed-skills/bundled-files.generated.json +1 -1
  160. package/src/be/session-source-backfill.ts +163 -0
  161. package/src/be/sql-connections.ts +7 -1
  162. package/src/be/swarm-config-guard.ts +3 -0
  163. package/src/be/users.ts +9 -0
  164. package/src/comb/review-batch.ts +28 -1
  165. package/src/comb/templates.ts +7 -2
  166. package/src/commands/claude-managed-setup.ts +7 -1
  167. package/src/commands/controller.ts +33 -0
  168. package/src/commands/credential-refresh.ts +76 -1
  169. package/src/commands/credential-wait.ts +2 -2
  170. package/src/commands/provider-credentials.ts +210 -170
  171. package/src/commands/runner.ts +31 -6
  172. package/src/extensions/apply-task-create.ts +64 -11
  173. package/src/extensions/contract-types.generated.ts +1 -1
  174. package/src/extensions/contract.ts +30 -0
  175. package/src/extensions/dispatcher.ts +4 -0
  176. package/src/extensions/post-bridge.ts +8 -0
  177. package/src/hooks/context-bound-note.ts +196 -0
  178. package/src/hooks/hook.ts +12 -0
  179. package/src/http/all-routes.ts +2 -0
  180. package/src/http/api-keys.ts +53 -0
  181. package/src/http/assets.ts +15 -0
  182. package/src/http/codex-oauth-accounts.ts +191 -0
  183. package/src/http/comb.ts +15 -2
  184. package/src/http/config.ts +49 -17
  185. package/src/http/core.ts +6 -0
  186. package/src/http/fleet-benchmark.ts +5 -2
  187. package/src/http/idempotency-key.ts +117 -0
  188. package/src/http/index.ts +11 -0
  189. package/src/http/integrations.ts +5 -1
  190. package/src/http/kv.ts +8 -3
  191. package/src/http/onboarding.ts +6 -1
  192. package/src/http/pool-keys.ts +354 -0
  193. package/src/http/sessions.ts +37 -12
  194. package/src/http/stats.ts +1 -0
  195. package/src/http/status.ts +26 -12
  196. package/src/http/tasks.ts +3 -0
  197. package/src/http/used-by.ts +7 -2
  198. package/src/http/workflows.ts +207 -33
  199. package/src/integrations/chat-sdk/inbound.ts +27 -0
  200. package/src/kv-reserved-namespaces.ts +19 -0
  201. package/src/prompts/context-bound-note.ts +48 -0
  202. package/src/prompts/session-templates.ts +25 -1
  203. package/src/providers/acp-targets.ts +1 -1
  204. package/src/providers/amp-adapter.ts +30 -8
  205. package/src/providers/claude-adapter.ts +34 -5
  206. package/src/providers/claude-managed-adapter.ts +42 -9
  207. package/src/providers/claude-sdk-session.ts +10 -0
  208. package/src/providers/claude-session-events.ts +13 -0
  209. package/src/providers/codex-adapter.ts +83 -11
  210. package/src/providers/opencode-adapter.ts +18 -0
  211. package/src/providers/pi-mono-adapter.ts +35 -0
  212. package/src/runtime-providers/docker.ts +6 -2
  213. package/src/scheduler/scheduler.ts +3 -2
  214. package/src/scripts-runtime/swarm-sdk.ts +7 -1
  215. package/src/scripts-runtime/types/stdlib.d.ts +1 -1
  216. package/src/scripts-runtime/types/swarm-extension.d.ts +52 -8
  217. package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -1
  218. package/src/slack/assistant.ts +7 -0
  219. package/src/slack/channel-labels.ts +74 -0
  220. package/src/slack/handlers.ts +8 -0
  221. package/src/slack/inbound-files.ts +5 -2
  222. package/src/slack/thread-buffer.ts +4 -0
  223. package/src/tasks/sibling-awareness.ts +12 -3
  224. package/src/telemetry-context.ts +5 -6
  225. package/src/telemetry-identity.ts +3 -3
  226. package/src/tests/amp-adapter.test.ts +97 -0
  227. package/src/tests/api-keys-validation.test.ts +170 -0
  228. package/src/tests/approval-requests.test.ts +10 -7
  229. package/src/tests/approval-sweeps.test.ts +12 -6
  230. package/src/tests/base-prompt.test.ts +1 -1
  231. package/src/tests/claude-hook-output.test.ts +79 -1
  232. package/src/tests/claude-managed-adapter.test.ts +321 -0
  233. package/src/tests/claude-routes-e2e.test.ts +7 -2
  234. package/src/tests/codex-adapter.test.ts +163 -0
  235. package/src/tests/codex-credential-pools.test.ts +120 -0
  236. package/src/tests/codex-gateway-route.test.ts +165 -0
  237. package/src/tests/codex-oauth-accounts.test.ts +357 -0
  238. package/src/tests/comb-review-batch.test.ts +63 -2
  239. package/src/tests/config-pool-keys.test.ts +533 -0
  240. package/src/tests/context-bound-note.test.ts +269 -0
  241. package/src/tests/credential-check.test.ts +290 -2
  242. package/src/tests/credential-refresh.test.ts +172 -1
  243. package/src/tests/db-retention.test.ts +9 -3
  244. package/src/tests/db-task-read-contract.test.ts +1 -0
  245. package/src/tests/db-task-write-contract.test.ts +1 -3
  246. package/src/tests/docs-index-footprint.test.js +3 -3
  247. package/src/tests/extensions-pre-task.test.ts +325 -0
  248. package/src/tests/fetch-resolved-env.test.ts +17 -0
  249. package/src/tests/fixtures/extensions/record-blocks/hooks.ts +10 -0
  250. package/src/tests/fixtures/extensions/record-blocks/manifest.json +7 -0
  251. package/src/tests/fixtures/extensions/record-task-create/hooks.ts +9 -0
  252. package/src/tests/fixtures/extensions/record-task-create/manifest.json +7 -0
  253. package/src/tests/fixtures/fake-amp.ts +22 -8
  254. package/src/tests/fixtures/held-template-render.ts +42 -0
  255. package/src/tests/fixtures/workflow-idempotency-trigger.child.ts +73 -0
  256. package/src/tests/fleet-benchmark.test.ts +8 -0
  257. package/src/tests/get-workflow-run-tool.test.ts +98 -0
  258. package/src/tests/integrations-http.test.ts +32 -2
  259. package/src/tests/internal-ai/complete-structured.test.ts +37 -1
  260. package/src/tests/internal-ai/credentials.test.ts +23 -0
  261. package/src/tests/kv-http.test.ts +32 -0
  262. package/src/tests/kv-tool.test.ts +25 -0
  263. package/src/tests/multi-runtime-registration.test.ts +12 -12
  264. package/src/tests/onboarding.test.ts +108 -1
  265. package/src/tests/openai-embedding-credentials.test.ts +35 -0
  266. package/src/tests/openapi-route-headers.test.ts +2 -0
  267. package/src/tests/opencode-adapter.test.ts +134 -0
  268. package/src/tests/openrouter-attribution-call-sites.test.ts +2 -2
  269. package/src/tests/org-website.test.ts +36 -0
  270. package/src/tests/pi-mono-adapter.test.ts +105 -0
  271. package/src/tests/prompt-template-session.test.ts +4 -3
  272. package/src/tests/provider-key-probes.test.ts +181 -0
  273. package/src/tests/rbac-e2e-helpers.test.ts +88 -1
  274. package/src/tests/rbac-e2e-helpers.ts +36 -11
  275. package/src/tests/runner-push-delivery.test.ts +25 -0
  276. package/src/tests/runtime-controller-liveness.test.ts +133 -0
  277. package/src/tests/runtime-providers-docker.test.ts +2 -0
  278. package/src/tests/script-connections.test.ts +5 -0
  279. package/src/tests/script-run-config-scope.test.ts +91 -3
  280. package/src/tests/script-runs-retention.test.ts +212 -0
  281. package/src/tests/sdk-allowlist.test.ts +39 -0
  282. package/src/tests/session-logs.test.ts +70 -0
  283. package/src/tests/session-source-backfill.test.ts +429 -0
  284. package/src/tests/session-source-keys.test.ts +236 -0
  285. package/src/tests/session-source-labels.test.ts +419 -0
  286. package/src/tests/sessions-detail-batch.test.ts +2 -0
  287. package/src/tests/sessions-move.test.ts +131 -0
  288. package/src/tests/sessions-tree.test.ts +336 -2
  289. package/src/tests/slack-inbound-files.test.ts +103 -23
  290. package/src/tests/status.test.ts +101 -18
  291. package/src/tests/tasks-workflow-run-filter.test.ts +146 -0
  292. package/src/tests/test-net.ts +31 -0
  293. package/src/tests/transcription.test.ts +8 -0
  294. package/src/tests/used-by-route.test.ts +11 -0
  295. package/src/tests/workflow-cascade-cancel.test.ts +1341 -0
  296. package/src/tests/workflow-foreach-supersede-retry.test.ts +488 -0
  297. package/src/tests/workflow-http-v2.test.ts +131 -0
  298. package/src/tests/workflow-idempotency.test.ts +915 -0
  299. package/src/tests/workflow-step-order.test.ts +92 -0
  300. package/src/tests/workflow-sub-workflow.test.ts +30 -0
  301. package/src/tools/get-tasks.ts +8 -0
  302. package/src/tools/kv/kv-read-auth.ts +4 -2
  303. package/src/tools/task-action.ts +1 -1
  304. package/src/tools/workflows/cancel-workflow-run.ts +1 -1
  305. package/src/tools/workflows/get-workflow-run.ts +14 -4
  306. package/src/types.ts +24 -0
  307. package/src/utils/company-profile.ts +51 -1
  308. package/src/utils/context-window.ts +8 -6
  309. package/src/utils/credentials.ts +5 -2
  310. package/src/utils/internal-ai/complete-structured.ts +5 -0
  311. package/src/utils/internal-ai/credentials.ts +4 -3
  312. package/src/utils/provider-key-probes.ts +328 -0
  313. package/src/utils/transcription.ts +4 -1
  314. package/src/workflows/approval-cancel.ts +15 -13
  315. package/src/workflows/approval-notifications.ts +5 -1
  316. package/src/workflows/checkpoint.ts +6 -3
  317. package/src/workflows/engine.ts +30 -8
  318. package/src/workflows/executors/foreach.ts +2 -2
  319. package/src/workflows/executors/sub-workflow.ts +8 -1
  320. package/src/workflows/foreach-driver.ts +10 -3
  321. package/src/workflows/foreach-join.ts +29 -6
  322. package/src/workflows/recovery.ts +6 -1
  323. package/src/workflows/resume.ts +168 -56
  324. package/src/workflows/retry-poller.ts +49 -3
  325. package/src/workflows/task-step-routing.ts +29 -7
  326. package/templates/ai-toolbox.manifest.json +8 -8
  327. package/templates/community/open-prompt-showreel/PLAYBOOK.md +5 -6
  328. package/templates/community/open-prompt-showreel/lead-prompt.md +1 -1
  329. package/templates/community/open-prompt-showreel/skills/open-prompt-showreel/SKILL.md +4 -4
  330. package/templates/skills/agent-browser/SKILL.md +4 -4
  331. package/templates/skills/agent-browser/content.md +4 -4
  332. package/templates/skills/code-quality/config.json +1 -1
  333. package/templates/skills/code-quality/content.md +2 -2
  334. package/templates/skills/delegate-work/content.md +29 -25
  335. package/templates/skills/delegate-work/files/scripts/codex-exec.sh +5 -5
  336. package/templates/skills/qa/content.md +2 -2
  337. package/templates/skills/researching/content.md +4 -0
  338. package/templates/skills/swarm-scripts/SKILL.md +1 -0
  339. package/templates/skills/swarm-scripts/content.md +1 -0
  340. package/templates/skills/work-on-task/SKILL.md +1 -1
  341. package/templates/skills/work-on-task/content.md +1 -1
  342. package/dist/cli-7421jzsw.js +0 -84
  343. package/dist/cli-e45e4m6b.js +0 -413
  344. package/dist/cli-zw88zjba.js +0 -307
  345. package/dist/handlers-19xr6dnz.js +0 -41
  346. package/dist/index-kw2mmat7.js +0 -43
  347. package/dist/lead-zyerk1x2.js +0 -79
  348. package/dist/render-v2-hgytz8nn.js +0 -12
  349. package/dist/worker-42t8emjt.js +0 -79
@@ -10,6 +10,7 @@ import {
10
10
  recordKeyRateLimitWindows,
11
11
  recordKeySeatMismatch,
12
12
  recordKeyUsage,
13
+ recordKeyValidation,
13
14
  setApiKeyName,
14
15
  setApiKeyPlan,
15
16
  } from "../be/db";
@@ -79,6 +80,39 @@ const reportRateLimit = route({
79
80
  auth: { apiKey: true },
80
81
  });
81
82
 
83
+ const reportValidation = route({
84
+ method: "post",
85
+ path: "/api/keys/report-validation",
86
+ pattern: ["api", "keys", "report-validation"],
87
+ summary: "Record live validation results for a credential pool",
88
+ tags: ["API Keys"],
89
+ body: z.object({
90
+ keyType: z.string(),
91
+ keys: z
92
+ .array(
93
+ z.object({
94
+ keyIndex: z.number().int().min(0),
95
+ keySuffix: z.string().min(1).max(10),
96
+ ok: z.boolean(),
97
+ httpStatus: z.number().int().optional(),
98
+ }),
99
+ )
100
+ .min(1)
101
+ .max(100),
102
+ scope: z.string().optional(),
103
+ scopeId: z.string().optional(),
104
+ }),
105
+ responses: {
106
+ 200: { description: "Validation recorded", schema: successMessageSchema },
107
+ 400: { description: "Validation error" },
108
+ 401: { description: "Unauthorized" },
109
+ },
110
+ auth: { apiKey: true },
111
+ rbac: {
112
+ ungated: "worker credential telemetry, same posture as POST /api/keys/report-rate-limit",
113
+ },
114
+ });
115
+
82
116
  const reportAuthFailure = route({
83
117
  method: "post",
84
118
  path: "/api/keys/report-auth-failure",
@@ -223,6 +257,7 @@ const getAvailable = route({
223
257
  schema: z.object({
224
258
  success: z.literal(true),
225
259
  availableIndices: z.array(z.number().int()),
260
+ rejectedIndices: z.array(z.number().int()).optional(),
226
261
  totalKeys: z.number().int(),
227
262
  /** Indices excluded only by an active model-scoped window block. Present only when `model` was passed. */
228
263
  modelBlockedIndices: z.array(z.number().int()).optional(),
@@ -273,6 +308,8 @@ const ApiKeyStatusSchema = z.object({
273
308
  /** Auth failures in a row since the last success or clear. */
274
309
  consecutiveAuthFailures: z.number().int(),
275
310
  lastAuthFailureAt: z.string().nullable(),
311
+ lastValidationError: z.string().nullable(),
312
+ lastValidationFailedAt: z.string().nullable(),
276
313
  /** Derived, readable view of any rejected model-scoped window (Fable/Opus/Sonnet) on this key. */
277
314
  modelLimits: z.array(
278
315
  z.object({
@@ -555,6 +592,21 @@ export async function handleApiKeys(
555
592
  return true;
556
593
  }
557
594
 
595
+ // POST /api/keys/report-validation
596
+ if (reportValidation.match(req.method, pathSegments)) {
597
+ const parsed = await reportValidation.parse(req, res, pathSegments, queryParams);
598
+ if (!parsed) return true;
599
+
600
+ const { keyType, keys, scope, scopeId } = parsed.body;
601
+ try {
602
+ await recordKeyValidation(keyType, keys, scope, scopeId ?? null);
603
+ reportValidation.respond(res, 200, { success: true, message: "Key validation recorded" });
604
+ } catch (err) {
605
+ jsonError(res, err instanceof Error ? err.message : "Failed to record key validation", 500);
606
+ }
607
+ return true;
608
+ }
609
+
558
610
  // POST /api/keys/report-auth-failure
559
611
  if (reportAuthFailure.match(req.method, pathSegments)) {
560
612
  const parsed = await reportAuthFailure.parse(req, res, pathSegments, queryParams);
@@ -650,6 +702,7 @@ export async function handleApiKeys(
650
702
  getAvailable.respond(res, 200, {
651
703
  success: true,
652
704
  availableIndices: result.availableIndices,
705
+ rejectedIndices: result.rejectedIndices,
653
706
  totalKeys,
654
707
  authFailureFence: result.authFailureFence,
655
708
  ...(model !== undefined
@@ -1,6 +1,8 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import { z } from "zod";
3
3
  import { getApp } from "../apps/store";
4
+ import { normalizeAssetKey } from "../assets/key";
5
+ import { sourceMoveError, sourceOfKey } from "../assets/source-key";
4
6
  import { auditAssetKeys } from "../be/asset-key-audit";
5
7
  import { AssetKeyAuthorizationError, authorizeAssetKeyWrite } from "../be/asset-key-auth";
6
8
  import { resolveHttpAuditUserId } from "../be/audit-user";
@@ -15,6 +17,7 @@ import {
15
17
  upsertAssetKeyMapping,
16
18
  } from "../be/db";
17
19
  import { getScriptById } from "../be/scripts/db";
20
+ import { getSessionRoot } from "../be/session-shares";
18
21
  import { can, type RbacResource } from "../rbac";
19
22
  import {
20
23
  type AssetEntityType,
@@ -334,6 +337,18 @@ export async function handleAssets(
334
337
  );
335
338
  return true;
336
339
  }
340
+ const destination = normalizeAssetKey(parsed.body.key);
341
+ if (sourceOfKey(destination)) {
342
+ // The session root's own source key is the only allowed source key
343
+ // (Undo). A root we cannot load allows none.
344
+ const rootId = task.parentTaskId ? (await getSessionRoot(task.id))?.id : task.id;
345
+ const root = rootId === task.id ? task : rootId ? await getTaskById(rootId) : null;
346
+ const sourceError = sourceMoveError(destination, root ?? { id: task.id });
347
+ if (sourceError) {
348
+ jsonError(res, sourceError, 400);
349
+ return true;
350
+ }
351
+ }
337
352
  // Only the tasks checked here move: a subtask created after this read stays put.
338
353
  const tasks = parsed.body.cascade ? await getRootTaskChain(task.id) : [task];
339
354
  for (const target of tasks) {
@@ -0,0 +1,191 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import { z } from "zod";
3
+ import { decryptSecret, getEncryptionKey } from "../be/crypto";
4
+ import { deleteKv, deleteSwarmConfigByKey, getDbClient, getKeyStatuses, getKv } from "../be/db";
5
+ import { retirePoolKeyStatus } from "../be/db/api-keys";
6
+ import { deriveCodexKeySuffix } from "../providers/codex-oauth/auth-json";
7
+ import { decodeJwt, extractChatgptPlanType } from "../providers/codex-oauth/flow";
8
+ import { MAX_CODEX_OAUTH_SLOT } from "../providers/codex-oauth/storage";
9
+ import {
10
+ CODEX_AUTH_FAILURE_BENCH_THRESHOLD,
11
+ CODEX_AUTH_WATCH_NAMESPACE,
12
+ codexAuthBenchMarkerKey,
13
+ } from "../utils/codex-auth-failure";
14
+ import { ensureConfigAdmin, requestSecretReadScope, runConfigWriteSideEffects } from "./config";
15
+ import { route } from "./route-def";
16
+ import { jsonError } from "./utils";
17
+
18
+ export const CodexAccountSchema = z.object({
19
+ slot: z.number().int().min(0),
20
+ configKey: z.string(),
21
+ legacy: z.boolean(),
22
+ suffix: z.string().nullable(),
23
+ plan: z.string().nullable(),
24
+ email: z.string().nullable(),
25
+ status: z.enum(["ok", "needs_login", "unchecked"]),
26
+ });
27
+ export type CodexAccount = z.infer<typeof CodexAccountSchema>;
28
+
29
+ export const CodexAccountsResponseSchema = z.object({ accounts: z.array(CodexAccountSchema) });
30
+ export type CodexAccountsResponse = z.infer<typeof CodexAccountsResponseSchema>;
31
+
32
+ const listAccounts = route({
33
+ method: "get",
34
+ path: "/api/codex-oauth/accounts",
35
+ pattern: ["api", "codex-oauth", "accounts"],
36
+ summary: "List stored Codex ChatGPT accounts",
37
+ tags: ["Codex OAuth"],
38
+ responses: {
39
+ 200: { description: "Stored account metadata", schema: CodexAccountsResponseSchema },
40
+ 403: { description: "Caller cannot read all secrets" },
41
+ },
42
+ });
43
+
44
+ const deleteAccount = route({
45
+ method: "delete",
46
+ path: "/api/codex-oauth/accounts/{slot}",
47
+ pattern: ["api", "codex-oauth", "accounts", null],
48
+ summary: "Remove a stored Codex ChatGPT account",
49
+ tags: ["Codex OAuth"],
50
+ rbac: { permission: "config.delete.any" },
51
+ params: z.object({ slot: z.coerce.number().int().min(0).max(MAX_CODEX_OAUTH_SLOT) }),
52
+ responses: {
53
+ 200: { description: "Remaining accounts", schema: CodexAccountsResponseSchema },
54
+ 403: { description: "Config administration and permission to read all secrets required" },
55
+ 404: { description: "Account slot does not exist" },
56
+ },
57
+ });
58
+
59
+ type AccountRow = { key: string; value: string; encrypted: number };
60
+
61
+ async function accountRows(): Promise<Array<AccountRow & { slot: number; legacy: boolean }>> {
62
+ const rows = await getDbClient().query<AccountRow>(
63
+ "SELECT key, value, encrypted FROM swarm_config WHERE scope = 'global' AND (key = 'codex_oauth' OR key GLOB 'codex_oauth_[0-9]*')",
64
+ );
65
+ const hasSlotZero = rows.some((row) => row.key === "codex_oauth_0");
66
+ return rows
67
+ .flatMap((row) => {
68
+ if (row.key === "codex_oauth") {
69
+ return hasSlotZero ? [] : [{ ...row, slot: 0, legacy: true }];
70
+ }
71
+ const match = /^codex_oauth_(0|[1-9]\d*)$/.exec(row.key);
72
+ if (!match) return [];
73
+ const slot = Number(match[1]);
74
+ return slot <= MAX_CODEX_OAUTH_SLOT ? [{ ...row, slot, legacy: false }] : [];
75
+ })
76
+ .sort((a, b) => a.slot - b.slot);
77
+ }
78
+
79
+ const AccountCredentialsSchema = z.object({
80
+ access: z.string().min(1),
81
+ refresh: z.string().min(1),
82
+ expires: z.number().finite(),
83
+ accountId: z.string().min(1),
84
+ });
85
+
86
+ function readCredentials(row: AccountRow): z.infer<typeof AccountCredentialsSchema> | null {
87
+ try {
88
+ const value = row.encrypted ? decryptSecret(row.value, getEncryptionKey()) : row.value;
89
+ return AccountCredentialsSchema.parse(JSON.parse(value));
90
+ } catch {
91
+ return null;
92
+ }
93
+ }
94
+
95
+ function accountSuffix(row: AccountRow): string | null {
96
+ const credentials = readCredentials(row);
97
+ return credentials ? deriveCodexKeySuffix(credentials.access, credentials.accountId) : null;
98
+ }
99
+
100
+ async function accountsResponse(): Promise<CodexAccountsResponse> {
101
+ const rows = await accountRows();
102
+ const statuses = await getKeyStatuses("CODEX_OAUTH", "global", null);
103
+ const accounts: CodexAccount[] = [];
104
+ for (const row of rows) {
105
+ const account: CodexAccount = {
106
+ slot: row.slot,
107
+ configKey: row.key,
108
+ legacy: row.legacy,
109
+ suffix: null,
110
+ plan: null,
111
+ email: null,
112
+ status: "needs_login",
113
+ };
114
+ const credentials = readCredentials(row);
115
+ if (!credentials) {
116
+ accounts.push(account);
117
+ continue;
118
+ }
119
+ account.suffix = deriveCodexKeySuffix(credentials.access, credentials.accountId);
120
+ account.plan = extractChatgptPlanType(credentials.access);
121
+ const profile = decodeJwt(credentials.access)?.["https://api.openai.com/profile"];
122
+ if (profile && typeof profile === "object" && "email" in profile) {
123
+ account.email = typeof profile.email === "string" && profile.email ? profile.email : null;
124
+ }
125
+ const status = statuses.find((entry) => entry.keySuffix === account.suffix);
126
+ const marker = await getKv(CODEX_AUTH_WATCH_NAMESPACE, codexAuthBenchMarkerKey(account.suffix));
127
+ const statusBenched =
128
+ status &&
129
+ status.consecutiveAuthFailures >= CODEX_AUTH_FAILURE_BENCH_THRESHOLD &&
130
+ status.status === "rate_limited";
131
+ account.status = marker || statusBenched ? "needs_login" : status ? "ok" : "unchecked";
132
+ accounts.push(account);
133
+ }
134
+ return { accounts };
135
+ }
136
+
137
+ /** Both responses list account emails and plans, so both routes need this gate. */
138
+ async function ensureSecretRead(req: IncomingMessage, res: ServerResponse): Promise<boolean> {
139
+ if ((await requestSecretReadScope(req)) === "all") return true;
140
+ jsonError(res, "Reading Codex accounts requires access to all secrets", 403);
141
+ return false;
142
+ }
143
+
144
+ export async function handleCodexOAuthAccounts(
145
+ req: IncomingMessage,
146
+ res: ServerResponse,
147
+ pathSegments: string[],
148
+ queryParams: URLSearchParams,
149
+ ): Promise<boolean> {
150
+ if (listAccounts.match(req.method, pathSegments)) {
151
+ const parsed = await listAccounts.parse(req, res, pathSegments, queryParams);
152
+ if (!parsed) return true;
153
+ if (!(await ensureSecretRead(req, res))) return true;
154
+ listAccounts.respond(res, 200, await accountsResponse());
155
+ return true;
156
+ }
157
+
158
+ if (deleteAccount.match(req.method, pathSegments)) {
159
+ const parsed = await deleteAccount.parse(req, res, pathSegments, queryParams);
160
+ if (!parsed) return true;
161
+ if (!(await ensureConfigAdmin(req, res, "config.delete.any"))) return true;
162
+ if (!(await ensureSecretRead(req, res))) return true;
163
+ const deletedKey = await getDbClient().transaction(async () => {
164
+ const rows = await accountRows();
165
+ const account = rows.find((row) => row.slot === parsed.params.slot);
166
+ if (!account || !(await deleteSwarmConfigByKey("global", null, account.key))) return null;
167
+ // A new sign-in takes the first free slot, so it must not inherit this
168
+ // account's auth bench (keyed by slot) or its bench marker.
169
+ const suffix = accountSuffix(account);
170
+ if (suffix) {
171
+ const remaining = rows.flatMap((row) => {
172
+ const keySuffix = row === account ? null : accountSuffix(row);
173
+ return keySuffix ? [{ keySuffix, keyIndex: row.slot }] : [];
174
+ });
175
+ await retirePoolKeyStatus("CODEX_OAUTH", suffix, remaining);
176
+ if (!remaining.some((row) => row.keySuffix === suffix)) {
177
+ await deleteKv(CODEX_AUTH_WATCH_NAMESPACE, codexAuthBenchMarkerKey(suffix));
178
+ }
179
+ }
180
+ return account.key;
181
+ });
182
+ if (deletedKey === null) {
183
+ jsonError(res, "Codex account slot does not exist", 404);
184
+ return true;
185
+ }
186
+ await runConfigWriteSideEffects("global", deletedKey, "");
187
+ deleteAccount.respond(res, 200, await accountsResponse());
188
+ return true;
189
+ }
190
+ return false;
191
+ }
package/src/http/comb.ts CHANGED
@@ -2,7 +2,12 @@ import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import { z } from "zod";
3
3
  import { resolveHttpAuditUserId } from "../be/audit-user";
4
4
  import { getCombConfig } from "../comb/config";
5
- import { REVIEW_BATCH_MAX, ReviewBatchError, sendReviewBatch } from "../comb/review-batch";
5
+ import {
6
+ REVIEW_BATCH_MAX,
7
+ REVIEW_NOTE_MAX,
8
+ ReviewBatchError,
9
+ sendReviewBatch,
10
+ } from "../comb/review-batch";
6
11
  import { can } from "../rbac";
7
12
  import { TaskCreationBlockedError } from "../tasks/errors";
8
13
  import { scrubSecrets } from "../utils/secret-scrubber";
@@ -24,6 +29,12 @@ const resultSchema = z.object({
24
29
  skipped: z.array(skippedSchema),
25
30
  /** Comments of an earlier send whose missing "sent" reply this batch posted again. */
26
31
  repaired: z.array(z.object({ id: z.string(), taskId: z.string() })),
32
+ /**
33
+ * With a new task only. `online`: a lead got it. `offline`: every lead is offline,
34
+ * so the task waits for one. `none`: no lead, so the task waits
35
+ * unassigned in the pool.
36
+ */
37
+ lead: z.enum(["online", "offline", "none"]).optional(),
27
38
  });
28
39
 
29
40
  const batchErrorSchema = z.object({ error: z.string(), skipped: z.array(skippedSchema) });
@@ -41,7 +52,7 @@ const reviewBatchRoute = route({
41
52
  pattern: ["api", "comb", "review-batches"],
42
53
  summary: "Send agent-fs comments to the swarm as one lead task",
43
54
  description:
44
- "Comb's 'Send to swarm'. The server reads each comment again from agent-fs with its bootstrap key, skips replies, resolved comments, and comments already sent, and creates ONE task for the lead. Each sent comment gets a `[comb:sent task=<id>]` reply from the swarm service account. A comment is sent at most once. When an earlier send lost its reply, the batch posts that reply again (`repaired`). Answers 404 while COMB_ENABLED is off.",
55
+ "Comb's 'Send to swarm'. The server reads each comment again from agent-fs with its bootstrap key, skips replies, resolved comments, and comments already sent, and creates ONE task for the lead. Each sent comment gets a `[comb:sent task=<id>]` reply from the swarm service account. A comment is sent at most once. An optional `note` from the sender goes into the task text. When an earlier send lost its reply, the batch posts that reply again (`repaired`). Answers 404 while COMB_ENABLED is off.",
45
56
  tags: ["Comb"],
46
57
  body: z.object({
47
58
  orgId: z.string().min(1),
@@ -54,6 +65,8 @@ const reviewBatchRoute = route({
54
65
  .min(1)
55
66
  .max(1024)
56
67
  .refine(isDrivePath, "scopePath must start with '/' and have no '.' or '..' segment"),
68
+ /** The sender's note for the lead about the whole batch. */
69
+ note: z.string().max(REVIEW_NOTE_MAX).optional(),
57
70
  }),
58
71
  responses: {
59
72
  200: {
@@ -23,8 +23,9 @@ import {
23
23
  isInternalConfigKey,
24
24
  validateConfigEntry,
25
25
  } from "../be/swarm-config-guard";
26
+ import { isScriptRunToken } from "../be/users";
26
27
  import { can, isRbacEnabled, type PermissionVerb } from "../rbac";
27
- import { SwarmConfigSchema } from "../types";
28
+ import { type SwarmConfig, SwarmConfigSchema } from "../types";
28
29
  import { getRequestAuth, type HttpRequestAuth } from "../utils/request-auth-context";
29
30
  import { registerVolatileSecret } from "../utils/secret-scrubber";
30
31
  import { reloadGlobalConfigsAndIntegrations, scheduleIntegrationsReload } from "./core";
@@ -66,7 +67,7 @@ export function readsRepoSecrets(
66
67
  }).allow;
67
68
  }
68
69
 
69
- async function requestSecretReadScope(
70
+ export async function requestSecretReadScope(
70
71
  req: IncomingMessage,
71
72
  resolvedScope?: { agentId?: string; repoId?: string },
72
73
  ): Promise<"all" | "self" | "none"> {
@@ -111,6 +112,49 @@ async function resolveSecretsRead(
111
112
  };
112
113
  }
113
114
 
115
+ /**
116
+ * Ids of the rows a script run's token reads unmasked on its own
117
+ * `/api/config/resolved` (its agent over global, no repo secrets). The run
118
+ * already holds these values in `ctx.swarm.config`. `undefined` for any other
119
+ * caller.
120
+ */
121
+ async function scriptRunHeldConfigIds(req: IncomingMessage): Promise<Set<string> | undefined> {
122
+ const auth = getRequestAuth(req);
123
+ if (auth?.kind !== "agent" || auth.tokenKind !== "agent") return undefined;
124
+ if (!(await isScriptRunToken(auth.tokenId))) return undefined;
125
+ const held = stripApiOnlyKeys(
126
+ await getResolvedConfig(auth.agentId, undefined, { includeRepoSecrets: false }),
127
+ );
128
+ return new Set(held.map((config) => config.id));
129
+ }
130
+
131
+ /**
132
+ * Mask a raw list or by-id read for this caller. A script run's token gets
133
+ * the rows it holds (see {@link scriptRunHeldConfigIds}) unmasked, so
134
+ * `config_list` and `ctx.swarm.config` agree; every other secret row stays
135
+ * masked. Unmasked secret values are registered with the scrubber.
136
+ */
137
+ async function maskConfigRead(
138
+ req: IncomingMessage,
139
+ configs: SwarmConfig[],
140
+ includeSecrets: boolean,
141
+ ): Promise<{ result: SwarmConfig[]; secretsNote: string }> {
142
+ const { effectiveIncludeSecrets, secretsNote } = await resolveSecretsRead(req, includeSecrets);
143
+ const held =
144
+ includeSecrets && !effectiveIncludeSecrets ? await scriptRunHeldConfigIds(req) : undefined;
145
+ const reveal = (config: SwarmConfig) => effectiveIncludeSecrets || !!held?.has(config.id);
146
+ const result = configs.map((config) =>
147
+ config.isSecret && !reveal(config) ? maskSecrets([config])[0]! : config,
148
+ );
149
+ for (const config of result) {
150
+ if (config.isSecret && config.value && reveal(config)) {
151
+ registerVolatileSecret(config.value, `config:${config.key}`);
152
+ }
153
+ }
154
+ const anyMasked = result.some((config) => config.isSecret && !reveal(config));
155
+ return { result, secretsNote: held && !anyMasked ? "" : secretsNote };
156
+ }
157
+
114
158
  /**
115
159
  * Gate a config write/delete over HTTP (DES-445 follow-up).
116
160
  *
@@ -455,13 +499,9 @@ export async function handleConfig(
455
499
  jsonError(res, "Config not found", 404);
456
500
  return true;
457
501
  }
458
- const { effectiveIncludeSecrets, secretsNote } = await resolveSecretsRead(req, includeSecrets);
459
- const singleResult = effectiveIncludeSecrets ? config : maskSecrets([config])[0]!;
460
- if (effectiveIncludeSecrets && singleResult.isSecret && singleResult.value) {
461
- registerVolatileSecret(singleResult.value, `config:${singleResult.key}`);
462
- }
502
+ const { result, secretsNote } = await maskConfigRead(req, [config], includeSecrets);
463
503
  getConfigById.respond(res, 200, {
464
- ...singleResult,
504
+ ...result[0]!,
465
505
  ...(secretsNote ? { message: `Found 1 config(s).${secretsNote}` } : {}),
466
506
  });
467
507
  return true;
@@ -471,7 +511,6 @@ export async function handleConfig(
471
511
  const parsed = await listConfig.parse(req, res, pathSegments, queryParams);
472
512
  if (!parsed) return true;
473
513
  const includeSecrets = parsed.query.includeSecrets === "true";
474
- const { effectiveIncludeSecrets, secretsNote } = await resolveSecretsRead(req, includeSecrets);
475
514
  const configs = stripApiOnlyKeys(
476
515
  await getSwarmConfigs({
477
516
  scope: parsed.query.scope || undefined,
@@ -479,14 +518,7 @@ export async function handleConfig(
479
518
  key: parsed.query.key,
480
519
  }),
481
520
  );
482
- const listResult = effectiveIncludeSecrets ? configs : maskSecrets(configs);
483
- if (effectiveIncludeSecrets) {
484
- for (const c of listResult) {
485
- if (c.isSecret && c.value) {
486
- registerVolatileSecret(c.value, `config:${c.key}`);
487
- }
488
- }
489
- }
521
+ const { result: listResult, secretsNote } = await maskConfigRead(req, configs, includeSecrets);
490
522
  listConfig.respond(res, 200, {
491
523
  configs: listResult,
492
524
  ...(secretsNote ? { message: `Found ${listResult.length} config(s).${secretsNote}` } : {}),
package/src/http/core.ts CHANGED
@@ -95,6 +95,12 @@ import {
95
95
  */
96
96
  const injectedEnvOriginals = new Map<string, { original: string | undefined; injected: string }>();
97
97
 
98
+ /** Read the deployment value without a stored config's temporary env injection. */
99
+ export function getDeploymentEnvValue(key: string): string | undefined {
100
+ const tracked = injectedEnvOriginals.get(key);
101
+ return tracked && process.env[key] === tracked.injected ? tracked.original : process.env[key];
102
+ }
103
+
98
104
  export async function loadGlobalConfigsIntoEnv(override = false): Promise<string[]> {
99
105
  const globalConfigs = await getInjectableGlobalConfigs();
100
106
  const updated: string[] = [];
@@ -11,7 +11,7 @@ import {
11
11
  localTasksPerWeek,
12
12
  } from "../fleet-benchmark";
13
13
  import { isTelemetryEnabled } from "../telemetry";
14
- import { COMPANY_TEAM_SIZES, COMPANY_USE_CASES } from "../utils/company-profile";
14
+ import { COMPANY_TEAM_SIZES, COMPANY_USE_CASES, reportedUseCase } from "../utils/company-profile";
15
15
  import { normalizeOrgWebsite } from "../utils/org-website";
16
16
  import { globalConfig } from "./global-config";
17
17
  import { rejectGuest } from "./request-principal";
@@ -188,6 +188,9 @@ export async function buildFleetBenchmarkResponse(
188
188
  normalizeOrgWebsite(website) && team.success && use.success
189
189
  ? { teamSize: team.data, useCase: use.data }
190
190
  : null;
191
+ // The proxy answers 400 for a use case its catalog does not list yet, so
192
+ // that use case compares against the whole fleet.
193
+ const proxyPeerGroup = peerGroup && reportedUseCase(peerGroup.useCase) ? peerGroup : null;
191
194
  const empty = {
192
195
  level: peerGroup ? ("full" as const) : ("basic" as const),
193
196
  peerGroup: null,
@@ -215,7 +218,7 @@ export async function buildFleetBenchmarkResponse(
215
218
  if (finishedTasks < FLEET_MIN_FINISHED_TASKS)
216
219
  return { ...empty, state: "not_enough_tasks", local };
217
220
 
218
- const fleet = await fleetBenchmarks(now.getTime(), peerGroup);
221
+ const fleet = await fleetBenchmarks(now.getTime(), proxyPeerGroup);
219
222
  const responsePeerGroup =
220
223
  peerGroup && fleet?.peer_group
221
224
  ? {
@@ -0,0 +1,117 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import { z } from "zod";
3
+ import {
4
+ findAcceptedWorkflowOperation,
5
+ type WorkflowIdempotencyClaim,
6
+ } from "../be/db/workflow-idempotency";
7
+ import { getRequestAuth } from "../utils/request-auth-context";
8
+ import { jsonError } from "./utils";
9
+
10
+ /** Visible ASCII, at most 255 characters: room for a UUID, a ULID or a client's own scheme. */
11
+ const IDEMPOTENCY_KEY_PATTERN = /^[\x21-\x7E]{1,255}$/;
12
+
13
+ /** Set on a response answered from an earlier accepted request. */
14
+ export const IDEMPOTENT_REPLAYED_HEADER = "Idempotent-Replayed";
15
+
16
+ /** OpenAPI declaration of the optional `Idempotency-Key` request header. */
17
+ export const idempotencyKeyHeader = (accepted: string) =>
18
+ z.object({
19
+ "Idempotency-Key": z
20
+ .string()
21
+ .regex(IDEMPOTENCY_KEY_PATTERN)
22
+ .optional()
23
+ .describe(
24
+ "Optional client-generated key, 1-255 visible ASCII characters (a UUID works). " +
25
+ `The first request the server accepts under a key ${accepted}; a retry with the same key ` +
26
+ "and an equivalent body gets that request's status and body back (with " +
27
+ `\`${IDEMPOTENT_REPLAYED_HEADER}: true\`) and changes nothing. The key is scoped to the ` +
28
+ "caller's identity and this operation; it is not a credential. The same key with a " +
29
+ "different body is a 409. A request rejected before it is accepted (validation, auth, " +
30
+ "not found, disabled) does not use up the key. Accepted keys do not expire.",
31
+ ),
32
+ });
33
+
34
+ /** A request that sent a usable `Idempotency-Key`. */
35
+ export interface IdempotencyKeyRequest {
36
+ /** Stable identity of the authenticated caller, never a credential value. */
37
+ principal: string;
38
+ key: string;
39
+ }
40
+
41
+ /**
42
+ * The caller identity a key is scoped to. A user or session token names its
43
+ * user or agent; rotating the token keeps the scope. Requests on the swarm API
44
+ * key are the operator, or the agent named by X-Agent-ID: every worker shares
45
+ * that key, so this keeps their keys apart (it is not an access boundary; the
46
+ * swarm key may act as any agent). A guest page session has no identity.
47
+ */
48
+ function idempotencyPrincipal(req: IncomingMessage, myAgentId: string | undefined): string | null {
49
+ const auth = getRequestAuth(req);
50
+ switch (auth?.kind) {
51
+ case "user":
52
+ return `user:${auth.userId}`;
53
+ case "agent":
54
+ return `agent:${auth.agentId}`;
55
+ case "controller":
56
+ return `controller:${auth.controllerId}`;
57
+ case "guest":
58
+ return null;
59
+ default:
60
+ return myAgentId ? `agent:${myAgentId}` : "operator";
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Read the `Idempotency-Key` header. Undefined when none was sent; null when
66
+ * it is malformed or the caller has no identity to scope it to, after writing
67
+ * the 400.
68
+ */
69
+ export function readIdempotencyKey(
70
+ req: IncomingMessage,
71
+ res: ServerResponse,
72
+ myAgentId: string | undefined,
73
+ ): IdempotencyKeyRequest | null | undefined {
74
+ const raw = req.headers["idempotency-key"];
75
+ if (raw === undefined) return undefined;
76
+ const key = Array.isArray(raw) ? raw.join(", ") : raw;
77
+ if (!IDEMPOTENCY_KEY_PATTERN.test(key)) {
78
+ jsonError(res, "Idempotency-Key must be 1-255 visible ASCII characters", 400);
79
+ return null;
80
+ }
81
+ const principal = idempotencyPrincipal(req, myAgentId);
82
+ if (!principal) {
83
+ jsonError(res, "Idempotency-Key needs an identified caller", 400);
84
+ return null;
85
+ }
86
+ return { principal, key };
87
+ }
88
+
89
+ /**
90
+ * Answer from the operation already accepted under this claim's scope, if
91
+ * any. Returns false, writing nothing, when there is none. Otherwise writes a
92
+ * 409 when the stored request differs, a 410 when its workflow was deleted,
93
+ * or hands the stored body to `send` with the replay header set, and returns
94
+ * true. Runs after authentication and before any check that may have changed
95
+ * since the original request.
96
+ */
97
+ export async function replayAcceptedOperation(
98
+ res: ServerResponse,
99
+ claim: WorkflowIdempotencyClaim,
100
+ send: (body: unknown) => void,
101
+ ): Promise<boolean> {
102
+ const accepted = await findAcceptedWorkflowOperation(claim);
103
+ if (!accepted) return false;
104
+ if (accepted.fingerprint !== claim.fingerprint) {
105
+ jsonError(res, "Idempotency-Key was already used for a different request", 409);
106
+ } else if (accepted.resourceDeletedAt !== null) {
107
+ jsonError(
108
+ res,
109
+ "The workflow this Idempotency-Key created or triggered was deleted; send a new key to start over",
110
+ 410,
111
+ );
112
+ } else {
113
+ res.setHeader(IDEMPOTENT_REPLAYED_HEADER, "true");
114
+ send(accepted.body);
115
+ }
116
+ return true;
117
+ }
package/src/http/index.ts CHANGED
@@ -76,6 +76,7 @@ import { handleApprovalRequests } from "./approval-requests";
76
76
  import { handleApps } from "./apps";
77
77
  import { handleAssets } from "./assets";
78
78
  import { handleBudgets } from "./budgets";
79
+ import { handleCodexOAuthAccounts } from "./codex-oauth-accounts";
79
80
  import { handleCodexOAuthDevice } from "./codex-oauth-device";
80
81
  import { handleCodexOAuthKeepWarm } from "./codex-oauth-keep-warm";
81
82
  import { handleComb } from "./comb";
@@ -115,6 +116,7 @@ import { handlePageProxy } from "./page-proxy";
115
116
  import { handlePages } from "./pages";
116
117
  import { handlePagesPublic } from "./pages-public";
117
118
  import { handlePoll } from "./poll";
119
+ import { handlePoolKeys } from "./pool-keys";
118
120
  import { handlePricing } from "./pricing";
119
121
  import { handlePromptTemplates } from "./prompt-templates";
120
122
  import { handleRanking } from "./ranking";
@@ -360,6 +362,7 @@ const httpServer = createHttpServer(async (req, res) => {
360
362
  () => handleStatus(req, res, pathSegments, queryParams),
361
363
  () => handleOnboarding(req, res, pathSegments, queryParams),
362
364
  () => handleCodexOAuthDevice(req, res, pathSegments, queryParams),
365
+ () => handleCodexOAuthAccounts(req, res, pathSegments, queryParams),
363
366
  () => handleActiveSessions(req, res, pathSegments, queryParams, myAgentId),
364
367
  () => handlePricing(req, res, pathSegments, queryParams, myAgentId),
365
368
  () => handleSchedules(req, res, pathSegments, queryParams, myAgentId),
@@ -367,6 +370,7 @@ const httpServer = createHttpServer(async (req, res) => {
367
370
  () => handleWorkflowEvents(req, res, pathSegments, queryParams),
368
371
  () => handleApprovalRequests(req, res, pathSegments, queryParams),
369
372
  () => handleApps(req, res, pathSegments, queryParams, myAgentId),
373
+ () => handlePoolKeys(req, res, pathSegments, queryParams),
370
374
  () => handleConfig(req, res, pathSegments, queryParams),
371
375
  () => handleFs(req, res, pathSegments, queryParams, myAgentId),
372
376
  () => handleComb(req, res, pathSegments, queryParams, myAgentId),
@@ -898,6 +902,13 @@ httpServer
898
902
  .catch((err) => {
899
903
  console.error("[boot-scrub-sweep] startup scrub failed (non-fatal):", err);
900
904
  });
905
+
906
+ // Source key backfill is idempotent, resumable, and non-fatal.
907
+ import("../be/session-source-backfill")
908
+ .then(({ runSessionSourceBackfill }) => runSessionSourceBackfill())
909
+ .catch((err) => {
910
+ console.error("[session-source-backfill] startup run failed (non-fatal):", err);
911
+ });
901
912
  })
902
913
  .on("error", (err) => {
903
914
  console.error("HTTP Server Error:", err);