@desplega.ai/agent-swarm 1.173.0 → 2.0.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 (432) hide show
  1. package/README.md +1 -1
  2. package/dist/{acp-adapter-ccm9xrep.js → acp-adapter-sj2g0m84.js} +17 -16
  3. package/dist/{actions-sgeyv08k.js → actions-fegk44gr.js} +39 -34
  4. package/dist/{amp-adapter-gpxp3kc9.js → amp-adapter-ad8ft89x.js} +8 -7
  5. package/dist/{app-km9qnv2f.js → app-kfvgbvzy.js} +18 -14
  6. package/dist/{assistant-5r8tcvvd.js → assistant-z7zdy9ja.js} +40 -35
  7. package/dist/{boot-reembed-swwzdgd7.js → boot-reembed-5c2z0mbe.js} +10 -9
  8. package/dist/{boot-reembed-16rv9843.js → boot-reembed-j8xa181e.js} +10 -9
  9. package/dist/{boot-scrub-sweep-zkr6vkqb.js → boot-scrub-sweep-hnkn3rgt.js} +11 -10
  10. package/dist/{boot-seeds-d8sp49t2.js → boot-seeds-kwjq4d2z.js} +14 -13
  11. package/dist/{claude-adapter-yzzzh8jp.js → claude-adapter-9vmvpvgd.js} +19 -18
  12. package/dist/{claude-managed-adapter-xgxhqrt2.js → claude-managed-adapter-k9qae05j.js} +7 -5
  13. package/dist/{claude-managed-setup-bnmjarjc.js → claude-managed-setup-q54zx5q2.js} +6 -4
  14. package/dist/{claude-sdk-session-4whxere4.js → claude-sdk-session-9jye6cxr.js} +11881 -11472
  15. package/dist/{cli-94b48jhk.js → cli-03gedhng.js} +16 -14
  16. package/dist/{cli-1w0vj3gw.js → cli-1e4b6bzh.js} +7 -7
  17. package/dist/{cli-d603qf86.js → cli-29jc5q7w.js} +10 -8
  18. package/dist/cli-2m1ktfzz.js +17795 -0
  19. package/dist/{cli-x119kdba.js → cli-3dj8k4vy.js} +1 -1
  20. package/dist/{cli-srqq3gc5.js → cli-3fbmmawm.js} +5 -5
  21. package/dist/{cli-gmdajfw5.js → cli-4jxm2qtz.js} +4987 -1708
  22. package/dist/{cli-tadfgd0t.js → cli-5hkm7whv.js} +33 -4
  23. package/dist/{cli-42qhyvwx.js → cli-5t4f29ez.js} +3 -3
  24. package/dist/{cli-7a0sde2x.js → cli-6cagzc96.js} +9 -0
  25. package/dist/cli-6cvfxs8r.js +47 -0
  26. package/dist/{cli-0kxf81jj.js → cli-6jyj59nb.js} +2 -2
  27. package/dist/{cli-w1apjecb.js → cli-6razj6t4.js} +2 -2
  28. package/dist/{cli-anzdk0z4.js → cli-74ahz61x.js} +2 -2
  29. package/dist/{cli-m89t9jdf.js → cli-7x0g4sn1.js} +15416 -16024
  30. package/dist/{cli-yatxqynm.js → cli-9ha6hs2d.js} +1 -290
  31. package/dist/{cli-81r1wjyv.js → cli-9nfx44eh.js} +1 -1
  32. package/dist/cli-9traj3dn.js +638 -0
  33. package/dist/{cli-smcrrmhy.js → cli-agzebbd5.js} +9 -9
  34. package/dist/{cli-na2pxe4f.js → cli-apx87jne.js} +2 -2
  35. package/dist/{cli-rfz2ve55.js → cli-bzszjf2p.js} +2855 -862
  36. package/dist/{cli-c5km5znr.js → cli-c8r3kzzb.js} +1 -1
  37. package/dist/{cli-hsem16hb.js → cli-cgtksawx.js} +7 -4
  38. package/dist/{cli-db3m6zdt.js → cli-d2qr7aj4.js} +1 -1
  39. package/dist/{cli-rfs4vddv.js → cli-d5f9ed5c.js} +5 -5
  40. package/dist/cli-e3g5mbkz.js +776 -0
  41. package/dist/{cli-7jykv7ef.js → cli-e6mpjhed.js} +1 -1
  42. package/dist/{cli-7a93cv8c.js → cli-ee9gtz70.js} +98 -23
  43. package/dist/{cli-3nxdmjrc.js → cli-eejsgvm4.js} +4 -4
  44. package/dist/{cli-fmhj1ehj.js → cli-ef1hg1wv.js} +470 -51
  45. package/dist/cli-f527ce74.js +781 -0
  46. package/dist/{cli-ppy4nnnh.js → cli-f7kr7svz.js} +2 -2
  47. package/dist/cli-hjwmhcg1.js +10622 -0
  48. package/dist/{cli-2bw3w47j.js → cli-hnhtk6cj.js} +1 -1
  49. package/dist/{cli-ywm1azg9.js → cli-hnmhtzvh.js} +6 -4
  50. package/dist/{cli-h7k0xtry.js → cli-hvqbtna4.js} +4 -1256
  51. package/dist/{cli-s8z6x4b4.js → cli-j0pfptdp.js} +218 -102
  52. package/dist/cli-jta2eyzc.js +619 -0
  53. package/dist/{cli-5er5gjbm.js → cli-k382tshb.js} +10 -11
  54. package/dist/{cli-v3zhnxsb.js → cli-k3dp5tpf.js} +2 -2
  55. package/dist/{cli-wd0hz6ah.js → cli-kbrt8gqa.js} +9 -91
  56. package/dist/cli-kceah1eh.js +92 -0
  57. package/dist/{cli-ne7smj1b.js → cli-ke896fcf.js} +10 -10
  58. package/dist/{cli-xc7p7s3g.js → cli-kt6zqg0e.js} +6 -6
  59. package/dist/{cli-wrh08zjd.js → cli-m50t4309.js} +5 -18
  60. package/dist/cli-n2wq4j7m.js +6985 -0
  61. package/dist/{cli-bv2etw2y.js → cli-n3hzg89b.js} +46 -15
  62. package/dist/{cli-tmhsxhb8.js → cli-n5vwg842.js} +23 -6
  63. package/dist/{cli-qm1448f9.js → cli-ne236afy.js} +8 -8
  64. package/dist/{cli-d161g69q.js → cli-p1x85d2t.js} +50 -2
  65. package/dist/{cli-7pz3b7x5.js → cli-przpmdgr.js} +25 -0
  66. package/dist/{cli-0a7mm3zp.js → cli-q0ke1yww.js} +25 -21
  67. package/dist/{cli-e3pmyt39.js → cli-q50942rs.js} +1 -1
  68. package/dist/{cli-r44t9wqc.js → cli-r106725k.js} +2 -2
  69. package/dist/{cli-phcqp1b9.js → cli-r2pnvj8g.js} +2 -2
  70. package/dist/{cli-jpc4r224.js → cli-r62a35ea.js} +19 -3
  71. package/dist/{cli-cd0hv9nh.js → cli-ra0xzggj.js} +2 -2
  72. package/dist/cli-rhs9pz33.js +294 -0
  73. package/dist/{cli-r4fqfpwf.js → cli-s2sp957d.js} +50 -27
  74. package/dist/{cli-1x7da0de.js → cli-s4setq91.js} +8 -8
  75. package/dist/{cli-6thk4jz3.js → cli-se8w6wny.js} +6 -6
  76. package/dist/cli-skxvvpap.js +10140 -0
  77. package/dist/{cli-014vns2y.js → cli-stfs0k5s.js} +208 -525
  78. package/dist/{cli-0p3enk35.js → cli-tqaazemc.js} +9 -1
  79. package/dist/{cli-nwrdx840.js → cli-tr4sxp68.js} +3 -3
  80. package/dist/{cli-r0byd61h.js → cli-tsde1hdr.js} +15 -15
  81. package/dist/{cli-b07hr3bt.js → cli-txqb9s38.js} +2 -2
  82. package/dist/{cli-6r2f7edk.js → cli-v1yb0rqb.js} +6 -6
  83. package/dist/{cli-saqyage0.js → cli-vatyhshy.js} +179 -27
  84. package/dist/{cli-5wpw4rab.js → cli-vdach5a9.js} +5 -5
  85. package/dist/{cli-7avy4v8w.js → cli-w1q7h2r2.js} +5 -3
  86. package/dist/{cli-rcp5aqdw.js → cli-w7m81tb7.js} +1 -1
  87. package/dist/{cli-c3rk1v0x.js → cli-wayw93nt.js} +3 -3
  88. package/dist/cli-wmgrkscj.js +23254 -0
  89. package/dist/{cli-9xf7fdtp.js → cli-z5jjpaxw.js} +29 -6
  90. package/dist/{cli-451ks6aw.js → cli-zkpks3r3.js} +1 -1
  91. package/dist/cli.js +29 -45
  92. package/dist/{codex-adapter-r3dn4fp5.js → codex-adapter-y81sssk0.js} +15 -14
  93. package/dist/{codex-login-841kv5fk.js → codex-login-f2rcfpp9.js} +3 -2
  94. package/dist/{codex-session-runner-fwt793h4.js → codex-session-runner-8vx4j6a9.js} +15 -14
  95. package/dist/{commands-z8fq9qna.js → commands-kcpvpcrr.js} +11 -10
  96. package/dist/{controller-hq5ggpcp.js → controller-ywrmt6yd.js} +5 -4
  97. package/dist/{controllers-connect-3mf2btyj.js → controllers-connect-kwpvj3rf.js} +42 -37
  98. package/dist/{cursor-adapter-q8pgv5qa.js → cursor-adapter-cehv1qns.js} +7 -6
  99. package/dist/{db-yyhqqrbf.js → db-k8ankj20.js} +12 -11
  100. package/dist/{db-qfpq4bt7.js → db-ww5d7jq3.js} +28 -9
  101. package/dist/{devin-adapter-xgtj680j.js → devin-adapter-x1022qb1.js} +3 -2
  102. package/dist/{dispatcher-x0x7qfcq.js → dispatcher-e8gq5m68.js} +38 -33
  103. package/dist/{docker-f503k9p0.js → docker-ff6y1190.js} +3 -2
  104. package/dist/{dsh-adapter-34etvb5m.js → dsh-adapter-3epjm3af.js} +7 -6
  105. package/dist/{e2b-66xra4az.js → e2b-8ygwhg0f.js} +4 -3
  106. package/dist/{e2b-xwhyj8cw.js → e2b-swhnpw3n.js} +4 -3
  107. package/dist/{event-bus-vjtv82gm.js → event-bus-6axnngdj.js} +3 -2
  108. package/dist/{external-channel-h1fjecwe.js → external-channel-h3359cwn.js} +11 -10
  109. package/dist/{google-generative-ai-szdxbpqg.js → google-generative-ai-g8db7x0f.js} +4 -2
  110. package/dist/{google-vertex-qzw3p36d.js → google-vertex-r2692q0r.js} +4 -2
  111. package/dist/{grok-adapter-e7mr287e.js → grok-adapter-0d4x7gkx.js} +18 -17
  112. package/dist/{handlers-znb9n7d4.js → handlers-an1gzv21.js} +41 -36
  113. package/dist/{heartbeat-bm4qkn8d.js → heartbeat-6dakeb61.js} +43 -38
  114. package/dist/{hook-254r4mza.js → hook-6v6cv6d5.js} +11 -23
  115. package/dist/{hook-xaa3q80s.js → hook-sa5xye4y.js} +12 -11
  116. package/dist/{http-3dgrgp8y.js → http-r7f3eky0.js} +1499 -251
  117. package/dist/index-52j1aja4.js +3465 -0
  118. package/dist/{index-fx3jqq1e.js → index-7ep4gzdk.js} +38 -33
  119. package/dist/index-9gw58n4k.js +193 -0
  120. package/dist/index-9j87mcm8.js +56305 -0
  121. package/dist/index-bsmcresk.js +79424 -0
  122. package/dist/index-mh85qhec.js +475 -0
  123. package/dist/{index-g2byveez.js → index-rmpw9a8q.js} +38 -33
  124. package/dist/{index-2ybk7khy.js → index-s2wxgzr0.js} +468 -683
  125. package/dist/{index-bdsqa799.js → index-v1v6n7hg.js} +43 -38
  126. package/dist/index-we27kd31.js +12810 -0
  127. package/dist/{index-jd7sejm2.js → index-zk96aq1k.js} +10 -9
  128. package/dist/{keepalive-6ywhpja1.js → keepalive-be9vwy5h.js} +12 -11
  129. package/dist/{lead-cw45n21s.js → lead-0861cwrf.js} +61 -56
  130. package/dist/{maintenance-ss1a1ge9.js → maintenance-mzzgc8bn.js} +10 -9
  131. package/dist/{node-fkajaq96.js → node-mn1zs7an.js} +2 -1
  132. package/dist/{oauth-refresh-sweep-3c6f8vyy.js → oauth-refresh-sweep-cpdnfwdj.js} +11 -10
  133. package/dist/{onboard-8hzdhc1k.js → onboard-fh8qs4r3.js} +11 -3
  134. package/dist/{opencode-adapter-qbchbcfz.js → opencode-adapter-nm42wwj8.js} +18 -13
  135. package/dist/{otel-impl-e6mnvc4v.js → otel-impl-x6c1c9g4.js} +242 -868
  136. package/dist/{pi-mono-adapter-3c9g9snh.js → pi-mono-adapter-9n2ya240.js} +244 -106
  137. package/dist/{pricing-refresh-gedkjdw0.js → pricing-refresh-qb0rv0ap.js} +12 -11
  138. package/dist/{rbac-roles-dt4vh1yy.js → rbac-roles-1gzdwbh1.js} +11 -10
  139. package/dist/{rbac-roles-eszdg6pm.js → rbac-roles-3mraey52.js} +12 -11
  140. package/dist/{render-v2-907cy6sa.js → render-v2-agpa45zz.js} +18 -14
  141. package/dist/{resume-42stcaz0.js → resume-x0ydy9s9.js} +25 -21
  142. package/dist/{runtime-qfry88v2.js → runtime-sa2vr16g.js} +2 -2
  143. package/dist/{setup-adbc1c10.js → setup-as9ga0d6.js} +2 -2
  144. package/dist/{virtual-modules-rtmt494q.js → virtual-modules-9gkxqrap.js} +6 -5
  145. package/dist/{work-objects-wpj8ck95.js → work-objects-y5g1y7ev.js} +41 -36
  146. package/dist/{worker-bzdra9rf.js → worker-860c3e38.js} +61 -56
  147. package/dist/{x-qfmygqqv.js → x-m477fr0h.js} +3 -2
  148. package/openapi.json +523 -344
  149. package/package.json +10 -11
  150. package/src/be/config-refs.ts +60 -0
  151. package/src/be/db/page-versions.ts +15 -0
  152. package/src/be/db/runtime.ts +35 -8
  153. package/src/be/db/skill-name-resolution.ts +36 -0
  154. package/src/be/db/tasks/read.ts +230 -0
  155. package/src/be/db/tasks/write.ts +7 -0
  156. package/src/be/db.ts +52 -7
  157. package/src/be/migrations/215_swarm_config_export_to_env.sql +34 -0
  158. package/src/be/migrations/216_task_resolved_effort.sql +5 -0
  159. package/src/be/migrations/217_chat_sdk_state.sql +104 -0
  160. package/src/be/model-catalog-store.ts +5 -1
  161. package/src/be/model-tier-resolution.ts +12 -1
  162. package/src/be/model-validation.ts +5 -2
  163. package/src/be/models-catalog.ts +26 -1
  164. package/src/be/modelsdev-cache.json +198 -63
  165. package/src/be/modelsdev-cache.ts +9 -1
  166. package/src/be/script-credential-broker.ts +39 -2
  167. package/src/be/scripts/run-saved.ts +1 -0
  168. package/src/be/scripts/typecheck.ts +10 -8
  169. package/src/be/seed/runner.ts +13 -1
  170. package/src/be/seed/state-db.ts +5 -0
  171. package/src/be/seed/types.ts +6 -0
  172. package/src/be/seed-pricing.ts +19 -2
  173. package/src/be/seed-scripts/catalog/ops-catalog-audit.inline.ts +1 -1
  174. package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +1 -1
  175. package/src/be/seed-skills/bundled-files.generated.json +0 -18
  176. package/src/be/seed-skills/index.ts +104 -3
  177. package/src/be/swarm-config-guard.ts +52 -0
  178. package/src/cli.tsx +0 -16
  179. package/src/commands/onboard/non-interactive.ts +4 -2
  180. package/src/commands/provider-credentials.ts +52 -6
  181. package/src/commands/runner.ts +190 -54
  182. package/src/e2b/env.ts +5 -0
  183. package/src/extensions/contract-types.generated.ts +1 -1
  184. package/src/extensions/contract.ts +45 -0
  185. package/src/extensions/dispatcher.ts +7 -0
  186. package/src/extensions/post-bridge.ts +40 -0
  187. package/src/github/handlers.ts +11 -4
  188. package/src/heartbeat/templates.ts +8 -5
  189. package/src/hooks/hook.ts +0 -26
  190. package/src/http/all-routes.ts +0 -1
  191. package/src/http/config.ts +36 -17
  192. package/src/http/core.ts +4 -0
  193. package/src/http/index.ts +14 -2
  194. package/src/http/integrations.ts +88 -1
  195. package/src/http/mcp.ts +15 -5
  196. package/src/http/page-proxy.ts +2 -4
  197. package/src/http/pages-public.ts +3 -3
  198. package/src/http/pages.ts +3 -13
  199. package/src/http/poll.ts +28 -68
  200. package/src/http/requested-by.ts +54 -0
  201. package/src/http/scripts.ts +1 -0
  202. package/src/http/stats.ts +1 -30
  203. package/src/http/tasks.ts +61 -5
  204. package/src/http/utils.ts +14 -0
  205. package/src/http/webhooks.ts +159 -1
  206. package/src/http/x.ts +1 -0
  207. package/src/integrations/chat-sdk/adapters.ts +129 -0
  208. package/src/integrations/chat-sdk/attachments.ts +232 -0
  209. package/src/integrations/chat-sdk/background.ts +36 -0
  210. package/src/integrations/chat-sdk/commands.ts +194 -0
  211. package/src/integrations/chat-sdk/config.ts +198 -0
  212. package/src/integrations/chat-sdk/discord-api.ts +63 -0
  213. package/src/integrations/chat-sdk/discord-gateway.ts +185 -0
  214. package/src/integrations/chat-sdk/handlers.ts +28 -0
  215. package/src/integrations/chat-sdk/inbound.ts +426 -0
  216. package/src/integrations/chat-sdk/limits.ts +48 -0
  217. package/src/integrations/chat-sdk/logger.ts +81 -0
  218. package/src/integrations/chat-sdk/outbound.ts +123 -0
  219. package/src/integrations/chat-sdk/relay.ts +172 -0
  220. package/src/integrations/chat-sdk/router.ts +169 -0
  221. package/src/integrations/chat-sdk/runtime.ts +351 -0
  222. package/src/integrations/chat-sdk/schema.ts +245 -0
  223. package/src/integrations/chat-sdk/state.ts +243 -0
  224. package/src/integrations/chat-sdk/status.ts +244 -0
  225. package/src/integrations/chat-sdk/steering.ts +58 -0
  226. package/src/integrations/chat-sdk/teams-origin.ts +77 -0
  227. package/src/integrations/chat-sdk/telegram-api.ts +56 -0
  228. package/src/integrations/chat-sdk/templates.ts +16 -0
  229. package/src/integrations/chat-sdk/test-hooks.ts +37 -0
  230. package/src/integrations/chat-sdk/thread-buffer.ts +100 -0
  231. package/src/integrations/chat-sdk/typing.ts +65 -0
  232. package/src/pages/version.ts +4 -5
  233. package/src/prompts/base-prompt.ts +111 -45
  234. package/src/providers/acp-target-catalog.ts +6 -0
  235. package/src/providers/acp-targets.ts +55 -2
  236. package/src/providers/amp-adapter.ts +1 -1
  237. package/src/providers/claude-adapter.ts +1 -1
  238. package/src/providers/claude-flag-settings.ts +17 -5
  239. package/src/providers/claude-managed-adapter.ts +1 -1
  240. package/src/providers/claude-session-events.ts +6 -1
  241. package/src/providers/dsh-adapter.ts +1 -1
  242. package/src/providers/opencode-adapter.ts +243 -26
  243. package/src/providers/opencode-swarm-plugin.ts +31 -0
  244. package/src/providers/pi-mono-adapter.ts +398 -153
  245. package/src/providers/pricing-sources.md +33 -2
  246. package/src/providers/reasoning-effort.ts +27 -0
  247. package/src/providers/types.ts +6 -2
  248. package/src/rbac/legacy-policy.ts +8 -2
  249. package/src/rbac/permissions.ts +21 -0
  250. package/src/scheduler/scheduler.ts +4 -2
  251. package/src/scripts-runtime/loader.ts +19 -7
  252. package/src/scripts-runtime/sdk-allowlist.ts +8 -6
  253. package/src/scripts-runtime/swarm-sdk.ts +0 -11
  254. package/src/scripts-runtime/types/stdlib.d.ts +27 -15
  255. package/src/scripts-runtime/types/swarm-extension.d.ts +63 -1
  256. package/src/scripts-runtime/types/swarm-sdk.d.ts +27 -15
  257. package/src/server.ts +52 -18
  258. package/src/slack/event-dedup.ts +1 -1
  259. package/src/tasks/additive-buffer.ts +4 -2
  260. package/src/tasks/context-key.ts +60 -1
  261. package/src/tasks/model-pin.ts +67 -0
  262. package/src/tasks/worker-follow-up.ts +4 -31
  263. package/src/tests/acp-adapter.test.ts +42 -0
  264. package/src/tests/api-drain-worker-e2e.test.ts +1 -6
  265. package/src/tests/base-prompt.test.ts +58 -54
  266. package/src/tests/chat-sdk-commands.test.ts +191 -0
  267. package/src/tests/chat-sdk-config.test.ts +334 -0
  268. package/src/tests/chat-sdk-discord-api.test.ts +68 -0
  269. package/src/tests/chat-sdk-discord-gateway.test.ts +252 -0
  270. package/src/tests/chat-sdk-discord-webhook.test.ts +126 -0
  271. package/src/tests/chat-sdk-fields-sync.test.ts +50 -0
  272. package/src/tests/chat-sdk-harness.ts +146 -0
  273. package/src/tests/chat-sdk-inbound.test.ts +363 -0
  274. package/src/tests/chat-sdk-relay.test.ts +470 -0
  275. package/src/tests/chat-sdk-reload.test.ts +128 -0
  276. package/src/tests/chat-sdk-shutdown.test.ts +220 -0
  277. package/src/tests/chat-sdk-slack.test.ts +268 -0
  278. package/src/tests/chat-sdk-state.test.ts +163 -0
  279. package/src/tests/chat-sdk-status-route.test.ts +382 -0
  280. package/src/tests/chat-sdk-teams.test.ts +277 -0
  281. package/src/tests/chat-sdk-tools.test.ts +520 -0
  282. package/src/tests/chat-sdk-typing.test.ts +120 -0
  283. package/src/tests/chat-sdk-webhook-telegram.test.ts +200 -0
  284. package/src/tests/claude-adapter-binary.test.ts +9 -4
  285. package/src/tests/claude-flag-settings.test.ts +11 -3
  286. package/src/tests/claude-managed-adapter.test.ts +1 -1
  287. package/src/tests/claude-sdk-lifecycle.test.ts +9 -3
  288. package/src/tests/claude-thinking-tokens.test.ts +57 -0
  289. package/src/tests/claude-worker-parity.test.ts +2 -7
  290. package/src/tests/config-env-export-chat-sdk.test.ts +11 -0
  291. package/src/tests/config-refs.test.ts +66 -0
  292. package/src/tests/context-key.test.ts +19 -0
  293. package/src/tests/cors-allowlist.test.ts +25 -0
  294. package/src/tests/create-page-tool.test.ts +18 -0
  295. package/src/tests/credential-check.test.ts +139 -0
  296. package/src/tests/credentials.test.ts +21 -0
  297. package/src/tests/db-runtime.test.ts +61 -0
  298. package/src/tests/db-task-read-contract.test.ts +20 -0
  299. package/src/tests/db-task-write-contract.test.ts +10 -0
  300. package/src/tests/docs-index-footprint.test.js +3 -3
  301. package/src/tests/dsh-adapter.test.ts +1 -1
  302. package/src/tests/enabled-capabilities.test.ts +20 -0
  303. package/src/tests/entrypoint-api-readiness.test.ts +14 -8
  304. package/src/tests/entrypoint-config-env-export.test.ts +5 -2
  305. package/src/tests/entrypoint-export-to-env.test.ts +78 -0
  306. package/src/tests/explicit-model-validation.test.ts +105 -1
  307. package/src/tests/extensions-example-route-chat-channel.test.ts +154 -0
  308. package/src/tests/extensions-post-bridge.test.ts +51 -0
  309. package/src/tests/fetch-resolved-env.test.ts +128 -0
  310. package/src/tests/fixtures/chat-sdk/telegram-private-text.json +10 -0
  311. package/src/tests/fixtures/extensions/route-chat-channel-to-agent/hooks.ts +27 -0
  312. package/src/tests/fixtures/extensions/route-chat-channel-to-agent/manifest.json +9 -0
  313. package/src/tests/fixtures/openai-compatible-registry-extension.child.ts +146 -0
  314. package/src/tests/fixtures/runner-legacy-resume-prompt.ts +122 -0
  315. package/src/tests/github-handlers.test.ts +35 -0
  316. package/src/tests/harness-provider-resolution.test.ts +12 -0
  317. package/src/tests/heartbeat-checklist.test.ts +3 -2
  318. package/src/tests/hook-runtime-close.test.ts +0 -2
  319. package/src/tests/http-api-integration.test.ts +0 -25
  320. package/src/tests/migration-046-budgets.test.ts +17 -16
  321. package/src/tests/migration-217-chat-sdk-state.test.ts +113 -0
  322. package/src/tests/model-tier-resolution.test.ts +93 -0
  323. package/src/tests/models-catalog.test.ts +42 -1
  324. package/src/tests/openai-compatible-providers.test.ts +134 -0
  325. package/src/tests/openai-compatible-registry-extension.test.ts +42 -0
  326. package/src/tests/opencode-adapter.test.ts +216 -2
  327. package/src/tests/opencode-helmcode-sdk.test.ts +201 -0
  328. package/src/tests/opencode-shell-env.test.ts +145 -0
  329. package/src/tests/openrouter-base-url.test.ts +101 -151
  330. package/src/tests/page-proxy.test.ts +1 -1
  331. package/src/tests/pages-browser-sdk.test.ts +76 -0
  332. package/src/tests/pages-feedback.test.ts +1 -1
  333. package/src/tests/paused-tasks-requested-by.test.ts +102 -0
  334. package/src/tests/pi-custom-provider-fake-server.test.ts +442 -0
  335. package/src/tests/pi-mono-adapter.test.ts +385 -5
  336. package/src/tests/pi-shell-env.test.ts +170 -0
  337. package/src/tests/prompt-template-remaining.test.ts +35 -0
  338. package/src/tests/rbac-engine.test.ts +11 -3
  339. package/src/tests/runner-exit-after-tasks.test.ts +0 -4
  340. package/src/tests/runner-legacy-resume-prompt.test.ts +64 -0
  341. package/src/tests/script-connections.test.ts +2 -0
  342. package/src/tests/scripts-http.test.ts +201 -1
  343. package/src/tests/scripts-only-gating.test.ts +2 -3
  344. package/src/tests/scripts-runtime.test.ts +58 -40
  345. package/src/tests/secret-scrubber.test.ts +86 -0
  346. package/src/tests/seed-skills-bundled-files.test.ts +45 -53
  347. package/src/tests/seed-skills-retire.test.ts +182 -0
  348. package/src/tests/send-task-requester-gate.test.ts +58 -0
  349. package/src/tests/session-cost-recompute-unit.test.ts +46 -0
  350. package/src/tests/skill-get-name-resolution.test.ts +137 -0
  351. package/src/tests/sonnet-5-5-registry.test.ts +9 -1
  352. package/src/tests/swarm-config-export-to-env.test.ts +267 -0
  353. package/src/tests/swarm-config-reserved-keys.test.ts +16 -0
  354. package/src/tests/swarm-diff.test.ts +1 -1
  355. package/src/tests/task-model-effort-rerun.test.ts +164 -0
  356. package/src/tests/task-session-prompts.test.ts +132 -0
  357. package/src/tests/task-tool-manifest.test.ts +14 -0
  358. package/src/tests/task-tools-ctx.test.ts +2 -0
  359. package/src/tests/tool-annotations.test.ts +6 -6
  360. package/src/tests/ui-logs-parser.test.ts +43 -0
  361. package/src/tests/used-by-route.test.ts +6 -0
  362. package/src/tests/vercel-ai-gateway.test.ts +149 -0
  363. package/src/tests/workflow-swarm-script.test.ts +38 -0
  364. package/src/tests/workflow-wait-filter.test.ts +5 -3
  365. package/src/tools/chat-access.ts +41 -0
  366. package/src/tools/chat-open-dm.ts +59 -0
  367. package/src/tools/chat-post.ts +74 -0
  368. package/src/tools/chat-read-thread.ts +100 -0
  369. package/src/tools/chat-reply.ts +75 -0
  370. package/src/tools/create-page.ts +3 -13
  371. package/src/tools/model-catalog/index.ts +2 -1
  372. package/src/tools/register-chat-channel.ts +139 -0
  373. package/src/tools/send-task.ts +24 -0
  374. package/src/tools/skills/skill-get.ts +17 -9
  375. package/src/tools/swarm-config/get-config.ts +2 -1
  376. package/src/tools/swarm-config/list-config.ts +2 -1
  377. package/src/tools/swarm-config/set-config.ts +16 -3
  378. package/src/tools/tool-config.ts +8 -6
  379. package/src/types.ts +12 -2
  380. package/src/utils/config-env-export.ts +74 -0
  381. package/src/utils/credentials.test.ts +92 -0
  382. package/src/utils/credentials.ts +52 -4
  383. package/src/utils/harness-provider.ts +15 -6
  384. package/src/utils/runtime-model-catalog.ts +11 -1
  385. package/src/utils/secret-scrubber.ts +71 -2
  386. package/src/utils/task-tool-manifest.ts +19 -2
  387. package/src/workflows/executors/swarm-script.ts +1 -0
  388. package/templates/skills/agent-browser/SKILL.md +1 -1
  389. package/templates/skills/agent-browser/content.md +1 -1
  390. package/templates/skills/apps/SKILL.md +8 -1
  391. package/templates/skills/apps/content.md +8 -1
  392. package/templates/skills/chat-sdk/config.json +14 -0
  393. package/templates/skills/chat-sdk/content.md +84 -0
  394. package/templates/skills/composio-gmail/content.md +7 -0
  395. package/templates/skills/composio-google-docs/content.md +1 -1
  396. package/templates/skills/email-domain-health/config.json +14 -0
  397. package/templates/skills/email-domain-health/content.md +131 -0
  398. package/templates/skills/kv-storage/content.md +2 -2
  399. package/templates/skills/pages/content.md +8 -10
  400. package/templates/skills/swarm-extensions/content.md +2 -0
  401. package/templates/skills/swarm-scripts/SKILL.md +4 -2
  402. package/templates/skills/swarm-scripts/config.json +1 -1
  403. package/templates/skills/swarm-scripts/content.md +4 -2
  404. package/templates/skills/workflow-iterate/content.md +38 -0
  405. package/dist/artifact-eczsknc3.js +0 -3843
  406. package/dist/cli-bd957dgb.js +0 -153
  407. package/dist/cli-q51yprfq.js +0 -1104
  408. package/dist/cli-t1ze64h3.js +0 -811
  409. package/dist/cli-tcc4wd5d.js +0 -56471
  410. package/dist/index-9ch1n761.js +0 -1102
  411. package/src/artifact-sdk/index.ts +0 -2
  412. package/src/artifact-sdk/localtunnel.d.ts +0 -20
  413. package/src/artifact-sdk/port.ts +0 -12
  414. package/src/artifact-sdk/server.ts +0 -183
  415. package/src/artifact-sdk/tunnel.ts +0 -19
  416. package/src/commands/artifact.ts +0 -249
  417. package/src/http/ecosystem.ts +0 -77
  418. package/src/tests/artifact-commands.test.ts +0 -92
  419. package/src/tests/artifact-sdk.test.ts +0 -793
  420. package/src/tests/register-service-security.test.ts +0 -194
  421. package/src/tools/list-services.ts +0 -107
  422. package/src/tools/register-service.ts +0 -177
  423. package/src/tools/unregister-service.ts +0 -73
  424. package/src/tools/update-service-status.ts +0 -96
  425. package/templates/skills/artifacts/config.json +0 -14
  426. package/templates/skills/artifacts/content.md +0 -344
  427. package/templates/skills/artifacts/files/examples/approval-flow.ts +0 -34
  428. package/templates/skills/artifacts/files/examples/hono-dashboard.ts +0 -31
  429. package/templates/skills/artifacts/files/examples/multi-artifact.ts +0 -20
  430. package/templates/skills/artifacts/files/examples/static-report.sh +0 -17
  431. /package/src/{artifact-sdk → pages}/browser-sdk.ts +0 -0
  432. /package/src/{artifact-sdk → pages}/feedback-overlay.ts +0 -0
