@desplega.ai/agent-swarm 2.5.4 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (354) hide show
  1. package/README.md +4 -2
  2. package/dist/{acp-adapter-ek38cwsv.js → acp-adapter-ma1js5n7.js} +9 -9
  3. package/dist/{actions-92mepq03.js → actions-1vhmtvg9.js} +21 -21
  4. package/dist/{amp-adapter-9gaxg015.js → amp-adapter-sxqqvmdk.js} +4 -4
  5. package/dist/{app-ctn8gsgx.js → app-mwx1d5a9.js} +3 -3
  6. package/dist/{assistant-zyr5kz47.js → assistant-yx1p6g82.js} +30 -26
  7. package/dist/{boot-reembed-w75maga2.js → boot-reembed-5h37a2c2.js} +1 -1
  8. package/dist/{boot-reembed-dwzfttdr.js → boot-reembed-7h78vmvv.js} +2 -1
  9. package/dist/{boot-scrub-sweep-e9kq43kf.js → boot-scrub-sweep-8mawd4gw.js} +4 -4
  10. package/dist/{boot-seeds-x28v3fhb.js → boot-seeds-ca84dhnn.js} +4 -4
  11. package/dist/{claude-adapter-f0afqnne.js → claude-adapter-wqt4hfkq.js} +9 -9
  12. package/dist/{claude-managed-adapter-x81a7r65.js → claude-managed-adapter-6fjqtr2z.js} +2 -2
  13. package/dist/{claude-managed-setup-bqzwjd9x.js → claude-managed-setup-rqv98sea.js} +10 -3
  14. package/dist/{claude-sdk-session-7b7whkaq.js → claude-sdk-session-f29f3b2x.js} +11 -11
  15. package/dist/{cli-sxgtf5fw.js → cli-07q03xm2.js} +1 -1
  16. package/dist/{cli-kxpfx2xr.js → cli-0z1xjzwa.js} +2 -2
  17. package/dist/{cli-g9vd9ece.js → cli-1hhbzkbb.js} +14 -16
  18. package/dist/{cli-qqzmjxkf.js → cli-1m9x8dr5.js} +11 -6
  19. package/dist/{cli-jbnn4vxb.js → cli-2pvzmas1.js} +3 -3
  20. package/dist/{cli-x77gen2p.js → cli-2y7v133v.js} +7 -7
  21. package/dist/{cli-dew6x255.js → cli-39aet655.js} +2 -2
  22. package/dist/{cli-g1cw7xtt.js → cli-39rrw8yp.js} +1 -1
  23. package/dist/{cli-jb30dzvk.js → cli-48gxjmcz.js} +530 -190
  24. package/dist/{cli-pzatvw94.js → cli-54sj64fc.js} +2 -2
  25. package/dist/{cli-bvpgr9ny.js → cli-60sty27p.js} +5 -5
  26. package/dist/{cli-asq5ydm4.js → cli-6a9qfa8b.js} +411 -69
  27. package/dist/{cli-277c31dw.js → cli-6mgdba42.js} +225 -3
  28. package/dist/{cli-19202kew.js → cli-6srv653c.js} +5 -5
  29. package/dist/{cli-n4c8e3s5.js → cli-708wdy32.js} +1 -1
  30. package/dist/{cli-pasyvjmm.js → cli-737pyqxr.js} +1 -1
  31. package/dist/{cli-6x9ma5w5.js → cli-803188pb.js} +1 -1
  32. package/dist/{cli-hh5we352.js → cli-86ehn4s5.js} +7 -7
  33. package/dist/{cli-9s05422w.js → cli-8mbc9za9.js} +1 -1
  34. package/dist/{cli-ff1tm9n9.js → cli-8wmj18cz.js} +1 -1
  35. package/dist/{cli-1r5ad7j5.js → cli-9gsm2rzp.js} +12 -12
  36. package/dist/{cli-mz9afxxf.js → cli-9pshx6rd.js} +17 -3
  37. package/dist/{cli-s682zgqz.js → cli-a6ds2epe.js} +1 -1
  38. package/dist/{cli-6a1xg1p7.js → cli-a89zdqz9.js} +2 -2
  39. package/dist/{cli-03vpq45j.js → cli-ay40dnj3.js} +2 -2
  40. package/dist/{cli-tsma7p8v.js → cli-aypd09nf.js} +2 -2
  41. package/dist/{cli-ethgy7ha.js → cli-b3yzyc57.js} +2 -2
  42. package/dist/{cli-bn0rewsy.js → cli-c081bj3b.js} +2 -2
  43. package/dist/{cli-mmhj8vd7.js → cli-c668vvcf.js} +11 -2
  44. package/dist/{cli-sbwsxvtd.js → cli-d20qvpr2.js} +2 -2
  45. package/dist/{cli-c952c7ac.js → cli-e1595bgx.js} +3 -3
  46. package/dist/{cli-621ce7kw.js → cli-f12mr20z.js} +3 -3
  47. package/dist/{cli-dqygeeh5.js → cli-f1e0ptjj.js} +10 -1
  48. package/dist/{cli-xzfq4at3.js → cli-f4dbh7a5.js} +16 -12
  49. package/dist/{cli-ha9rmwgq.js → cli-fb8m699c.js} +2 -2
  50. package/dist/{cli-5bnsc0e1.js → cli-gsfk58nw.js} +5 -5
  51. package/dist/cli-gt4h8cxw.js +895 -0
  52. package/dist/{cli-nwyw3zhg.js → cli-gxee0bqa.js} +1 -1
  53. package/dist/{cli-25n3mv5d.js → cli-h5jeg46p.js} +6 -6
  54. package/dist/{cli-gn5af0x5.js → cli-he9gbdx0.js} +62 -12
  55. package/dist/{cli-gbfxp48k.js → cli-j0ppywdr.js} +42 -35
  56. package/dist/{cli-k4tjsdx9.js → cli-j3y0x2n4.js} +277 -199
  57. package/dist/{cli-7p4jxgtt.js → cli-j9bpmykh.js} +75 -27
  58. package/dist/{cli-dabehxhp.js → cli-kt907da6.js} +1 -1
  59. package/dist/{cli-wx2nj19m.js → cli-kv7sbc2c.js} +6 -6
  60. package/dist/{cli-abs8ndtr.js → cli-m30k9777.js} +26 -26
  61. package/dist/{cli-cz5nmdxt.js → cli-m5e802r7.js} +2 -2
  62. package/dist/{cli-0shx9j93.js → cli-pfk92eqz.js} +2 -2
  63. package/dist/{cli-xtrz6bzs.js → cli-ptfq202x.js} +5 -3
  64. package/dist/{cli-6n8axjv1.js → cli-qj442fws.js} +2 -2
  65. package/dist/{cli-c61mbdds.js → cli-rd5m70gj.js} +4 -4
  66. package/dist/{cli-eftzyh9y.js → cli-s09j9vp5.js} +2 -2
  67. package/dist/{cli-m4ecdxvz.js → cli-s0mgx214.js} +7 -7
  68. package/dist/{cli-wghjkr0e.js → cli-sgqm2582.js} +10 -7
  69. package/dist/{cli-2655k16q.js → cli-sjctjr48.js} +2 -2
  70. package/dist/{cli-nzfnt2gs.js → cli-tf1n0r0b.js} +1 -1
  71. package/dist/{cli-x0f2v69c.js → cli-twctnkng.js} +12 -7
  72. package/dist/{cli-j6rktc91.js → cli-v6shg7na.js} +1 -1
  73. package/dist/{cli-gx4rm0qx.js → cli-vz78s5wm.js} +1 -1
  74. package/dist/{cli-gwhjmh8g.js → cli-wqr7cea4.js} +5 -5
  75. package/dist/{cli-e42vss7b.js → cli-x37r7qwf.js} +16 -8
  76. package/dist/{cli-4b2z6n0m.js → cli-yd2nreqw.js} +17 -8
  77. package/dist/{cli-64mh9mf3.js → cli-ymjpkxgn.js} +69 -17
  78. package/dist/{cli-5kqnemen.js → cli-yvy48785.js} +1 -1
  79. package/dist/{cli-9j129brq.js → cli-yxbsmx56.js} +6 -6
  80. package/dist/cli.js +22 -20
  81. package/dist/{codex-adapter-wch12q99.js → codex-adapter-zk59x2zh.js} +7 -7
  82. package/dist/{codex-hook-brpnycn5.js → codex-hook-xyjhvxwd.js} +1 -1
  83. package/dist/{codex-login-rn7c5gxt.js → codex-login-8cg0vhf2.js} +1 -1
  84. package/dist/{codex-session-runner-41pfcr95.js → codex-session-runner-kh6s4j3n.js} +8 -8
  85. package/dist/{commands-9849dkj6.js → commands-68et1qv2.js} +2 -2
  86. package/dist/{controller-ja0sd95p.js → controller-fyrbnzg4.js} +4 -4
  87. package/dist/{controllers-connect-xp05dj6x.js → controllers-connect-nep9h2xv.js} +23 -23
  88. package/dist/{cursor-adapter-623qxqew.js → cursor-adapter-yss3pg7t.js} +2 -2
  89. package/dist/{db-w759qmx0.js → db-28gjyvxj.js} +4 -2
  90. package/dist/{db-47vj40dy.js → db-8ftwvzba.js} +3 -3
  91. package/dist/{devin-adapter-4ycy4vrf.js → devin-adapter-byen2cxt.js} +1 -1
  92. package/dist/{dispatcher-7b8e4z2j.js → dispatcher-hayexmkr.js} +20 -20
  93. package/dist/{docker-s84hjs91.js → docker-fdzyewpz.js} +2 -2
  94. package/dist/{dsh-adapter-2ezw3xea.js → dsh-adapter-46wawff9.js} +2 -2
  95. package/dist/{e2b-9gkdhm3c.js → e2b-222hsajc.js} +2 -2
  96. package/dist/{e2b-cjtf8k1z.js → e2b-fn3zacvt.js} +2 -2
  97. package/dist/{easter-egg-3d-e4qv8dxj.js → easter-egg-3d-8d1e0h4m.js} +1 -1
  98. package/dist/{event-bus-3z78ep5n.js → event-bus-pghg8hqy.js} +1 -1
  99. package/dist/{execute-0xpz8950.js → execute-cjrfxfnm.js} +1 -1
  100. package/dist/{external-channel-e2s028da.js → external-channel-xbmxcg09.js} +2 -2
  101. package/dist/{grok-adapter-ys7gaqee.js → grok-adapter-ag4wks2a.js} +10 -10
  102. package/dist/handlers-tzmevqb5.js +41 -0
  103. package/dist/{heartbeat-w3mntghj.js → heartbeat-psfvcgwp.js} +23 -23
  104. package/dist/{hook-g7kvv6x9.js → hook-8pz04697.js} +6 -6
  105. package/dist/{hook-3jjywnpb.js → hook-cyq4mryn.js} +8 -8
  106. package/dist/{http-tkp7d4kb.js → http-haj1nd6k.js} +921 -169
  107. package/dist/{index-q7m26kvs.js → index-jwt2bj6f.js} +265 -97
  108. package/dist/{index-3n8m9ea2.js → index-q52f47pe.js} +1 -1
  109. package/dist/{index-31ceyf42.js → index-syvhnvtw.js} +1 -1
  110. package/dist/index-y1ajx04a.js +43 -0
  111. package/dist/{index-1ef1myq2.js → index-z5h22pes.js} +20 -20
  112. package/dist/{index-xdaahthb.js → index-zyq8ct52.js} +20 -20
  113. package/dist/{keepalive-v4ta8wgg.js → keepalive-xdvsamvj.js} +4 -4
  114. package/dist/lead-hqg57kdp.js +79 -0
  115. package/dist/{maintenance-84n1ex21.js → maintenance-4y2xzh3a.js} +1 -1
  116. package/dist/{oauth-refresh-sweep-vjnnandw.js → oauth-refresh-sweep-jd4r7zbs.js} +9 -4
  117. package/dist/{onboard-sfmpe4np.js → onboard-18447q9q.js} +6 -6
  118. package/dist/{opencode-adapter-jmpst4q4.js → opencode-adapter-26b2frdf.js} +7 -7
  119. package/dist/{otel-impl-v6cc0zve.js → otel-impl-f9tgr8yw.js} +2 -2
  120. package/dist/{pi-mono-adapter-fk8fkcf6.js → pi-mono-adapter-qn6qbga1.js} +38 -17
  121. package/dist/{pricing-refresh-fsk1e1zc.js → pricing-refresh-dw2tgb53.js} +3 -3
  122. package/dist/{rbac-roles-prmkdnkw.js → rbac-roles-aygm2760.js} +2 -2
  123. package/dist/{rbac-roles-zxmxqa39.js → rbac-roles-s4nwj2m3.js} +1 -1
  124. package/dist/render-v2-yfvdfr0x.js +16 -0
  125. package/dist/{resume-frdtgfa0.js → resume-1bmvjbf3.js} +9 -9
  126. package/dist/{runtime-bt5pkpm1.js → runtime-f7tpzedy.js} +1 -1
  127. package/dist/{session-access-fpez4y5q.js → session-access-r5garej6.js} +4 -4
  128. package/dist/session-source-backfill-424wzv0r.js +110 -0
  129. package/dist/{setup-mq6m3m8q.js → setup-8tygvxkq.js} +1 -1
  130. package/dist/{virtual-modules-mnfbh5mr.js → virtual-modules-xsw323b3.js} +2 -2
  131. package/dist/{work-objects-kfahhjsq.js → work-objects-ggnghpgf.js} +23 -23
  132. package/dist/worker-sqjd1nqf.js +79 -0
  133. package/dist/{x-pbpezr0s.js → x-wxrzvzx8.js} +2 -2
  134. package/openapi.json +1324 -14
  135. package/package.json +5 -3
  136. package/src/assets/source-key.ts +95 -0
  137. package/src/be/automation-preflight.ts +2 -0
  138. package/src/be/db/api-keys.ts +89 -0
  139. package/src/be/db/session-labels.ts +28 -0
  140. package/src/be/db/session-moves.ts +5 -6
  141. package/src/be/db/session-tree.ts +153 -14
  142. package/src/be/db/tasks/read.ts +11 -0
  143. package/src/be/db/tasks/write.ts +6 -1
  144. package/src/be/db/workflow-runs.ts +219 -6
  145. package/src/be/db-retention.ts +61 -6
  146. package/src/be/db.ts +135 -132
  147. package/src/be/memory/providers/openai-embedding.ts +5 -1
  148. package/src/be/memory/task-learning.ts +155 -0
  149. package/src/be/migrations/244_api_key_validation.sql +4 -0
  150. package/src/be/migrations/245_script_executor.sql +3 -0
  151. package/src/be/oauth-refresh-sweep.ts +2 -1
  152. package/src/be/onboarding.ts +4 -0
  153. package/src/be/scripts/boot-reembed.ts +2 -0
  154. package/src/be/scripts/db.ts +14 -4
  155. package/src/be/scripts/embeddings.ts +2 -0
  156. package/src/be/scripts/run-saved.ts +1 -0
  157. package/src/be/scripts/typecheck.ts +10 -4
  158. package/src/be/seed-skills/index.ts +7 -0
  159. package/src/be/session-source-backfill.ts +163 -0
  160. package/src/be/swarm-config-guard.ts +1 -0
  161. package/src/comb/review-batch.ts +28 -1
  162. package/src/comb/templates.ts +7 -2
  163. package/src/commands/claude-managed-setup.ts +7 -1
  164. package/src/commands/credential-refresh.ts +76 -1
  165. package/src/commands/credential-wait.ts +2 -2
  166. package/src/commands/onboard/compose-generator.ts +5 -3
  167. package/src/commands/provider-credentials.ts +212 -171
  168. package/src/commands/runner.ts +25 -4
  169. package/src/extensions/apply-task-create.ts +8 -1
  170. package/src/extensions/contract-types.generated.ts +1 -1
  171. package/src/extensions/contract.ts +5 -0
  172. package/src/heartbeat/heartbeat.ts +14 -1
  173. package/src/heartbeat/script-runner-probe.ts +47 -0
  174. package/src/http/all-routes.ts +2 -0
  175. package/src/http/api-keys.ts +77 -9
  176. package/src/http/assets.ts +15 -0
  177. package/src/http/codex-oauth-accounts.ts +191 -0
  178. package/src/http/comb.ts +15 -2
  179. package/src/http/config.ts +1 -1
  180. package/src/http/core.ts +6 -0
  181. package/src/http/fleet-benchmark.ts +5 -2
  182. package/src/http/idempotency-key.ts +1 -1
  183. package/src/http/index.ts +14 -3
  184. package/src/http/integrations.ts +5 -1
  185. package/src/http/kv.ts +8 -3
  186. package/src/http/onboarding.ts +6 -1
  187. package/src/http/pages-public.ts +38 -5
  188. package/src/http/pool-keys.ts +354 -0
  189. package/src/http/scripts.ts +27 -0
  190. package/src/http/sessions.ts +37 -12
  191. package/src/http/stats.ts +1 -0
  192. package/src/http/status.ts +26 -12
  193. package/src/http/tasks.ts +3 -0
  194. package/src/http/used-by.ts +7 -2
  195. package/src/http/workflows.ts +88 -9
  196. package/src/http/x.ts +5 -0
  197. package/src/integrations/chat-sdk/inbound.ts +27 -0
  198. package/src/kv-reserved-namespaces.ts +19 -0
  199. package/src/prompts/base-prompt.ts +21 -1
  200. package/src/prompts/session-templates.ts +17 -3
  201. package/src/providers/acp-targets.ts +1 -1
  202. package/src/providers/amp-adapter.ts +1 -1
  203. package/src/providers/claude-adapter.ts +5 -1
  204. package/src/providers/claude-managed-adapter.ts +16 -9
  205. package/src/providers/codex-adapter.ts +71 -11
  206. package/src/providers/dsh-adapter.ts +1 -1
  207. package/src/providers/pi-mono-adapter.ts +41 -11
  208. package/src/scheduler/scheduler.ts +5 -3
  209. package/src/scripts-runtime/eval-harness.ts +8 -1
  210. package/src/scripts-runtime/executors/native.ts +22 -16
  211. package/src/scripts-runtime/executors/quickjs-prelude.ts +103 -1
  212. package/src/scripts-runtime/executors/quickjs-runner.ts +2 -1
  213. package/src/scripts-runtime/executors/quickjs.ts +7 -0
  214. package/src/scripts-runtime/executors/registry.ts +32 -0
  215. package/src/scripts-runtime/executors/types.ts +7 -0
  216. package/src/scripts-runtime/host-validated-args.ts +133 -0
  217. package/src/scripts-runtime/loader.ts +30 -3
  218. package/src/scripts-runtime/swarm-sdk.ts +16 -3
  219. package/src/scripts-runtime/types/stdlib.d.ts +10 -2
  220. package/src/scripts-runtime/types/swarm-extension.d.ts +17 -8
  221. package/src/scripts-runtime/types/swarm-sdk.d.ts +10 -2
  222. package/src/slack/assistant.ts +7 -0
  223. package/src/slack/channel-labels.ts +74 -0
  224. package/src/slack/handlers.ts +8 -0
  225. package/src/slack/inbound-files.ts +5 -2
  226. package/src/slack/thread-buffer.ts +4 -0
  227. package/src/tasks/sibling-awareness.ts +12 -3
  228. package/src/telemetry-context.ts +5 -6
  229. package/src/telemetry-identity.ts +3 -3
  230. package/src/tests/api-key-status-by-suffix.test.ts +242 -0
  231. package/src/tests/api-keys-validation.test.ts +170 -0
  232. package/src/tests/base-prompt.test.ts +70 -2
  233. package/src/tests/claude-managed-adapter.test.ts +46 -0
  234. package/src/tests/claude-routes-e2e.test.ts +7 -2
  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/community-linear-delivery-loop.test.ts +2099 -0
  240. package/src/tests/config-pool-keys.test.ts +533 -0
  241. package/src/tests/credential-check.test.ts +294 -4
  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/docs-index.test.ts +135 -0
  248. package/src/tests/dsh-adapter.test.ts +1 -1
  249. package/src/tests/extensions-pre-task.test.ts +53 -1
  250. package/src/tests/fetch-resolved-env.test.ts +16 -2
  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/fleet-benchmark.test.ts +8 -0
  254. package/src/tests/get-workflow-run-tool.test.ts +98 -0
  255. package/src/tests/heartbeat-checklist.test.ts +89 -0
  256. package/src/tests/heartbeat-script-runner-probe.test.ts +83 -0
  257. package/src/tests/integrations-http.test.ts +32 -2
  258. package/src/tests/internal-ai/complete-structured.test.ts +37 -1
  259. package/src/tests/internal-ai/credentials.test.ts +23 -0
  260. package/src/tests/kv-http.test.ts +32 -0
  261. package/src/tests/kv-tool.test.ts +25 -0
  262. package/src/tests/multi-runtime-registration.test.ts +12 -12
  263. package/src/tests/oauth-refresh-sweep.test.ts +34 -1
  264. package/src/tests/onboard-compose.test.ts +20 -0
  265. package/src/tests/onboarding.test.ts +108 -1
  266. package/src/tests/openai-embedding-credentials.test.ts +35 -0
  267. package/src/tests/opencode-adapter.test.ts +2 -1
  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/pages-frame-ancestors.test.ts +162 -0
  271. package/src/tests/pi-codemode.test.ts +2 -1
  272. package/src/tests/pi-mono-adapter.test.ts +54 -0
  273. package/src/tests/pi-native-mcp.test.ts +13 -1
  274. package/src/tests/pi-tool-exposure.test.ts +8 -8
  275. package/src/tests/probe-tool-schema-tokens.test.ts +144 -0
  276. package/src/tests/prompt-template-session.test.ts +4 -3
  277. package/src/tests/provider-key-probes.test.ts +181 -0
  278. package/src/tests/rbac-e2e-helpers.test.ts +88 -1
  279. package/src/tests/rbac-e2e-helpers.ts +36 -11
  280. package/src/tests/request-composition-derive.test.ts +329 -0
  281. package/src/tests/script-connections.test.ts +4 -0
  282. package/src/tests/script-executor-conformance.test.ts +36 -1
  283. package/src/tests/script-executor-quickjs-pool.test.ts +50 -0
  284. package/src/tests/script-executor-quickjs.test.ts +140 -0
  285. package/src/tests/script-host-validated-args-native.test.ts +196 -0
  286. package/src/tests/script-per-script-executor.test.ts +415 -0
  287. package/src/tests/script-runs-retention.test.ts +212 -0
  288. package/src/tests/scripts-external-api.test.ts +45 -0
  289. package/src/tests/sdk-allowlist.test.ts +42 -2
  290. package/src/tests/self-improvement.test.ts +4 -4
  291. package/src/tests/session-source-backfill.test.ts +429 -0
  292. package/src/tests/session-source-keys.test.ts +236 -0
  293. package/src/tests/session-source-labels.test.ts +419 -0
  294. package/src/tests/sessions-detail-batch.test.ts +2 -0
  295. package/src/tests/sessions-move.test.ts +131 -0
  296. package/src/tests/sessions-tree.test.ts +299 -2
  297. package/src/tests/slack-inbound-files.test.ts +103 -23
  298. package/src/tests/status.test.ts +101 -18
  299. package/src/tests/store-progress-learning.test.ts +288 -0
  300. package/src/tests/tasks-workflow-run-filter.test.ts +146 -0
  301. package/src/tests/test-net.ts +31 -0
  302. package/src/tests/transcription.test.ts +8 -0
  303. package/src/tests/ui-logs-parser.test.ts +180 -0
  304. package/src/tests/used-by-route.test.ts +11 -0
  305. package/src/tests/workflow-cascade-cancel.test.ts +172 -0
  306. package/src/tests/workflow-engine-v2.test.ts +116 -1
  307. package/src/tests/workflow-http-v2.test.ts +131 -0
  308. package/src/tests/workflow-mcp-trigger-schema.test.ts +209 -0
  309. package/src/tests/workflow-run-finished-event.test.ts +263 -0
  310. package/src/tests/workflow-step-order.test.ts +92 -0
  311. package/src/tests/workflow-sub-workflow.test.ts +30 -0
  312. package/src/tests/workflow-swarm-script.test.ts +34 -0
  313. package/src/tools/get-tasks.ts +8 -0
  314. package/src/tools/kv/kv-read-auth.ts +4 -2
  315. package/src/tools/script-common.ts +1 -0
  316. package/src/tools/script-run.ts +7 -0
  317. package/src/tools/script-upsert.ts +7 -0
  318. package/src/tools/store-progress.ts +67 -1
  319. package/src/tools/task-action.ts +1 -1
  320. package/src/tools/workflows/get-workflow-run.ts +14 -4
  321. package/src/tools/workflows/trigger-workflow.ts +104 -10
  322. package/src/types.ts +36 -1
  323. package/src/utils/company-profile.ts +51 -1
  324. package/src/utils/credentials.ts +10 -4
  325. package/src/utils/internal-ai/complete-structured.ts +5 -0
  326. package/src/utils/internal-ai/credentials.ts +4 -3
  327. package/src/utils/provider-key-probes.ts +328 -0
  328. package/src/utils/transcription.ts +4 -1
  329. package/src/workflows/engine.ts +18 -16
  330. package/src/workflows/executors/sub-workflow.ts +8 -1
  331. package/src/workflows/executors/swarm-script.ts +5 -0
  332. package/src/workflows/foreach-driver.ts +5 -11
  333. package/src/workflows/node-timeout.ts +30 -0
  334. package/templates/ai-toolbox.manifest.json +2 -2
  335. package/templates/community/linear-delivery-loop/PLAYBOOK.md +323 -0
  336. package/templates/community/linear-delivery-loop/README.md +38 -0
  337. package/templates/community/linear-delivery-loop/config.example.json +31 -0
  338. package/templates/community/linear-delivery-loop/lead-prompt.md +34 -0
  339. package/templates/community/linear-delivery-loop/schedule.json +10 -0
  340. package/templates/community/linear-delivery-loop/scripts/linear-delivery-setup.ts +148 -0
  341. package/templates/community/linear-delivery-loop/scripts/linear-delivery-tick.ts +1781 -0
  342. package/templates/skills/researching/content.md +2 -0
  343. package/templates/skills/swarm-docs/config.json +14 -0
  344. package/templates/skills/swarm-docs/content.md +144 -0
  345. package/templates/skills/swarm-scripts/SKILL.md +1 -0
  346. package/templates/skills/swarm-scripts/content.md +1 -0
  347. package/dist/cli-7421jzsw.js +0 -84
  348. package/dist/cli-e45e4m6b.js +0 -413
  349. package/dist/cli-zw88zjba.js +0 -307
  350. package/dist/handlers-wy1dr6ja.js +0 -41
  351. package/dist/index-nmm79qgr.js +0 -43
  352. package/dist/lead-sawnyqxk.js +0 -79
  353. package/dist/render-v2-pjwxvnkw.js +0 -12
  354. package/dist/worker-v5e5rxj3.js +0 -79