@@ -6,8 +6,29 @@
6
6
  * discovered at session creation and registered as custom tools.
7
7
  */
8
8
 
9
- import { existsSync, lstatSync, symlinkSync, unlinkSync } from "node:fs";
9
+ import {
10
+ existsSync,
11
+ lstatSync,
12
+ readFileSync,
13
+ symlinkSync,
14
+ unlinkSync,
15
+ writeFileSync,
16
+ } from "node:fs";
17
+ import { mkdtemp, rm } from "node:fs/promises";
18
+ import { tmpdir } from "node:os";
10
19
  import { join } from "node:path";
20
+ import {
21
+ ignoredBaseUrlVar,
22
+ type OpenAICompatibleProvider,
23
+ openAICompatibleKeyVars,
24
+ openAICompatibleProvider,
25
+ openAICompatibleProviders,
26
+ providerBaseUrl,
27
+ providerForModel,
28
+ providerHarnessModels,
29
+ providerKeyFromEnv,
30
+ providerKeyVars,
31
+ } from "@desplega/model-catalog";
11
32
  import {
12
33
  getBuiltinModel as getModel,
13
34
  getBuiltinModels as getModels,
@@ -23,11 +44,14 @@ import type {
23
44
  } from "@earendil-works/pi-coding-agent";
24
45
  import {
25
46
  type AgentSession,
47
+ type BashSpawnContext,
26
48
  createAgentSession,
49
+ createBashToolDefinition,
27
50
  createCodemodeExtension,
28
51
  createMcpExtension,
29
52
  createToolSearchExtension,
30
53
  DefaultResourceLoader,
54
+ defineTool,
31
55
  getAgentDir,
32
56
  ModelRuntime,
33
57
  SessionManager,
@@ -44,7 +68,8 @@ import { parseEnvFlag } from "../utils/env-flag";
44
68
  import { fetchInstalledMcpServers } from "../utils/mcp-server-fetcher";
45
69
  import { swarmRuntimeInstanceId } from "../utils/multi-runtime";
46
70
  import { DEFAULT_OPENROUTER_BASE_URL, getOpenRouterBaseUrl } from "../utils/openrouter-base-url";
47
- import { scrubSecrets } from "../utils/secret-scrubber";
71
+ import { snapshotCatalogRows } from "../utils/runtime-model-catalog";
72
+ import { registerEnvSecrets, scrubObject, scrubSecrets } from "../utils/secret-scrubber";
48
73
  import { readPkgVersion } from "./harness-version";
49
74
  import { createSwarmHooksExtension } from "./pi-mono-extension";
50
75
  import { McpHttpClient, type McpTool } from "./pi-mono-mcp-client";
@@ -90,6 +115,11 @@ function modelToCredKeys(modelStr: string | undefined): string[] | null {
90
115
  if (provider === "openrouter") return ["OPENROUTER_API_KEY"];
91
116
  if (provider === "openai") return ["OPENAI_API_KEY"];
92
117
  if (provider === "google") return ["GOOGLE_API_KEY", "GEMINI_API_KEY"];
118
+ // models.json custom providers (`writeSessionModelsJson`). The
119
+ // runner normalizes key aliases (HELMCODE_API_NAN → NAN_API_KEY), but a
120
+ // worker without that step (or a boot-time check) may only see the alias.
121
+ const direct = openAICompatibleProvider(provider);
122
+ if (direct) return providerKeyVars(direct);
93
123
  }
94
124
  // Bare model name with no provider prefix — adapter falls through to a
95
125
  // best-effort resolution against multiple providers, so the boot loop
@@ -303,14 +333,25 @@ export async function checkPiMonoCredentials(
303
333
  };
304
334
  }
305
335
 
306
- // Permissive case: any one supported key works.
307
- if (env.ANTHROPIC_API_KEY || env.OPENROUTER_API_KEY || env.OPENAI_API_KEY) {
336
+ // Permissive case: any one supported key works. A registry provider key
337
+ // (NaN, Helmcode) only serves `<id>/*` models, but it is still a usable pi
338
+ // credential (the task's explicit model is checked above once it is set).
339
+ if (
340
+ env.ANTHROPIC_API_KEY ||
341
+ env.OPENROUTER_API_KEY ||
342
+ env.OPENAI_API_KEY ||
343
+ openAICompatibleKeyVars().some((k) => env[k])
344
+ ) {
308
345
  return { ready: true, missing: [], satisfiedBy: "env" };
309
346
  }
310
347
  return {
311
348
  ready: false,
312
349
  missing: ["ANTHROPIC_API_KEY", "OPENROUTER_API_KEY", "OPENAI_API_KEY", authFile],
313
- hint: "Set one of ANTHROPIC_API_KEY / OPENROUTER_API_KEY / OPENAI_API_KEY (any one suffices), or run `pi auth login` to create ~/.pi/agent/auth.json.",
350
+ hint: `Set one of ANTHROPIC_API_KEY / OPENROUTER_API_KEY / OPENAI_API_KEY (any one suffices; ${openAICompatibleProviders()
351
+ .map((p) => p.envVar)
352
+ .join(" or ")} serve only ${openAICompatibleProviders()
353
+ .map((p) => `${p.id}/*`)
354
+ .join(" or ")} models), or run \`pi auth login\` to create ~/.pi/agent/auth.json.`,
314
355
  };
315
356
  }
316
357
 
@@ -433,13 +474,21 @@ function envHasAnthropicCred(env: Record<string, string | undefined>): boolean {
433
474
  return !!(env.ANTHROPIC_API_KEY || env.ANTHROPIC_OAUTH_TOKEN);
434
475
  }
435
476
 
436
- const PI_RUNTIME_API_KEYS = [
477
+ const PI_RUNTIME_API_KEYS: readonly (readonly [envKey: string, provider: string])[] = [
437
478
  ["OPENROUTER_API_KEY", "openrouter"],
438
479
  ["ANTHROPIC_API_KEY", "anthropic"],
439
480
  ["OPENAI_API_KEY", "openai"],
440
481
  ["GEMINI_API_KEY", "google"],
441
482
  ["GOOGLE_API_KEY", "google"],
442
- ] as const;
483
+ // models.json custom providers (the registry). A runtime key beats the
484
+ // file's `apiKey: "$NAN_API_KEY"`, which pi would otherwise read from the
485
+ // shared process.env, where a credential pool is still the raw comma list.
486
+ // Later rows overwrite earlier ones, so each provider's aliases come first
487
+ // and the canonical var (NAN_API_KEY over HELMCODE_API_NAN) wins.
488
+ ...openAICompatibleProviders().flatMap((p) =>
489
+ [...p.aliasEnvVars, p.envVar].map((name) => [name, p.id] as const),
490
+ ),
491
+ ];
443
492
 
444
493
  /**
445
494
  * Build pi-coding-agent auth services from the runner's per-task resolved env.
@@ -451,8 +500,13 @@ const PI_RUNTIME_API_KEYS = [
451
500
  */
452
501
  export async function createPiRuntimeAuth(
453
502
  env: Record<string, string | undefined> = process.env,
503
+ modelsPath?: string,
454
504
  ): Promise<ModelRuntime> {
455
- const modelRuntime = await ModelRuntime.create();
505
+ // A per-session models.json (`writeSessionModelsJson`) still shares the
506
+ // agent dir's catalog cache, which only holds built-in provider catalogs.
507
+ const modelRuntime = await ModelRuntime.create(
508
+ modelsPath ? { modelsPath, modelsStorePath: join(getAgentDir(), "models-store.json") } : {},
509
+ );
456
510
  for (const [envKey, provider] of PI_RUNTIME_API_KEYS) {
457
511
  const apiKey = env[envKey];
458
512
  if (apiKey) {
@@ -464,11 +518,12 @@ export async function createPiRuntimeAuth(
464
518
  }
465
519
 
466
520
  /**
467
- * Sidecar marker recording the gateway `baseUrl` WE wrote into models.json
468
- * (and any user value we displaced). Lets `ensureOpenRouterModelsOverride`
469
- * revert precisely when the env returns to default, without ever touching a
470
- * hand-authored override it didn't create. Lives next to models.json; NOT
471
- * part of pi's config surface.
521
+ * Legacy sidecar marker. Earlier versions wrote the OpenRouter gateway into
522
+ * the shared `<agentDir>/models.json` and recorded here the `baseUrl` they
523
+ * wrote (and any user value they displaced). Nothing writes it any more;
524
+ * `writeSessionModelsJson` reads it to keep such a leftover gateway out of a
525
+ * default-gateway session. Lives next to models.json; NOT part of pi's config
526
+ * surface.
472
527
  */
473
528
  const OPENROUTER_OVERRIDE_MARKER = ".agent-swarm-openrouter-override.json";
474
529
 
@@ -492,141 +547,183 @@ async function readJsonFile(path: string): Promise<Record<string, unknown> | und
492
547
  }
493
548
 
494
549
  /**
495
- * Materialize (or revert) the OpenRouter gateway override in
496
- * `<agentDir>/models.json` so pi-coding-agent's `ModelRuntime` (which loads
497
- * that file at create time) composes every built-in OpenRouter model with
498
- * the gateway `baseUrl` — see `src/utils/openrouter-base-url.ts` for the
499
- * env contract.
500
- *
501
- * Gateway set (non-default): merge-preserving write — existing provider
502
- * entries and other openrouter keys are kept, only
503
- * `providers.openrouter.baseUrl` is set; a displaced user value is recorded
504
- * in a sidecar marker. A corrupt existing file is overwritten with just the
505
- * override — for gateway deployments, guaranteed rerouting wins over
506
- * preserving unparseable config.
550
+ * The models.json `providers.<id>` block for one OpenAI-compatible registry
551
+ * provider, at `baseUrl` (the `<ID>_BASE_URL` override or the registry default).
507
552
  *
508
- * Gateway unset/default: reverts a PREVIOUSLY WRITTEN override (marker
509
- * present and models.json still carries the marker's value) — restoring the
510
- * displaced user baseUrl or deleting the key, dropping now-empty objects,
511
- * and removing models.json entirely if the override was all it contained.
512
- * The env value in a shared worker isn't per-task in practice, but reverts
513
- * make rollback = "unset the env" and keep repo-scoped/self-hosted setups
514
- * from inheriting a stale gateway (PR #1010 review). Without a marker the
515
- * function never touches models.json, so hand-authored overrides survive.
553
+ * `apiKey` is always the `"$VAR"` interpolation of the canonical env var, never
554
+ * a key value: pi reads a bare name as a literal key. The key a session really
555
+ * uses arrives as a runtime key (`createPiRuntimeAuth`), which beats this field.
556
+ * Costs are 0: the server reprices every session from the pricing table.
557
+ * A `modelsFromCatalog` provider (`vercel`) lists every agent chat model of its
558
+ * catalog snapshot section (`providerHarnessModels`).
559
+ * A `modelId` missing from the list (a catalog id outside it) gets a bare
560
+ * entry so pi still resolves it instead of falling back to its default model;
561
+ * pi fills the omitted limits with its own defaults.
516
562
  */
517
- export async function ensureOpenRouterModelsOverride(
518
- agentDir: string,
519
- env: Record<string, string | undefined> = process.env,
520
- ): Promise<void> {
521
- const baseUrl = getOpenRouterBaseUrl(env as NodeJS.ProcessEnv);
522
- const modelsPath = join(agentDir, "models.json");
523
- const markerPath = join(agentDir, OPENROUTER_OVERRIDE_MARKER);
524
-
525
- if (baseUrl === DEFAULT_OPENROUTER_BASE_URL) {
526
- await revertOpenRouterModelsOverride(modelsPath, markerPath);
527
- return;
528
- }
529
-
530
- let existing: Record<string, unknown> = {};
531
- try {
532
- const file = Bun.file(modelsPath);
533
- if (await file.exists()) {
534
- existing = JSON.parse(await file.text()) as Record<string, unknown>;
535
- }
536
- } catch (err) {
537
- console.warn(
538
- `[pi-mono] ${modelsPath} is unreadable/invalid (${err instanceof Error ? err.message : err}); rewriting with the OpenRouter gateway override`,
539
- );
540
- existing = {};
563
+ export function customProviderModelsBlock(
564
+ provider: OpenAICompatibleProvider,
565
+ modelId?: string,
566
+ baseUrl: string = provider.baseUrl,
567
+ ) {
568
+ const zeroCost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
569
+ const listed = providerHarnessModels(provider, snapshotCatalogRows(provider.id));
570
+ const models: Record<string, unknown>[] = listed.map((m) => ({
571
+ id: m.id,
572
+ name: m.name,
573
+ reasoning: m.reasoning,
574
+ input: [...m.input],
575
+ contextWindow: m.contextWindow,
576
+ maxTokens: m.maxOutputTokens,
577
+ cost: { ...zeroCost },
578
+ compat: { ...m.compat },
579
+ }));
580
+ if (modelId && !listed.some((m) => m.id === modelId)) {
581
+ models.push({ id: modelId, name: modelId, cost: { ...zeroCost } });
541
582
  }
542
-
543
- const providers =
544
- typeof existing.providers === "object" && existing.providers !== null
545
- ? (existing.providers as Record<string, unknown>)
546
- : {};
547
- const openrouter =
548
- typeof providers.openrouter === "object" && providers.openrouter !== null
549
- ? (providers.openrouter as Record<string, unknown>)
550
- : {};
551
-
552
- // Record what we displace, once — re-runs with the marker already present
553
- // keep the ORIGINAL displaced value (the current baseUrl is then ours).
554
- const priorMarker = (await readJsonFile(markerPath)) as OpenRouterOverrideMarker | undefined;
555
- const currentBaseUrl = typeof openrouter.baseUrl === "string" ? openrouter.baseUrl : undefined;
556
- const previous = priorMarker
557
- ? priorMarker.previous
558
- : currentBaseUrl !== baseUrl
559
- ? currentBaseUrl
560
- : undefined;
561
-
562
- const next = {
563
- ...existing,
564
- providers: {
565
- ...providers,
566
- openrouter: { ...openrouter, baseUrl },
567
- },
568
- };
569
- await Bun.write(modelsPath, `${JSON.stringify(next, null, 2)}\n`);
570
- const marker: OpenRouterOverrideMarker = {
583
+ return {
571
584
  baseUrl,
572
- ...(previous !== undefined ? { previous } : {}),
585
+ api: "openai-completions",
586
+ apiKey: `$${provider.envVar}`,
587
+ models,
573
588
  };
574
- await Bun.write(markerPath, `${JSON.stringify(marker, null, 2)}\n`);
589
+ }
590
+
591
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
592
+ return typeof value === "object" && value !== null && !Array.isArray(value);
593
+ }
594
+
595
+ /** The models.json one pi session loads; see `writeSessionModelsJson`. */
596
+ export interface SessionModelsJson {
597
+ /** Per-session models.json, or undefined to load the shared `<agentDir>/models.json`. */
598
+ modelsPath: string | undefined;
599
+ /** Remove the per-session file. Safe to call more than once. */
600
+ dispose(): Promise<void>;
575
601
  }
576
602
 
577
603
  /**
578
- * Undo a marker-recorded gateway override. Only acts when the marker exists
579
- * AND models.json's `providers.openrouter.baseUrl` still equals the value
580
- * the marker says we wrote — anything else means the user edited the file
581
- * since, and we leave it alone (the stale marker is dropped either way).
604
+ * Compose the models.json this session loads from the shared
605
+ * `<agentDir>/models.json` plus this session's env, and write it to a private
606
+ * temp dir. Concurrent sessions in one worker share `<agentDir>`, so writing
607
+ * per-task values into the shared file lets one session load another's
608
+ * endpoint (and send its key there). The shared file is only ever read: it
609
+ * holds what the operator wrote by hand.
610
+ *
611
+ * The private copy keeps every top-level key and provider of the shared file
612
+ * and adds, from `env` only:
613
+ * - `providers.openrouter.baseUrl`: the session's OpenRouter gateway, over a
614
+ * hand-authored one. With the default gateway, a value an earlier version
615
+ * wrote into the shared file (recorded in the legacy marker) is reverted, so
616
+ * a stale gateway never leaks in.
617
+ * - one block per registry provider (`nan`, `helmcode`, `vercel`) whose key
618
+ * (canonical var or alias) is set, at its `<ID>_BASE_URL` or the registry
619
+ * default. A block already in the shared file is user-authored and wins,
620
+ * except that a `baseUrlLocked` provider (`vercel`) always gets its one
621
+ * endpoint and API, with a warning when the shared block named another.
622
+ * - `model` (the task's model) adds a bare entry to its provider's block when
623
+ * its id is outside the table.
624
+ *
625
+ * Nothing to add (no registry key, default gateway, no override marker): no
626
+ * copy, the session loads the shared file as before. A corrupt shared file is
627
+ * replaced by an empty object in the copy; pi cannot load it either way.
582
628
  */
583
- async function revertOpenRouterModelsOverride(
584
- modelsPath: string,
585
- markerPath: string,
586
- ): Promise<void> {
587
- const marker = (await readJsonFile(markerPath)) as OpenRouterOverrideMarker | undefined;
588
- if (!marker) return;
589
- const removeMarker = () => Bun.file(markerPath).delete();
590
-
591
- const existing = await readJsonFile(modelsPath);
592
- if (!existing) {
593
- await removeMarker();
594
- return;
595
- }
596
- const providers =
597
- typeof existing.providers === "object" && existing.providers !== null
598
- ? { ...(existing.providers as Record<string, unknown>) }
599
- : undefined;
600
- const openrouter =
601
- providers && typeof providers.openrouter === "object" && providers.openrouter !== null
602
- ? { ...(providers.openrouter as Record<string, unknown>) }
603
- : undefined;
604
- if (!providers || !openrouter || openrouter.baseUrl !== marker.baseUrl) {
605
- await removeMarker();
606
- return;
629
+ export async function writeSessionModelsJson(
630
+ agentDir: string,
631
+ env: Record<string, string | undefined> = process.env,
632
+ model?: string,
633
+ ): Promise<SessionModelsJson> {
634
+ const sharedPath = join(agentDir, "models.json");
635
+ const registry = openAICompatibleProviders();
636
+ const wanted = registry.filter((p) => providerKeyFromEnv(p, env) !== undefined);
637
+ const gateway = getOpenRouterBaseUrl(env as NodeJS.ProcessEnv);
638
+ const marker = (await readJsonFile(join(agentDir, OPENROUTER_OVERRIDE_MARKER))) as
639
+ | OpenRouterOverrideMarker
640
+ | undefined;
641
+ if (wanted.length === 0 && gateway === DEFAULT_OPENROUTER_BASE_URL && !marker) {
642
+ return { modelsPath: undefined, dispose: async () => {} };
607
643
  }
608
644
 
609
- if (marker.previous !== undefined) {
610
- openrouter.baseUrl = marker.previous;
611
- } else {
612
- delete openrouter.baseUrl;
645
+ let shared: Record<string, unknown> = {};
646
+ const sharedFile = Bun.file(sharedPath);
647
+ if (await sharedFile.exists()) {
648
+ try {
649
+ const parsed: unknown = JSON.parse(await sharedFile.text());
650
+ if (!isPlainObject(parsed)) throw new Error("not a JSON object");
651
+ shared = parsed;
652
+ } catch (err) {
653
+ console.warn(
654
+ scrubSecrets(
655
+ `[pi-mono] ${sharedPath} is unreadable/invalid (${err instanceof Error ? err.message : String(err)}); this session loads its own provider blocks only`,
656
+ ),
657
+ );
658
+ }
613
659
  }
614
- if (Object.keys(openrouter).length === 0) {
615
- delete providers.openrouter;
616
- } else {
617
- providers.openrouter = openrouter;
660
+
661
+ const providers = isPlainObject(shared.providers) ? { ...shared.providers } : {};
662
+ const openrouter = isPlainObject(providers.openrouter) ? { ...providers.openrouter } : {};
663
+ if (gateway !== DEFAULT_OPENROUTER_BASE_URL) {
664
+ openrouter.baseUrl = gateway;
665
+ } else if (marker && openrouter.baseUrl === marker.baseUrl) {
666
+ if (marker.previous !== undefined) openrouter.baseUrl = marker.previous;
667
+ else delete openrouter.baseUrl;
618
668
  }
619
- const next: Record<string, unknown> = { ...existing, providers };
620
- if (Object.keys(providers).length === 0) {
621
- delete next.providers;
669
+ if (Object.keys(openrouter).length > 0) providers.openrouter = openrouter;
670
+ else delete providers.openrouter;
671
+
672
+ const modelProvider = providerForModel(model);
673
+ const modelId = modelProvider && model ? model.slice(modelProvider.id.length + 1) : undefined;
674
+ for (const provider of wanted) {
675
+ const ignored = ignoredBaseUrlVar(provider, env);
676
+ if (ignored) {
677
+ console.warn(
678
+ scrubSecrets(
679
+ `[pi-mono] ${ignored} is ignored: ${provider.displayName} is only reached at ${provider.baseUrl}`,
680
+ ),
681
+ );
682
+ }
683
+ const existing = providers[provider.id];
684
+ if (provider.baseUrlLocked && isPlainObject(existing)) {
685
+ if (existing.baseUrl !== provider.baseUrl || existing.api !== "openai-completions") {
686
+ console.warn(
687
+ scrubSecrets(
688
+ `[pi-mono] ${sharedPath} providers.${provider.id} points at ${String(existing.baseUrl)}; using ${provider.baseUrl}`,
689
+ ),
690
+ );
691
+ }
692
+ providers[provider.id] = {
693
+ ...customProviderModelsBlock(
694
+ provider,
695
+ modelProvider?.id === provider.id ? modelId : undefined,
696
+ ),
697
+ ...existing,
698
+ baseUrl: provider.baseUrl,
699
+ api: "openai-completions",
700
+ };
701
+ continue;
702
+ }
703
+ if (existing !== undefined) {
704
+ console.warn(
705
+ scrubSecrets(
706
+ `[pi-mono] ${sharedPath} already has a providers.${provider.id} block; using it as is`,
707
+ ),
708
+ );
709
+ continue;
710
+ }
711
+ providers[provider.id] = customProviderModelsBlock(
712
+ provider,
713
+ modelProvider?.id === provider.id ? modelId : undefined,
714
+ providerBaseUrl(provider, env),
715
+ );
622
716
  }
623
717
 
624
- if (Object.keys(next).length === 0) {
625
- await Bun.file(modelsPath).delete();
626
- } else {
627
- await Bun.write(modelsPath, `${JSON.stringify(next, null, 2)}\n`);
628
- }
629
- await removeMarker();
718
+ const next: Record<string, unknown> = { ...shared, providers };
719
+ if (Object.keys(providers).length === 0) delete next.providers;
720
+ const dir = await mkdtemp(join(tmpdir(), "pi-session-models-"));
721
+ const modelsPath = join(dir, "models.json");
722
+ await Bun.write(modelsPath, `${JSON.stringify(next, null, 2)}\n`);
723
+ return {
724
+ modelsPath,
725
+ dispose: () => rm(dir, { recursive: true, force: true }),
726
+ };
630
727
  }
631
728
 
632
729
  /**
@@ -639,10 +736,33 @@ async function revertOpenRouterModelsOverride(
639
736
  * anthropic-provider env lookup from failing at session-start with
640
737
  * "No API key found for anthropic" — see task 37a4a87a and the chronic
641
738
  * weekly-fire pattern (2026-04-13 → 2026-05-11) tracked in HEARTBEAT.md.
739
+ *
740
+ * `provider/id` strings that pi-ai's builtin catalog does not know (the
741
+ * models.json custom providers of the registry, `nan` / `helmcode`) fall back to
742
+ * `modelRuntime.getModel(provider, id)` when a runtime is passed. When nothing
743
+ * resolves, a scrubbed warning names the model: the session then silently runs
744
+ * pi's own default model, which is otherwise invisible.
642
745
  */
643
746
  export function resolveModel(
644
747
  modelStr: string,
645
748
  env: Record<string, string | undefined> = process.env,
749
+ modelRuntime?: Pick<ModelRuntime, "getModel">,
750
+ ) {
751
+ const model = resolveModelInner(modelStr, env, modelRuntime);
752
+ if (modelStr && !model) {
753
+ console.warn(
754
+ scrubSecrets(
755
+ `[pi-mono] Model "${modelStr}" did not resolve to a builtin or models.json model; pi will fall back to its default model`,
756
+ ),
757
+ );
758
+ }
759
+ return model;
760
+ }
761
+
762
+ function resolveModelInner(
763
+ modelStr: string,
764
+ env: Record<string, string | undefined>,
765
+ modelRuntime: Pick<ModelRuntime, "getModel"> | undefined,
646
766
  ) {
647
767
  if (!modelStr) return undefined;
648
768
 
@@ -687,11 +807,17 @@ export function resolveModel(
687
807
  const slashIdx = modelStr.indexOf("/");
688
808
  const provider = modelStr.slice(0, slashIdx);
689
809
  const modelId = modelStr.slice(slashIdx + 1);
810
+ let builtin: ReturnType<typeof getModel> | undefined;
690
811
  try {
691
- return getModel(provider as "anthropic", modelId as never);
812
+ // pi-ai returns undefined (does not throw) for an unknown provider/id.
813
+ builtin = getModel(provider as "anthropic", modelId as never);
692
814
  } catch {
693
- return undefined;
815
+ builtin = undefined;
694
816
  }
817
+ if (builtin) return builtin;
818
+ // Custom providers live only in models.json, so only the runtime's
819
+ // composed store knows them.
820
+ return modelRuntime?.getModel(provider, modelId);
695
821
  }
696
822
 
697
823
  // Try as a full model ID with common providers
@@ -786,6 +912,31 @@ function describeAssistantTurn(message: unknown): AssistantTurnShape {
786
912
 
787
913
  const TERMINAL_STORE_PROGRESS_STATUSES = new Set(["completed", "failed"]);
788
914
 
915
+ /** Rewrite a pi session file with every line scrubbed. No-op when absent or clean. */
916
+ function scrubSessionFileSync(path: string | undefined): void {
917
+ if (!path || !existsSync(path)) return;
918
+ const text = readFileSync(path, "utf-8");
919
+ const scrubbed = scrubSecrets(text);
920
+ if (scrubbed !== text) writeFileSync(path, scrubbed);
921
+ }
922
+
923
+ /**
924
+ * pi persists every session entry to its own `sessions/<cwd>/*.jsonl`, an
925
+ * egress none of ours covers: a provider error that echoes the request's key
926
+ * lands there verbatim. Scrub each entry on its way to disk. The in-memory
927
+ * entries (the model's context) are left as they are. pi's first write flushes
928
+ * every entry held so far, so that file is scrubbed once right after it.
929
+ */
930
+ export function scrubPiSessionPersistence(sessionManager: SessionManager): void {
931
+ const persist = sessionManager._persist.bind(sessionManager);
932
+ sessionManager._persist = (entry) => {
933
+ const file = sessionManager.getSessionFile();
934
+ const existed = file ? existsSync(file) : true;
935
+ persist(scrubObject(entry));
936
+ if (!existed) scrubSessionFileSync(file);
937
+ };
938
+ }
939
+
789
940
  export class PiMonoSession implements ProviderSession {
790
941
  private listeners: Array<(event: ProviderEvent) => void> = [];
791
942
  private eventQueue: ProviderEvent[] = [];
@@ -840,14 +991,18 @@ export class PiMonoSession implements ProviderSession {
840
991
  private emptyTurnReprompted = false;
841
992
  /** `abort()` was called: never prompt a session someone is trying to stop. */
842
993
  private abortRequested = false;
994
+ /** Removes this session's own models.json once the session ends. */
995
+ private disposeModels: () => Promise<void>;
843
996
 
844
997
  constructor(
845
998
  agentSession: AgentSession,
846
999
  config: ProviderSessionConfig,
847
1000
  createdSymlink: boolean,
848
1001
  appliedReasoningEffort: ReasoningEffort | null = null,
1002
+ disposeModels: () => Promise<void> = async () => {},
849
1003
  ) {
850
1004
  this.agentSession = agentSession;
1005
+ this.disposeModels = disposeModels;
851
1006
  this.config = config;
852
1007
  this.createdSymlink = createdSymlink;
853
1008
  this.appliedReasoningEffort = appliedReasoningEffort;
@@ -885,12 +1040,10 @@ export class PiMonoSession implements ProviderSession {
885
1040
  }
886
1041
 
887
1042
  private emit(event: ProviderEvent): void {
888
- // Scrub secrets from raw_log / raw_stderr content before egress (log file
889
- // write, listener dispatch, downstream session-logs push + pretty-print).
890
- const scrubbed: ProviderEvent =
891
- event.type === "raw_log" || event.type === "raw_stderr"
892
- ? { ...event, content: scrubSecrets(event.content) }
893
- : event;
1043
+ // Scrub every event before egress (log file write, listener dispatch,
1044
+ // downstream session-logs push + pretty-print): an error or result event
1045
+ // can carry a provider message that echoes the request's key.
1046
+ const scrubbed = scrubObject(event);
894
1047
 
895
1048
  // Log all events
896
1049
  this.logFileHandle.write(
@@ -1094,7 +1247,7 @@ export class PiMonoSession implements ProviderSession {
1094
1247
  // message; anything else surfaces its raw error text.
1095
1248
  if (this.terminalError) {
1096
1249
  const classification = classifyAwsSdkError(this.terminalError);
1097
- const message = classification?.message ?? this.terminalError;
1250
+ const message = scrubSecrets(classification?.message ?? this.terminalError);
1098
1251
  const category = classification?.category;
1099
1252
  this.emit({ type: "error", message, category });
1100
1253
  return {
@@ -1143,7 +1296,7 @@ export class PiMonoSession implements ProviderSession {
1143
1296
  sessionId: this._sessionId,
1144
1297
  isError: true,
1145
1298
  errorCategory: awsCatchError?.category,
1146
- failureReason: awsCatchError?.message ?? errorMessage,
1299
+ failureReason: scrubSecrets(awsCatchError?.message ?? errorMessage),
1147
1300
  appliedReasoningEffort: this.appliedReasoningEffort,
1148
1301
  };
1149
1302
  } finally {
@@ -1153,6 +1306,15 @@ export class PiMonoSession implements ProviderSession {
1153
1306
  cleanupAgentsMdSymlink(this.config.cwd);
1154
1307
  }
1155
1308
  this.agentSession.dispose();
1309
+ // Entries pi wrote outside `_persist` (a rewrite on migration or
1310
+ // branching) are covered here.
1311
+ try {
1312
+ // Test doubles of AgentSession may carry no session manager.
1313
+ scrubSessionFileSync(this.agentSession.sessionManager?.getSessionFile());
1314
+ } catch (err) {
1315
+ console.warn(scrubSecrets(`[pi-mono] could not scrub the session file: ${String(err)}`));
1316
+ }
1317
+ await this.disposeModels();
1156
1318
  }
1157
1319
  }
1158
1320
 
@@ -1504,6 +1666,32 @@ export function piDefaultToolAdditions(features: PiSessionFeatures): string[] {
1504
1666
  return additions;
1505
1667
  }
1506
1668
 
1669
+ /** Keys pi computes for each Bash call (PATH carries pi's managed `rg`/`fd` bin dir). */
1670
+ const PI_SPAWN_ENV_KEYS = [
1671
+ "PATH",
1672
+ "PI_SESSION_ID",
1673
+ "PI_SESSION_FILE",
1674
+ "PI_PROVIDER",
1675
+ "PI_MODEL",
1676
+ "PI_REASONING_LEVEL",
1677
+ ] as const;
1678
+
1679
+ /**
1680
+ * The env one pi Bash call runs with: the session's own env plus the keys pi
1681
+ * computed for the call. Nothing else from the process env gets through.
1682
+ */
1683
+ export function buildPiShellEnv(
1684
+ sessionEnv: Record<string, string | undefined>,
1685
+ spawnEnv: BashSpawnContext["env"],
1686
+ ): BashSpawnContext["env"] {
1687
+ const env: BashSpawnContext["env"] = { ...sessionEnv };
1688
+ for (const key of PI_SPAWN_ENV_KEYS) {
1689
+ if (spawnEnv[key] !== undefined) env[key] = spawnEnv[key];
1690
+ else if (key !== "PATH") delete env[key];
1691
+ }
1692
+ return env;
1693
+ }
1694
+
1507
1695
  /**
1508
1696
  * Builds and loads the resource loader for a pi SDK session.
1509
1697
  *
@@ -1627,19 +1815,53 @@ export class PiMonoAdapter implements ProviderAdapter {
1627
1815
 
1628
1816
  const sessionEnv = config.env ?? process.env;
1629
1817
 
1818
+ // A key picked from a pool for this task lives only in sessionEnv, so the
1819
+ // scrubber's process.env pass cannot see it. Register it before anything
1820
+ // (a provider error that echoes it, a log line) can reach an egress.
1821
+ registerEnvSecrets(sessionEnv);
1822
+
1630
1823
  // 3. Resolve model
1631
- // Write the gateway override BEFORE ModelRuntime.create — the runtime
1632
- // loads `<agentDir>/models.json` exactly once at creation.
1633
- await ensureOpenRouterModelsOverride(getAgentDir(), sessionEnv);
1634
- const builtinModel = resolveModel(config.model, sessionEnv);
1635
- const modelRuntime = await createPiRuntimeAuth(sessionEnv);
1824
+ // This session's own models.json: the shared file plus the gateway and
1825
+ // the registry's custom provider blocks from sessionEnv. Written BEFORE
1826
+ // ModelRuntime.create, which loads it exactly once. Nothing writes the
1827
+ // shared file, so concurrent sessions never read each other's endpoints.
1828
+ const sessionModels = await writeSessionModelsJson(getAgentDir(), sessionEnv, config.model);
1829
+ try {
1830
+ return await this.createSessionWithModels(config, sessionEnv, sessionModels, {
1831
+ createdSymlink,
1832
+ customTools,
1833
+ deferTools,
1834
+ piMcpServers,
1835
+ });
1836
+ } catch (err) {
1837
+ await sessionModels.dispose();
1838
+ throw err;
1839
+ }
1840
+ }
1841
+
1842
+ private async createSessionWithModels(
1843
+ config: ProviderSessionConfig,
1844
+ sessionEnv: Record<string, string | undefined>,
1845
+ sessionModels: SessionModelsJson,
1846
+ setup: {
1847
+ createdSymlink: boolean;
1848
+ customTools: ToolDefinition[];
1849
+ deferTools: boolean;
1850
+ piMcpServers: Record<string, McpServerConfig>;
1851
+ },
1852
+ ): Promise<ProviderSession> {
1853
+ const { createdSymlink, customTools, deferTools, piMcpServers } = setup;
1854
+ // The runtime is created before resolving so custom providers, which only
1855
+ // exist in its composed store, can resolve.
1856
+ const modelRuntime = await createPiRuntimeAuth(sessionEnv, sessionModels.modelsPath);
1857
+ const resolvedModel = resolveModel(config.model, sessionEnv, modelRuntime);
1636
1858
  // models.json overrides (e.g. the OpenRouter gateway baseUrl) only apply
1637
1859
  // to models resolved through the runtime's composed store; the builtin
1638
1860
  // catalog object from `resolveModel` carries the hardcoded upstream URL
1639
1861
  // and would be used verbatim by the session.
1640
- const model = builtinModel
1641
- ? (modelRuntime.getModel(builtinModel.provider, builtinModel.id) ?? builtinModel)
1642
- : builtinModel;
1862
+ const model = resolvedModel
1863
+ ? (modelRuntime.getModel(resolvedModel.provider, resolvedModel.id) ?? resolvedModel)
1864
+ : resolvedModel;
1643
1865
 
1644
1866
  // 4. Create swarm hooks extension
1645
1867
  const swarmExtension = createSwarmHooksExtension({
@@ -1684,10 +1906,27 @@ export class PiMonoAdapter implements ProviderAdapter {
1684
1906
  reasoningApplication.kind === "pi-session" ? reasoningApplication.sessionOptions : {};
1685
1907
  const appliedReasoningEffort =
1686
1908
  reasoningApplication.kind === "pi-session" ? (config.reasoningEffort ?? null) : null;
1909
+ // pi's built-in Bash tool spawns with a copy of process.env, which is
1910
+ // shared by every concurrent session and never sees this task's env (rows
1911
+ // admitted or withheld by fetchResolvedEnv after boot). A custom tool
1912
+ // named "bash" replaces the built-in, so every shell call, including
1913
+ // codemode's executeTool("bash"), runs with this session's env.
1914
+ const bashTool = defineTool(
1915
+ createBashToolDefinition(config.cwd, {
1916
+ commandPrefix: settingsManager.getShellCommandPrefix(),
1917
+ shellPath: settingsManager.getShellPath(),
1918
+ spawnHook: (context) => ({ ...context, env: buildPiShellEnv(sessionEnv, context.env) }),
1919
+ }),
1920
+ );
1921
+ // pi's own default session manager (same session dir), with its writes
1922
+ // to `sessions/*.jsonl` scrubbed.
1923
+ const sessionManager = SessionManager.create(config.cwd);
1924
+ scrubPiSessionPersistence(sessionManager);
1687
1925
  const sessionOptions: CreateAgentSessionOptions = {
1688
1926
  cwd: config.cwd,
1927
+ sessionManager,
1689
1928
  model,
1690
- customTools,
1929
+ customTools: [...customTools, bashTool],
1691
1930
  resourceLoader,
1692
1931
  settingsManager,
1693
1932
  modelRuntime,
@@ -1700,7 +1939,13 @@ export class PiMonoAdapter implements ProviderAdapter {
1700
1939
  const { session } = await createAgentSession(sessionOptions);
1701
1940
  await session.bindExtensions({});
1702
1941
 
1703
- return new PiMonoSession(session, config, createdSymlink, appliedReasoningEffort);
1942
+ return new PiMonoSession(
1943
+ session,
1944
+ config,
1945
+ createdSymlink,
1946
+ appliedReasoningEffort,
1947
+ sessionModels.dispose,
1948
+ );
1704
1949
  }
1705
1950
 
1706
1951
  async canResume(sessionId: string): Promise<boolean> {