@@ -0,0 +1,162 @@
1
+ /**
2
+ * `PAGES_FRAME_ANCESTORS` widens `frame-ancestors` on `/p/:id` for PUBLIC
3
+ * pages only, so an outside site (for example a blog) can iframe a chart.
4
+ * Authed and password pages keep the app-origin-only header, and an unset
5
+ * env leaves every header unchanged.
6
+ */
7
+ import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test";
8
+ import crypto from "node:crypto";
9
+ import { unlink } from "node:fs/promises";
10
+ import {
11
+ createServer as createHttpServer,
12
+ type IncomingMessage,
13
+ type Server,
14
+ type ServerResponse,
15
+ } from "node:http";
16
+ import { closeDb, initDb } from "../be/db";
17
+ import { handlePages } from "../http/pages";
18
+ import { getPageFrameAncestors, handlePagesPublic } from "../http/pages-public";
19
+ import { getPathSegments, parseQueryParams } from "../http/utils";
20
+ import { signPageSession } from "../utils/page-session";
21
+ import { listenOnFreePort } from "./test-net";
22
+
23
+ const TEST_DB_PATH = "./test-pages-frame-ancestors.sqlite";
24
+ const EMBEDDERS = "https://www.agent-swarm.dev https://agent-swarm.dev";
25
+ let BASE = "";
26
+
27
+ function createTestServer(): Server {
28
+ return createHttpServer(async (req: IncomingMessage, res: ServerResponse) => {
29
+ const pathSegments = getPathSegments(req.url || "");
30
+ const queryParams = parseQueryParams(req.url || "");
31
+ const myAgentId = req.headers["x-agent-id"] as string | undefined;
32
+ if (await handlePagesPublic(req, res, pathSegments, queryParams)) return;
33
+ if (await handlePages(req, res, pathSegments, queryParams, myAgentId)) return;
34
+ res.writeHead(404);
35
+ res.end("not found");
36
+ });
37
+ }
38
+
39
+ function frameAncestors(res: Response): string {
40
+ const csp = res.headers.get("content-security-policy") ?? "";
41
+ return csp.split(";").find((d) => d.trim().startsWith("frame-ancestors ")) ?? "";
42
+ }
43
+
44
+ describe("PAGES_FRAME_ANCESTORS on /p/:id", () => {
45
+ let server: Server;
46
+ const agentId = crypto.randomUUID();
47
+ const headers = { "Content-Type": "application/json", "X-Agent-ID": agentId };
48
+ const ORIG = {
49
+ PAGES_FRAME_ANCESTORS: process.env.PAGES_FRAME_ANCESTORS,
50
+ APP_URL: process.env.APP_URL,
51
+ DASHBOARD_URL: process.env.DASHBOARD_URL,
52
+ PAGE_SESSION_SECRET: process.env.PAGE_SESSION_SECRET,
53
+ };
54
+
55
+ function restore(key: keyof typeof ORIG): void {
56
+ if (ORIG[key] === undefined) delete process.env[key];
57
+ else process.env[key] = ORIG[key];
58
+ }
59
+
60
+ async function createPage(
61
+ slug: string,
62
+ authMode: "public" | "authed" | "password",
63
+ ): Promise<string> {
64
+ const post = await fetch(`${BASE}/api/pages`, {
65
+ method: "POST",
66
+ headers,
67
+ body: JSON.stringify({
68
+ slug,
69
+ title: slug,
70
+ contentType: "text/html",
71
+ authMode,
72
+ ...(authMode === "password" ? { password: "swordfish" } : {}),
73
+ body: "<!doctype html><html><head></head><body><h1>chart</h1></body></html>",
74
+ }),
75
+ });
76
+ expect(post.status).toBe(201);
77
+ const { id } = (await post.json()) as { id: string };
78
+ return id;
79
+ }
80
+
81
+ beforeAll(async () => {
82
+ process.env.PAGE_SESSION_SECRET = "example-test-frame-ancestors-secret";
83
+ process.env.APP_URL = "https://app.example.test";
84
+ delete process.env.DASHBOARD_URL;
85
+ for (const suffix of ["", "-wal", "-shm"]) {
86
+ try {
87
+ await unlink(`${TEST_DB_PATH}${suffix}`);
88
+ } catch {}
89
+ }
90
+ initDb(TEST_DB_PATH);
91
+ server = createTestServer();
92
+ const port = await listenOnFreePort(server);
93
+ BASE = `http://localhost:${port}`;
94
+ });
95
+
96
+ afterEach(() => restore("PAGES_FRAME_ANCESTORS"));
97
+
98
+ afterAll(async () => {
99
+ await new Promise<void>((resolve) => server.close(() => resolve()));
100
+ closeDb();
101
+ for (const key of Object.keys(ORIG) as (keyof typeof ORIG)[]) restore(key);
102
+ for (const suffix of ["", "-wal", "-shm"]) {
103
+ try {
104
+ await unlink(`${TEST_DB_PATH}${suffix}`);
105
+ } catch {}
106
+ }
107
+ });
108
+
109
+ test("public page gets the configured origins", async () => {
110
+ process.env.PAGES_FRAME_ANCESTORS = EMBEDDERS;
111
+ const id = await createPage("public-embed", "public");
112
+ const res = await fetch(`${BASE}/p/${id}`);
113
+ expect(res.status).toBe(200);
114
+ const fa = frameAncestors(res);
115
+ expect(fa).toContain("'self'");
116
+ expect(fa).toContain("https://app.example.test");
117
+ expect(fa).toContain("https://www.agent-swarm.dev");
118
+ expect(fa).toContain("https://agent-swarm.dev");
119
+ });
120
+
121
+ test("authed page keeps the app-origin-only header", async () => {
122
+ const id = await createPage("authed-embed", "authed");
123
+ const exp = Math.floor(Date.now() / 1000) + 3600;
124
+ const cookie = `page_session_${id}=${await signPageSession({ pageId: id, exp })}`;
125
+
126
+ const before = frameAncestors(await fetch(`${BASE}/p/${id}`, { headers: { cookie } }));
127
+ process.env.PAGES_FRAME_ANCESTORS = EMBEDDERS;
128
+ const res = await fetch(`${BASE}/p/${id}`, { headers: { cookie } });
129
+ expect(res.status).toBe(200);
130
+ expect(frameAncestors(res)).toBe(before);
131
+ expect(frameAncestors(res)).not.toContain("agent-swarm.dev");
132
+ });
133
+
134
+ test("password page keeps the app-origin-only header", async () => {
135
+ process.env.PAGES_FRAME_ANCESTORS = EMBEDDERS;
136
+ const id = await createPage("password-embed", "password");
137
+ const res = await fetch(`${BASE}/p/${id}?key=swordfish`);
138
+ expect(res.status).toBe(200);
139
+ expect(frameAncestors(res)).not.toContain("agent-swarm.dev");
140
+ });
141
+
142
+ test("unset env leaves the public header unchanged", async () => {
143
+ const id = await createPage("public-unset", "public");
144
+ delete process.env.PAGES_FRAME_ANCESTORS;
145
+ const unset = frameAncestors(await fetch(`${BASE}/p/${id}`));
146
+ process.env.PAGES_FRAME_ANCESTORS = "";
147
+ const blank = frameAncestors(await fetch(`${BASE}/p/${id}`));
148
+ expect(unset).toContain("https://app.example.test");
149
+ expect(unset).not.toContain("agent-swarm.dev");
150
+ expect(blank).toBe(unset);
151
+ });
152
+
153
+ test("parser accepts comma or space lists and drops invalid entries", () => {
154
+ process.env.PAGES_FRAME_ANCESTORS =
155
+ "https://www.agent-swarm.dev/, https://*.example.com:8443 blog.example.org,* 'none' https://evil.test;script-src javascript:alert(1) https://x.test/path";
156
+ expect(getPageFrameAncestors()).toEqual([
157
+ "https://www.agent-swarm.dev",
158
+ "https://*.example.com:8443",
159
+ "blog.example.org",
160
+ ]);
161
+ });
162
+ });
@@ -104,7 +104,8 @@ describe("PiMonoAdapter.createSession — codemode", () => {
104
104
  process.env.OPENROUTER_BASE_URL = "http://127.0.0.1:9/api/v1";
105
105
  delete process.env.PI_CODEMODE;
106
106
  delete process.env.PI_CODEMODE_MODELS;
107
- delete process.env.PI_TOOL_DEFERRAL;
107
+ // Deferral off, so every swarm tool shows up as an active tool name.
108
+ process.env.PI_TOOL_DEFERRAL = "false";
108
109
  });
109
110
  afterEach(() => {
110
111
  for (const key of envKeys) {
@@ -1859,3 +1859,57 @@ describe("PiMonoSession context-bound note", () => {
1859
1859
  expect(await lateSteers([assistantTurn(usage(5000))])).toEqual([HELD_NOTE_TEXT]);
1860
1860
  });
1861
1861
  });
1862
+
1863
+ describe("PiMonoSession — per-call usage line", () => {
1864
+ function assistantEnd(content: unknown[], usage?: unknown): AgentSessionEvent {
1865
+ return {
1866
+ type: "message_end",
1867
+ message: { role: "assistant", content, stopReason: "stop", ...(usage ? { usage } : {}) },
1868
+ } as unknown as AgentSessionEvent;
1869
+ }
1870
+
1871
+ test("one call_usage raw_log line per assistant message_end with usage", async () => {
1872
+ const { events } = await runWithEvents([
1873
+ assistantEnd([{ type: "toolCall", id: "call-1", name: "bash", arguments: {} }], {
1874
+ input: 120,
1875
+ cacheRead: 9_000,
1876
+ cacheWrite: 400,
1877
+ output: 60,
1878
+ reasoning: 12,
1879
+ totalTokens: 9_580,
1880
+ }),
1881
+ {
1882
+ type: "message_end",
1883
+ message: { role: "user", content: [{ type: "text", text: "tool output" }] },
1884
+ } as unknown as AgentSessionEvent,
1885
+ // No usage (some providers omit it): no line.
1886
+ assistantEnd([{ type: "text", text: "thinking out loud" }]),
1887
+ assistantEnd([{ type: "text", text: "Done" }], { input: 5, cacheRead: 9_500, output: 3 }),
1888
+ ]);
1889
+
1890
+ const raw = events.flatMap((e) =>
1891
+ e.type === "raw_log" ? [JSON.parse(e.content) as Record<string, unknown>] : [],
1892
+ );
1893
+ const usageLines = raw.filter((line) => line.type === "call_usage");
1894
+ expect(usageLines).toEqual([
1895
+ {
1896
+ type: "call_usage",
1897
+ harness: "pi",
1898
+ usage: { input: 120, cacheRead: 9_000, cacheWrite: 400, output: 60, reasoning: 12 },
1899
+ },
1900
+ {
1901
+ type: "call_usage",
1902
+ harness: "pi",
1903
+ usage: { input: 5, cacheRead: 9_500, cacheWrite: 0, output: 3, reasoning: 0 },
1904
+ },
1905
+ ]);
1906
+ // The usage line precedes the call's own text line.
1907
+ const done = raw.findIndex(
1908
+ (line) => line.type === "assistant" && JSON.stringify(line).includes('"Done"'),
1909
+ );
1910
+ expect(done).toBeGreaterThan(0);
1911
+ expect(raw[done - 1]).toEqual(usageLines[1]);
1912
+ // Token counts only: no message content rides on the line.
1913
+ expect(JSON.stringify(usageLines)).not.toContain("tool output");
1914
+ });
1915
+ });
@@ -30,6 +30,17 @@ describe("toPiMcpServers", () => {
30
30
  });
31
31
  });
32
32
 
33
+ test("deferred maps every installed server to deferred exposure", () => {
34
+ const servers = toPiMcpServers(
35
+ {
36
+ local: { command: "context-mode" },
37
+ remote: { url: "https://mcp.example.com/mcp" },
38
+ },
39
+ { deferred: true },
40
+ );
41
+ expect(Object.values(servers).map((s) => s.exposure)).toEqual(["deferred", "deferred"]);
42
+ });
43
+
33
44
  test("no installed servers maps to an empty set", () => {
34
45
  expect(toPiMcpServers(null)).toEqual({});
35
46
  });
@@ -70,7 +81,8 @@ describe("PiMonoAdapter.createSession — native MCP for installed servers", ()
70
81
  process.env.OPENROUTER_API_KEY = "example-test-key";
71
82
  // The session prompts on creation; keep that request off the network.
72
83
  process.env.OPENROUTER_BASE_URL = "http://127.0.0.1:9/api/v1";
73
- delete process.env.PI_TOOL_DEFERRAL;
84
+ // Direct exposure, so installed tools show up as active tool names.
85
+ process.env.PI_TOOL_DEFERRAL = "false";
74
86
  mkdirSync(join(dir, "agent"), { recursive: true });
75
87
  });
76
88
  afterEach(() => {
@@ -176,8 +176,8 @@ describe("pi resource loader trust", () => {
176
176
  });
177
177
 
178
178
  describe("PI_TOOL_DEFERRAL flag", () => {
179
- test("defaults off and parses boolean literals", () => {
180
- expect(isPiToolDeferralEnabled({})).toBe(false);
179
+ test("defaults on and parses boolean literals", () => {
180
+ expect(isPiToolDeferralEnabled({})).toBe(true);
181
181
  expect(isPiToolDeferralEnabled({ PI_TOOL_DEFERRAL: "true" })).toBe(true);
182
182
  expect(isPiToolDeferralEnabled({ PI_TOOL_DEFERRAL: "1" })).toBe(true);
183
183
  expect(isPiToolDeferralEnabled({ PI_TOOL_DEFERRAL: "false" })).toBe(false);
@@ -187,9 +187,9 @@ describe("PI_TOOL_DEFERRAL flag", () => {
187
187
  "traits.hasToolSearch follows the flag live",
188
188
  withEnv("PI_TOOL_DEFERRAL", undefined, () => {
189
189
  const adapter = new PiMonoAdapter();
190
- expect(adapter.traits.hasToolSearch).toBe(false);
191
- process.env.PI_TOOL_DEFERRAL = "true";
192
190
  expect(adapter.traits.hasToolSearch).toBe(true);
191
+ process.env.PI_TOOL_DEFERRAL = "false";
192
+ expect(adapter.traits.hasToolSearch).toBe(false);
193
193
  }),
194
194
  );
195
195
  });
@@ -259,8 +259,8 @@ describe("PiMonoAdapter.createSession — tool deferral wiring", () => {
259
259
  }
260
260
 
261
261
  test(
262
- "flag on: non-core tools deferred and tool_search enabled by default",
263
- withEnv("PI_TOOL_DEFERRAL", "true", async () => {
262
+ "flag unset: non-core tools deferred and tool_search enabled by default",
263
+ withEnv("PI_TOOL_DEFERRAL", undefined, async () => {
264
264
  const opts = await captureSessionOptions();
265
265
  const tools = opts.customTools as ToolDefinition[];
266
266
  expect(Object.fromEntries(tools.map((t) => [t.name, t.exposure]))).toEqual({
@@ -274,8 +274,8 @@ describe("PiMonoAdapter.createSession — tool deferral wiring", () => {
274
274
  );
275
275
 
276
276
  test(
277
- "flag off: every tool declared and no tool_search",
278
- withEnv("PI_TOOL_DEFERRAL", undefined, async () => {
277
+ "flag false: every tool declared and no tool_search",
278
+ withEnv("PI_TOOL_DEFERRAL", "false", async () => {
279
279
  const opts = await captureSessionOptions();
280
280
  const tools = opts.customTools as ToolDefinition[];
281
281
  expect(tools.map((t) => t.exposure)).toEqual([undefined, undefined, undefined]);
@@ -0,0 +1,144 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import {
6
+ bareToolName,
7
+ controlReached,
8
+ countSwarmTools,
9
+ hasControlResult,
10
+ inputTokens,
11
+ toolNames,
12
+ turnOne,
13
+ withSessionDirs,
14
+ } from "../../scripts/probe-tool-schema-tokens";
15
+
16
+ describe("probe-tool-schema-tokens", () => {
17
+ let dir = "";
18
+ afterEach(() => {
19
+ if (dir) rmSync(dir, { recursive: true, force: true });
20
+ });
21
+
22
+ test("matches swarm tools under every harness naming", () => {
23
+ const swarm = new Set(["get_swarm", "store_progress"]);
24
+ expect(bareToolName("mcp__agent_swarm__get_swarm")).toBe("get_swarm");
25
+ expect(bareToolName("agent-swarm_store-progress")).toBe("store_progress");
26
+ expect(
27
+ countSwarmTools(["get-swarm", "mcp__agent_swarm__store_progress", "bash", "read"], swarm),
28
+ ).toBe(2);
29
+ });
30
+
31
+ test("lists tools in chat-completions and namespaced responses bodies", () => {
32
+ expect(toolNames({ tools: [{ type: "function", function: { name: "get-swarm" } }] })).toEqual([
33
+ "get-swarm",
34
+ ]);
35
+ const responses = {
36
+ input: [
37
+ {
38
+ type: "additional_tools",
39
+ tools: [{ type: "namespace", name: "functions", tools: [{ name: "exec" }] }],
40
+ },
41
+ ],
42
+ tools: [{ name: "wait" }],
43
+ };
44
+ expect(toolNames(responses).sort()).toEqual(["exec", "wait"]);
45
+ });
46
+
47
+ test("reads the last reported input token count from a stream", () => {
48
+ const sse = 'data: {"usage":null}\n\ndata: {"usage":{"prompt_tokens":90776}}\n';
49
+ expect(inputTokens(sse)).toBe(90776);
50
+ expect(inputTokens('{"response":{"usage":{"input_tokens":16629}}}')).toBe(16629);
51
+ expect(inputTokens("data: [DONE]")).toBeNull();
52
+ });
53
+
54
+ test("turn one skips side requests for other models", () => {
55
+ dir = mkdtempSync(join(tmpdir(), "probe-tool-schema-"));
56
+ writeFileSync(join(dir, "req-1.json"), JSON.stringify({ model: "openai/gpt-6-luna" }));
57
+ writeFileSync(join(dir, "res-1.txt"), '{"usage":{"prompt_tokens":453}}');
58
+ writeFileSync(
59
+ join(dir, "req-2.json"),
60
+ JSON.stringify({ model: "deepseek/deepseek-v4.1-flash", tools: [{ name: "bash" }] }),
61
+ );
62
+ writeFileSync(join(dir, "res-2.txt"), '{"usage":{"prompt_tokens":33115}}');
63
+ expect(turnOne(dir, "openrouter/deepseek/deepseek-v4.1-flash")).toEqual({
64
+ tokens: 33115,
65
+ names: ["bash"],
66
+ });
67
+ });
68
+
69
+ const found = "Found 5 agent(s) in the swarm. Requested by session: s1";
70
+
71
+ test("control passes only on a get-swarm tool output", () => {
72
+ dir = mkdtempSync(join(tmpdir(), "probe-tool-schema-"));
73
+ // The schema alone carries "in the swarm" in get-swarm's description.
74
+ const schemaOnly = {
75
+ model: "m",
76
+ tools: [
77
+ {
78
+ type: "function",
79
+ function: { name: "agent-swarm_get-swarm", description: "Lists agents in the swarm." },
80
+ },
81
+ ],
82
+ messages: [{ role: "user", content: "Call get-swarm. Found 5 agent(s) in the swarm." }],
83
+ };
84
+ expect(hasControlResult(schemaOnly)).toBe(false);
85
+ writeFileSync(join(dir, "req-1.json"), JSON.stringify(schemaOnly));
86
+ expect(controlReached(dir)).toBe(false);
87
+
88
+ const chat = {
89
+ ...schemaOnly,
90
+ messages: [
91
+ {
92
+ role: "assistant",
93
+ tool_calls: [{ id: "c1", type: "function", function: { name: "agent-swarm_get-swarm" } }],
94
+ },
95
+ { role: "tool", tool_call_id: "c1", content: [{ type: "text", text: found }] },
96
+ ],
97
+ };
98
+ expect(hasControlResult(chat)).toBe(true);
99
+ writeFileSync(join(dir, "req-2.json"), JSON.stringify(chat));
100
+ expect(controlReached(dir)).toBe(true);
101
+ });
102
+
103
+ test("control reads Responses items and rejects other tools and failed calls", () => {
104
+ const responses = (name: string, output: string) => ({
105
+ input: [
106
+ { type: "function_call", name, call_id: "call_1", arguments: "{}" },
107
+ { type: "function_call_output", call_id: "call_1", output },
108
+ ],
109
+ });
110
+ expect(hasControlResult(responses("mcp__agent_swarm__get_swarm", found))).toBe(true);
111
+ expect(hasControlResult(responses("mcp__agent_swarm__get_tasks", found))).toBe(false);
112
+ expect(hasControlResult(responses("mcp__agent_swarm__get_swarm", "Agent ID required."))).toBe(
113
+ false,
114
+ );
115
+ // An output for a different call id does not count.
116
+ const mismatched = responses("mcp__agent_swarm__get_swarm", found);
117
+ mismatched.input[1] = { type: "function_call_output", call_id: "call_2", output: found };
118
+ expect(hasControlResult(mismatched)).toBe(false);
119
+ });
120
+
121
+ test("session dirs: home is removed on success and on error, captures stay", async () => {
122
+ dir = mkdtempSync(join(tmpdir(), "probe-tool-schema-"));
123
+ let home = "";
124
+ const capDir = await withSessionDirs(dir, "ok", async (cap, h) => {
125
+ home = h;
126
+ writeFileSync(join(h, "config.toml"), "x");
127
+ writeFileSync(join(cap, "req-1.json"), "{}");
128
+ return cap;
129
+ });
130
+ expect(existsSync(home)).toBe(false);
131
+ expect(existsSync(join(capDir, "req-1.json"))).toBe(true);
132
+
133
+ await expect(
134
+ withSessionDirs(dir, "boom", async (cap, h) => {
135
+ home = h;
136
+ writeFileSync(join(h, "config.toml"), "x");
137
+ writeFileSync(join(cap, "req-1.json"), "{}");
138
+ throw new Error("codex exec exited 1");
139
+ }),
140
+ ).rejects.toThrow("codex exec exited 1");
141
+ expect(existsSync(home)).toBe(false);
142
+ expect(existsSync(join(dir, "boom-cap", "req-1.json"))).toBe(true);
143
+ });
144
+ });
@@ -25,6 +25,7 @@ const SYSTEM_TEMPLATES = [
25
25
  "system.agent.context.bound_note",
26
26
  "system.agent.cursor.first_message",
27
27
  "system.agent.lead",
28
+ "system.agent.lead.docs",
28
29
  "system.agent.memory",
29
30
  "system.agent.memory.remote",
30
31
  "system.agent.outputs",
@@ -118,11 +119,11 @@ describe("Session templates: registration", () => {
118
119
  expect(sessionSystemEventTypes("session")).toEqual([...SESSION_TEMPLATES].sort());
119
120
  });
120
121
 
121
- test("registers 32 system and session templates in total", () => {
122
+ test("registers 33 system and session templates in total", () => {
122
123
  const all = getAllTemplateDefinitions();
123
124
  const sessionSystem = all.filter((d) => d.category === "system" || d.category === "session");
124
- // 27 system blocks + 5 session composites.
125
- expect(sessionSystem.length).toBe(32);
125
+ // 28 system blocks + 5 session composites.
126
+ expect(sessionSystem.length).toBe(33);
126
127
  });
127
128
 
128
129
  test("drops the v1 blocks that prompt v2 deleted", () => {
@@ -0,0 +1,181 @@
1
+ import { afterEach, describe, expect, mock, test } from "bun:test";
2
+ import { checkProviderPoolKey } from "../utils/provider-key-probes";
3
+
4
+ describe("API pool key probes", () => {
5
+ const realFetch = globalThis.fetch;
6
+ const originalNodeEnv = process.env.NODE_ENV;
7
+ const originalPrivateUrls = process.env.ALLOW_PRIVATE_NETWORK_URLS;
8
+
9
+ afterEach(() => {
10
+ globalThis.fetch = realFetch;
11
+ if (originalNodeEnv === undefined) delete process.env.NODE_ENV;
12
+ else process.env.NODE_ENV = originalNodeEnv;
13
+ if (originalPrivateUrls === undefined) delete process.env.ALLOW_PRIVATE_NETWORK_URLS;
14
+ else process.env.ALLOW_PRIVATE_NETWORK_URLS = originalPrivateUrls;
15
+ });
16
+
17
+ test("uses the supplied OpenRouter URL and forbids redirects", async () => {
18
+ const calls: Array<{ url: string; init?: RequestInit }> = [];
19
+ globalThis.fetch = mock(async (url: string | URL | Request, init?: RequestInit) => {
20
+ calls.push({ url: String(url), init });
21
+ return new Response("{}", { status: calls.length === 1 ? 404 : 200 });
22
+ }) as typeof fetch;
23
+ expect(
24
+ await checkProviderPoolKey("OPENROUTER_API_KEY", "fake-abcde", {
25
+ OPENROUTER_BASE_URL: "https://gateway.example/v1/",
26
+ }),
27
+ ).toMatchObject({ ok: true });
28
+ expect(calls.map((call) => call.url)).toEqual([
29
+ "https://gateway.example/v1/key",
30
+ "https://gateway.example/v1/models",
31
+ ]);
32
+ expect(calls.every((call) => call.init?.redirect === "manual")).toBe(true);
33
+ });
34
+
35
+ test("rejects a private registry URL before sending credentials", async () => {
36
+ process.env.NODE_ENV = "production";
37
+ delete process.env.ALLOW_PRIVATE_NETWORK_URLS;
38
+ const fetch = mock(async () => new Response("{}"));
39
+ globalThis.fetch = fetch as unknown as typeof globalThis.fetch;
40
+ const result = await checkProviderPoolKey("NAN_API_KEY", "fake-abcde", {
41
+ NAN_BASE_URL: "https://127.0.0.1/v1",
42
+ });
43
+ expect(result.ok).toBe(false);
44
+ expect(result.httpStatus).toBeUndefined();
45
+ expect(fetch).not.toHaveBeenCalled();
46
+ });
47
+
48
+ test("supports registry aliases and configured URLs", async () => {
49
+ const fetch = mock(async (url: string | URL | Request, init?: RequestInit) => {
50
+ expect(String(url)).toBe("https://gateway.example/v1/models");
51
+ expect(init?.headers).toEqual({ Authorization: "Bearer fake-abcde" });
52
+ expect(init?.redirect).toBe("manual");
53
+ return new Response("Unauthorized", { status: 401 });
54
+ });
55
+ globalThis.fetch = fetch as typeof globalThis.fetch;
56
+ expect(
57
+ await checkProviderPoolKey("HELMCODE_API_NAN", "fake-abcde", {
58
+ NAN_BASE_URL: "https://gateway.example/v1",
59
+ }),
60
+ ).toMatchObject({ ok: false, httpStatus: 401 });
61
+ });
62
+
63
+ test.each([
64
+ [200, "{}", true, false],
65
+ [403, '{"error":{"type":"permission_error"}}', true, false],
66
+ [403, '{"error":{"type":"other"}}', false, false],
67
+ [401, "Unauthorized", false, false],
68
+ [429, "Throttled", true, true],
69
+ [503, "Unavailable", true, true],
70
+ ])("preserves Claude OAuth semantics for HTTP %i", async (status, body, ok, skipped) => {
71
+ globalThis.fetch = mock(async (url: string | URL | Request, init?: RequestInit) => {
72
+ expect(String(url)).toBe("https://api.anthropic.com/api/oauth/profile");
73
+ expect(init?.redirect).toBe("manual");
74
+ expect(init?.signal).toBeInstanceOf(AbortSignal);
75
+ return new Response(body, { status });
76
+ }) as typeof fetch;
77
+ const result = await checkProviderPoolKey("CLAUDE_CODE_OAUTH_TOKEN", "fake-abcde", {});
78
+ expect(result.ok).toBe(ok);
79
+ expect(result.skipped === true).toBe(skipped);
80
+ if (!ok) expect(result.httpStatus).toBe(status);
81
+ });
82
+
83
+ test("checks an Anthropic key at its gateway, keeping the URL path prefix", async () => {
84
+ const calls: Array<{ url: string; init?: RequestInit }> = [];
85
+ globalThis.fetch = mock(async (url: string | URL | Request, init?: RequestInit) => {
86
+ calls.push({ url: String(url), init });
87
+ return new Response("Unauthorized", { status: 401 });
88
+ }) as unknown as typeof fetch;
89
+ const result = await checkProviderPoolKey("ANTHROPIC_API_KEY", "fake-abcde", {
90
+ ANTHROPIC_BASE_URL: "https://gateway.example/api",
91
+ });
92
+ expect(result).toMatchObject({ ok: false, httpStatus: 401 });
93
+ expect(calls.map((call) => call.url)).toEqual(["https://gateway.example/api/v1/models"]);
94
+ expect(new Headers(calls[0]?.init?.headers).get("x-api-key")).toBe("fake-abcde");
95
+ expect(calls[0]?.init?.redirect).toBe("manual");
96
+
97
+ globalThis.fetch = mock(async () => new Response("{}")) as unknown as typeof fetch;
98
+ expect(
99
+ await checkProviderPoolKey("ANTHROPIC_API_KEY", "fake-abcde", {
100
+ ANTHROPIC_BASE_URL: "https://gateway.example/api",
101
+ }),
102
+ ).toMatchObject({ ok: true });
103
+ });
104
+
105
+ test("a gateway that lists no models leaves the Anthropic key unchecked", async () => {
106
+ globalThis.fetch = mock(
107
+ async () => new Response("Not found", { status: 404 }),
108
+ ) as unknown as typeof fetch;
109
+ expect(
110
+ await checkProviderPoolKey("ANTHROPIC_API_KEY", "fake-abcde", {
111
+ ANTHROPIC_BASE_URL: "https://gateway.example",
112
+ }),
113
+ ).toMatchObject({ ok: true, skipped: true });
114
+ });
115
+
116
+ test("an Anthropic key stays on api.anthropic.com when the gateway uses ANTHROPIC_AUTH_TOKEN", async () => {
117
+ const urls: string[] = [];
118
+ globalThis.fetch = mock(async (url: string | URL | Request) => {
119
+ urls.push(String(url));
120
+ return new Response("{}");
121
+ }) as unknown as typeof fetch;
122
+ expect(
123
+ await checkProviderPoolKey("ANTHROPIC_API_KEY", "fake-abcde", {
124
+ ANTHROPIC_BASE_URL: "https://gateway.example",
125
+ ANTHROPIC_AUTH_TOKEN: "gateway-token",
126
+ }),
127
+ ).toMatchObject({ ok: true });
128
+ expect(urls).toEqual(["https://api.anthropic.com/v1/models"]);
129
+ });
130
+
131
+ test("a private Anthropic gateway gets no credentials and no rejection", async () => {
132
+ process.env.NODE_ENV = "production";
133
+ delete process.env.ALLOW_PRIVATE_NETWORK_URLS;
134
+ const fetch = mock(async () => new Response("{}"));
135
+ globalThis.fetch = fetch as unknown as typeof globalThis.fetch;
136
+ const result = await checkProviderPoolKey("ANTHROPIC_API_KEY", "fake-abcde", {
137
+ ANTHROPIC_BASE_URL: "http://127.0.0.1:4000",
138
+ });
139
+ expect(result.ok).toBe(false);
140
+ expect(result.httpStatus).toBeUndefined();
141
+ expect(fetch).not.toHaveBeenCalled();
142
+ });
143
+
144
+ test("skips keys without an API-safe probe", async () => {
145
+ const fetch = mock(async () => new Response("{}"));
146
+ globalThis.fetch = fetch as unknown as typeof globalThis.fetch;
147
+ for (const keyType of ["DEVIN_API_KEY", "AMP_API_KEY", "CURSOR_API_KEY"]) {
148
+ expect(await checkProviderPoolKey(keyType, "fake-abcde", {})).toMatchObject({
149
+ ok: true,
150
+ skipped: true,
151
+ });
152
+ }
153
+ expect(fetch).not.toHaveBeenCalled();
154
+ });
155
+
156
+ test("reports network errors without exposing the submitted key", async () => {
157
+ globalThis.fetch = mock(async () => {
158
+ throw new Error("Rejected fake-abcde");
159
+ }) as unknown as typeof fetch;
160
+ const result = await checkProviderPoolKey("OPENAI_API_KEY", "fake-abcde", {});
161
+ expect(result.ok).toBe(false);
162
+ expect(result.error).not.toContain("fake-abcde");
163
+ });
164
+
165
+ test.each([
166
+ "ANTHROPIC_API_KEY",
167
+ "OPENAI_API_KEY",
168
+ "OPENROUTER_API_KEY",
169
+ "NAN_API_KEY",
170
+ "CLAUDE_CODE_OAUTH_TOKEN",
171
+ ])("redacts a long echoed %s before truncating the provider error", async (keyType) => {
172
+ const apiKey = "Z".repeat(240);
173
+ globalThis.fetch = mock(
174
+ async () => new Response(`Rejected ${apiKey}`, { status: 403 }),
175
+ ) as unknown as typeof fetch;
176
+ const result = await checkProviderPoolKey(keyType, apiKey, {});
177
+ expect(result).toMatchObject({ ok: false, httpStatus: 403 });
178
+ expect(result.error).toContain("[REDACTED]");
179
+ expect(result.error).not.toContain(apiKey.slice(0, 40));
180
+ });
181
+ });