@desplega.ai/agent-swarm 1.164.1 → 1.166.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 (328) hide show
  1. package/README.md +1 -1
  2. package/dist/acp-adapter-yzfxn8vj.js +23 -0
  3. package/dist/{actions-hp2h32dx.js → actions-qz3m692w.js} +24 -28
  4. package/dist/{amp-adapter-zfd7djyx.js → amp-adapter-j1xbmsqz.js} +7 -7
  5. package/dist/{app-w2d26h4f.js → app-43vrdmv8.js} +9 -10
  6. package/dist/{assistant-45pc4en7.js → assistant-ertffmka.js} +25 -29
  7. package/dist/{boot-reembed-k1q2s4fc.js → boot-reembed-998jtpdg.js} +12 -15
  8. package/dist/{boot-reembed-6rhcpsjf.js → boot-reembed-s7k7c6v2.js} +13 -15
  9. package/dist/boot-scrub-sweep-nepsk4t1.js +211 -0
  10. package/dist/{boot-seeds-ex93edx7.js → boot-seeds-vvzs1zhk.js} +12 -12
  11. package/dist/{claude-adapter-hcxd0szm.js → claude-adapter-2zvmekrp.js} +15 -17
  12. package/dist/{claude-managed-adapter-fz7xps2r.js → claude-managed-adapter-s49wbk8t.js} +5 -5
  13. package/dist/{claude-managed-setup-xx6f15h9.js → claude-managed-setup-cvhqq8qn.js} +4 -4
  14. package/dist/{claude-sdk-session-6nb9raem.js → claude-sdk-session-kkbxtw5q.js} +2624 -2606
  15. package/dist/{cli-etbrt8x1.js → cli-0nhdwxd5.js} +4 -4
  16. package/dist/{cli-jeefgppq.js → cli-0qjcsxgz.js} +7 -7
  17. package/dist/{cli-6behs2bt.js → cli-32bca7nr.js} +1 -1
  18. package/dist/{cli-gt8v96qx.js → cli-3ddybbbx.js} +4 -4
  19. package/dist/{acp-adapter-rxvek4ym.js → cli-3qn4r334.js} +317 -35
  20. package/dist/{cli-1d3w4xm3.js → cli-4r1sjfc4.js} +14 -6
  21. package/dist/{cli-691f2rxm.js → cli-66zh26sp.js} +68 -16
  22. package/dist/{cli-et8wdyqh.js → cli-68r1j4sg.js} +9 -9
  23. package/dist/{cli-xb7kyanp.js → cli-8kgvmkak.js} +3 -3
  24. package/dist/{cli-fdpkw8vk.js → cli-8t229cmr.js} +8 -8
  25. package/dist/{cli-gc54qc0p.js → cli-aqcey741.js} +1 -1
  26. package/dist/{cli-4q4cmr4t.js → cli-cpfmqz3n.js} +8 -8
  27. package/dist/{cli-a9bw0xxt.js → cli-df56tqdw.js} +997 -408
  28. package/dist/{cli-n7h1kqpf.js → cli-dmtrxqhg.js} +6 -1
  29. package/dist/{cli-xfb7333f.js → cli-dw2q9f7g.js} +5 -5
  30. package/dist/{cli-4drstbb9.js → cli-ecknp7p1.js} +81 -23
  31. package/dist/{cli-v0pvvhfq.js → cli-efnd2kce.js} +36 -1
  32. package/dist/{cli-1wetf34q.js → cli-f2n17zhp.js} +1 -1
  33. package/dist/{cli-2c8eky5y.js → cli-feh3dkah.js} +1 -1
  34. package/dist/cli-fn697bnt.js +126 -0
  35. package/dist/{cli-v5pr2fgt.js → cli-gt8azyet.js} +3 -3
  36. package/dist/{cli-dbszf3rw.js → cli-h3x613ze.js} +4 -4
  37. package/dist/{cli-nwhb8y2e.js → cli-hf5s9pgf.js} +1 -1
  38. package/dist/cli-j2jbr8af.js +516 -0
  39. package/dist/{cli-agjzafns.js → cli-jhm6hzjn.js} +593 -10
  40. package/dist/{cli-j0tmc908.js → cli-jvj6a0cv.js} +484 -477
  41. package/dist/{cli-xxz89c2n.js → cli-mb75wpxf.js} +1 -1
  42. package/dist/{cli-07s39g1n.js → cli-mv2520nm.js} +1 -1
  43. package/dist/{cli-v90vfwyb.js → cli-mwegs4af.js} +5 -5
  44. package/dist/{cli-bz1gbac9.js → cli-p4ztb80h.js} +2 -2
  45. package/dist/{cli-1wd1n2ya.js → cli-rfphkzh5.js} +1 -1
  46. package/dist/{cli-976r1zqx.js → cli-rggdq8wq.js} +5 -5
  47. package/dist/{cli-3ajw5v5v.js → cli-rsy933yz.js} +6 -6
  48. package/dist/{cli-yj790e19.js → cli-tadfgd0t.js} +8 -1
  49. package/dist/{cli-cn53e7vc.js → cli-ttbsse3f.js} +6 -1
  50. package/dist/{cli-dyqsrqcf.js → cli-v2a8x45d.js} +4 -4
  51. package/dist/{cli-wbvnn6tk.js → cli-v6vv7dy8.js} +91 -9
  52. package/dist/{cli-z2ap2c0c.js → cli-vzc73wet.js} +90 -55
  53. package/dist/cli-wt7ccy4r.js +2654 -0
  54. package/dist/{cli-4cn19jvn.js → cli-x7kpr72a.js} +3 -3
  55. package/dist/{cli-003785v9.js → cli-xvc3q9br.js} +1 -1
  56. package/dist/{cli-v0e7jpr5.js → cli-xvdgasgy.js} +13 -4
  57. package/dist/{cli-asqeg4ve.js → cli-y59h2pk9.js} +2 -2
  58. package/dist/{cli-0wn3ypqv.js → cli-yshaazs1.js} +14121 -3478
  59. package/dist/{cli-wg8yjqtw.js → cli-ysnyhws9.js} +16 -3
  60. package/dist/cli-zp67qg5x.js +92 -0
  61. package/dist/{cli-g3qgrzck.js → cli-zsh093hk.js} +71 -3
  62. package/dist/{cli-ma3zvh39.js → cli-zy7vwsne.js} +6066 -5829
  63. package/dist/cli.js +79 -26
  64. package/dist/{codex-adapter-pcxdn10v.js → codex-adapter-dw1wxqt8.js} +10 -10
  65. package/dist/{codex-hook-b1fb7c2k.js → codex-hook-d2rkvf39.js} +1 -1
  66. package/dist/{codex-login-059nm145.js → codex-login-terbdsqq.js} +2 -2
  67. package/dist/{codex-session-runner-xzwwddp8.js → codex-session-runner-3cgb8y1g.js} +10 -10
  68. package/dist/{commands-98at8mkn.js → commands-mhxam6z2.js} +9 -9
  69. package/dist/{cursor-adapter-dkg6qnkp.js → cursor-adapter-2qda3st7.js} +6 -6
  70. package/dist/{db-01g8wtvw.js → db-sjks418k.js} +11 -9
  71. package/dist/{devin-adapter-qmttt2fz.js → devin-adapter-bffjj5n4.js} +2 -2
  72. package/dist/{dispatcher-nn2k1wxd.js → dispatcher-1es759rn.js} +22 -26
  73. package/dist/{dsh-adapter-w4d4vwdq.js → dsh-adapter-bek4cyw9.js} +6 -6
  74. package/dist/{e2b-9e4g2hvt.js → e2b-nd50eccg.js} +1 -1
  75. package/dist/{event-bus-z7v2t26e.js → event-bus-vnfthafg.js} +2 -2
  76. package/dist/grok-adapter-ar9cw9jg.js +29 -0
  77. package/dist/{handlers-b0kqsbg8.js → handlers-1gkbhktr.js} +25 -29
  78. package/dist/{hook-sap7v1aj.js → hook-ds2zxq9v.js} +16 -18
  79. package/dist/{hook-pz50gqep.js → hook-vxa3tcsy.js} +10 -12
  80. package/dist/{http-ncr8rmd0.js → http-3h21z9md.js} +679 -227
  81. package/dist/{index-c4aqcyz1.js → index-9b65j1m5.js} +11 -11
  82. package/dist/{index-r46s01nq.js → index-bn6cy8ps.js} +22 -26
  83. package/dist/{index-38kqdt3e.js → index-dkfxpkmm.js} +23 -27
  84. package/dist/{index-630kj5vd.js → index-f8xnqrye.js} +55 -43
  85. package/dist/{index-fdk1t4d6.js → index-n0rxa4ca.js} +11962 -9900
  86. package/dist/{index-9nrj7797.js → index-n99fycq7.js} +22 -26
  87. package/dist/{keepalive-31613r2h.js → keepalive-3xspa9jw.js} +14 -11
  88. package/dist/{lead-mx59tr5s.js → lead-6qphc124.js} +43 -43
  89. package/dist/{maintenance-55ff39ca.js → maintenance-0teem1bb.js} +10 -12
  90. package/dist/{node-f0ykebbv.js → node-9sg7ajby.js} +25 -7
  91. package/dist/{oauth-refresh-sweep-anma954w.js → oauth-refresh-sweep-sh9q25xp.js} +13 -10
  92. package/dist/{onboard-b2fy8ftb.js → onboard-54qmnqxe.js} +4 -3
  93. package/dist/{opencode-adapter-p1s92tmy.js → opencode-adapter-wjfa2wvd.js} +6 -6
  94. package/dist/{otel-impl-9brvt7xv.js → otel-impl-ycqzp2a1.js} +2 -2
  95. package/dist/{pi-mono-adapter-kz4rrwvd.js → pi-mono-adapter-e77e9zst.js} +6 -6
  96. package/dist/{pricing-refresh-7w8mbmva.js → pricing-refresh-atnkngqs.js} +10 -10
  97. package/dist/{rbac-roles-ejp7qwvb.js → rbac-roles-9pjphh9q.js} +10 -10
  98. package/dist/{rbac-roles-nhk4r7bc.js → rbac-roles-j78wdksm.js} +10 -10
  99. package/dist/{render-v2-2gzt8skq.js → render-v2-pxjs49fx.js} +9 -10
  100. package/dist/{setup-5et8t412.js → setup-ymej0xtx.js} +2 -2
  101. package/dist/{work-objects-tb0er7qa.js → work-objects-3nh4ymzb.js} +26 -30
  102. package/dist/{worker-62cv075d.js → worker-qa9fkz5s.js} +43 -43
  103. package/dist/{x-vbxb31jr.js → x-ma4gnfgy.js} +2 -2
  104. package/openapi.json +457 -31
  105. package/package.json +14 -6
  106. package/plugin/commands/create-pr.md +7 -1
  107. package/plugin/pi-skills/create-pr/SKILL.md +7 -1
  108. package/src/agentmail/app.ts +5 -4
  109. package/src/be/automation-preflight.ts +11 -9
  110. package/src/be/boot-scrub-sweep.ts +302 -0
  111. package/src/be/connector-codes.ts +81 -0
  112. package/src/be/crypto/key-bootstrap.ts +1 -1
  113. package/src/be/db/approvals.ts +15 -1
  114. package/src/be/db/codex-oauth-device-flows.ts +40 -0
  115. package/src/be/db/config-secrets.ts +31 -0
  116. package/src/be/db/runtime.ts +4 -0
  117. package/src/be/db/script-runs.ts +15 -0
  118. package/src/be/db/tasks/write.ts +1 -1
  119. package/src/be/db-queries/mcp-oauth.ts +5 -0
  120. package/src/be/db-queries/oauth.ts +77 -0
  121. package/src/be/db.ts +162 -59
  122. package/src/be/events.ts +17 -1
  123. package/src/be/memory/index-content.ts +2 -2
  124. package/src/be/memory/providers/sqlite-store.ts +63 -11
  125. package/src/be/memory/types.ts +14 -0
  126. package/src/be/migrations/197_connector_codes.sql +23 -0
  127. package/src/be/migrations/198_permission_audit_guest.sql +33 -0
  128. package/src/be/migrations/200_backfill_auto_memory_expiry.sql +20 -0
  129. package/src/be/migrations/201_workflow_replay_columns.sql +10 -0
  130. package/src/be/migrations/202_codex_oauth_device_flows.sql +24 -0
  131. package/src/be/model-tier-resolution.ts +2 -1
  132. package/src/be/model-validation.ts +1 -1
  133. package/src/be/pricing-normalize.ts +3 -0
  134. package/src/be/rbac-audit.ts +2 -1
  135. package/src/be/scripts/db.ts +28 -4
  136. package/src/be/scripts/source-secrets.ts +37 -0
  137. package/src/be/scripts/typecheck.ts +23 -2
  138. package/src/be/scrub-json.ts +32 -0
  139. package/src/be/sealed-json.ts +59 -0
  140. package/src/be/secret-registry.ts +131 -0
  141. package/src/be/seed-pricing.ts +14 -0
  142. package/src/be/users.ts +7 -1
  143. package/src/be/workflow-replay.ts +77 -0
  144. package/src/cli.tsx +7 -0
  145. package/src/commands/provider-credentials.ts +46 -8
  146. package/src/commands/runner.ts +2 -10
  147. package/src/extensions/contract-types.generated.ts +1 -1
  148. package/src/github/app.ts +4 -4
  149. package/src/heartbeat/heartbeat.ts +20 -8
  150. package/src/hooks/pr-body-guard.ts +1 -1
  151. package/src/http/agents.ts +1 -0
  152. package/src/http/approval-requests.ts +1 -0
  153. package/src/http/apps.ts +3 -0
  154. package/src/http/auth.ts +4 -1
  155. package/src/http/codex-oauth-device.ts +8 -10
  156. package/src/http/codex-oauth-keep-warm.ts +2 -2
  157. package/src/http/config-values.ts +59 -0
  158. package/src/http/config.ts +5 -0
  159. package/src/http/db-query-bounded.ts +3 -0
  160. package/src/http/db-query-shared.ts +107 -15
  161. package/src/http/db-query.ts +10 -2
  162. package/src/http/extensions.ts +1 -0
  163. package/src/http/index.ts +23 -6
  164. package/src/http/integrations.ts +1 -38
  165. package/src/http/ip-rate-limit.ts +82 -0
  166. package/src/http/kv.ts +38 -4
  167. package/src/http/mcp-servers.ts +1 -0
  168. package/src/http/memory.ts +9 -0
  169. package/src/http/models-catalog.ts +2 -0
  170. package/src/http/operator-actor.ts +4 -0
  171. package/src/http/page-proxy.ts +22 -0
  172. package/src/http/pages.ts +4 -2
  173. package/src/http/realtime.ts +1 -1
  174. package/src/http/request-principal.ts +14 -1
  175. package/src/http/script-connections.ts +8 -0
  176. package/src/http/script-runs.ts +12 -4
  177. package/src/http/scripts.ts +15 -0
  178. package/src/http/session-cost-recompute.ts +3 -2
  179. package/src/http/tasks.ts +9 -0
  180. package/src/http/users.ts +226 -0
  181. package/src/http/workflows.ts +3 -2
  182. package/src/http.ts +1 -0
  183. package/src/integrations/kapso/client.ts +3 -1
  184. package/src/jira/app.ts +2 -2
  185. package/src/kv-view.ts +324 -0
  186. package/src/linear/app.ts +2 -2
  187. package/src/oauth/mcp-wrapper.ts +1 -1
  188. package/src/prompts/memories.ts +42 -1
  189. package/src/providers/acp-adapter.ts +85 -24
  190. package/src/providers/acp-target-catalog.ts +54 -1
  191. package/src/providers/acp-targets.ts +358 -4
  192. package/src/providers/amp-adapter.ts +1 -1
  193. package/src/providers/cursor-adapter.ts +1 -1
  194. package/src/providers/grok-adapter.ts +186 -0
  195. package/src/providers/index.ts +5 -1
  196. package/src/providers/pricing-sources.md +6 -0
  197. package/src/providers/reasoning-effort.ts +17 -1
  198. package/src/providers/types.ts +2 -1
  199. package/src/rbac/can.ts +4 -3
  200. package/src/rbac/legacy-policy.ts +3 -3
  201. package/src/rbac/types.ts +6 -0
  202. package/src/script-workflows/supervisor.ts +11 -2
  203. package/src/scripts-runtime/eval-harness.ts +2 -12
  204. package/src/scripts-runtime/executors/quickjs-bundle.ts +152 -0
  205. package/src/scripts-runtime/executors/quickjs-prelude.ts +285 -0
  206. package/src/scripts-runtime/executors/quickjs-runner.ts +580 -0
  207. package/src/scripts-runtime/executors/quickjs-worker.ts +81 -0
  208. package/src/scripts-runtime/executors/quickjs.ts +196 -0
  209. package/src/scripts-runtime/executors/registry.ts +2 -0
  210. package/src/scripts-runtime/executors/sourcemap.ts +79 -0
  211. package/src/scripts-runtime/script-error-hints.ts +14 -0
  212. package/src/scripts-runtime/swarm-sdk.ts +9 -2
  213. package/src/scripts-runtime/types/stdlib.d.ts +27 -8
  214. package/src/scripts-runtime/types/swarm-extension.d.ts +3 -0
  215. package/src/scripts-runtime/types/swarm-sdk.d.ts +27 -8
  216. package/src/server-user.ts +2 -20
  217. package/src/server.ts +3 -1
  218. package/src/slack/app.ts +4 -0
  219. package/src/slack/config.ts +3 -1
  220. package/src/slack/egress-scrub.ts +87 -0
  221. package/src/tests/acp-adapter.test.ts +191 -2
  222. package/src/tests/boot-scrub-sweep.test.ts +390 -0
  223. package/src/tests/budget-user-scope.test.ts +7 -21
  224. package/src/tests/check-pr-body.test.ts +149 -6
  225. package/src/tests/check-text-columns.test.ts +145 -0
  226. package/src/tests/claude-adapter-binary.test.ts +1 -1
  227. package/src/tests/claude-sdk-process.fixture.ts +13 -2
  228. package/src/tests/codex-oauth-device.test.ts +23 -37
  229. package/src/tests/codex-oauth-keep-warm.test.ts +29 -0
  230. package/src/tests/console-scrub.test.ts +141 -0
  231. package/src/tests/credential-check.test.ts +90 -0
  232. package/src/tests/db-query-denylist.test.ts +202 -0
  233. package/src/tests/db-task-read-contract.test.ts +1 -0
  234. package/src/tests/db-task-write-contract.test.ts +1 -0
  235. package/src/tests/docs-index-footprint.test.js +3 -3
  236. package/src/tests/entrypoint-acp-target-binary.test.ts +44 -1
  237. package/src/tests/events-scrub.test.ts +88 -0
  238. package/src/tests/explicit-model-validation.test.ts +81 -0
  239. package/src/tests/extensions-pre-task.test.ts +33 -1
  240. package/src/tests/fixtures/grok/acp-updates-recorded.jsonl +53 -0
  241. package/src/tests/fixtures/grok/prompt-response-meta.json +36 -0
  242. package/src/tests/fixtures/secret-corpus.ts +320 -0
  243. package/src/tests/grok-adapter.test.ts +647 -0
  244. package/src/tests/heartbeat-checklist.test.ts +44 -0
  245. package/src/tests/integration-disable-flags.test.ts +156 -0
  246. package/src/tests/kv-http.test.ts +34 -0
  247. package/src/tests/kv-tool.test.ts +274 -1
  248. package/src/tests/kv-view.test.ts +147 -0
  249. package/src/tests/mcp-user-route.test.ts +257 -15
  250. package/src/tests/memory-injection-outcome.test.ts +143 -0
  251. package/src/tests/model-tier-resolution.test.ts +76 -3
  252. package/src/tests/page-guest-session.test.ts +424 -0
  253. package/src/tests/page-proxy.test.ts +1 -1
  254. package/src/tests/page-viewer-auth.test.ts +22 -1
  255. package/src/tests/pr-body-leaks.test.ts +157 -2
  256. package/src/tests/preload.ts +10 -0
  257. package/src/tests/provider-registration.test.ts +3 -1
  258. package/src/tests/rbac-audit.test.ts +32 -0
  259. package/src/tests/rbac-mcp-admission-e2e.test.ts +4 -62
  260. package/src/tests/script-connections.test.ts +9 -0
  261. package/src/tests/script-executor-conformance.test.ts +3 -0
  262. package/src/tests/script-executor-quickjs.test.ts +442 -0
  263. package/src/tests/script-executor-registry.test.ts +6 -1
  264. package/src/tests/script-runs-http.test.ts +126 -1
  265. package/src/tests/script-runs-scrub.test.ts +275 -0
  266. package/src/tests/scripts-http.test.ts +82 -1
  267. package/src/tests/scripts-mcp-e2e.test.ts +42 -6
  268. package/src/tests/scripts-runtime.test.ts +37 -0
  269. package/src/tests/scrubbed-text-12e.test.ts +289 -0
  270. package/src/tests/sealed-key-guard.test.ts +166 -0
  271. package/src/tests/secret-canary-sweep.test.ts +958 -0
  272. package/src/tests/secret-registry.test.ts +355 -0
  273. package/src/tests/secret-rules.test.ts +230 -0
  274. package/src/tests/secret-scrubber.test.ts +317 -1
  275. package/src/tests/session-costs-golden.test.ts +17 -0
  276. package/src/tests/slack-egress-scrub.test.ts +216 -0
  277. package/src/tests/swarm-tool-result-gate.test.ts +143 -30
  278. package/src/tests/task-tools-ctx.test.ts +3 -0
  279. package/src/tests/user-token-routes.test.ts +342 -3
  280. package/src/tests/workflow-replay.test.ts +518 -0
  281. package/src/tests/workflow-scrub.test.ts +242 -0
  282. package/src/tests/workflow-system-one-decision-providers.test.ts +737 -0
  283. package/src/tools/db-query.ts +1 -1
  284. package/src/tools/inject-learning.ts +1 -1
  285. package/src/tools/kv/kv-get.ts +136 -6
  286. package/src/tools/send-task.ts +61 -17
  287. package/src/tools/utils.ts +186 -43
  288. package/src/tools/workflows/get-workflow-run.ts +3 -2
  289. package/src/types.ts +16 -1
  290. package/src/utils/console-scrub-install.ts +5 -0
  291. package/src/utils/console-scrub.ts +57 -0
  292. package/src/utils/credentials.ts +8 -0
  293. package/src/utils/page-session.ts +6 -2
  294. package/src/utils/pr-body-leaks.ts +149 -10
  295. package/src/utils/request-auth-context.ts +2 -0
  296. package/src/utils/secret-rules.generated.ts +2055 -0
  297. package/src/utils/secret-scrubber.ts +662 -36
  298. package/src/workflows/engine.ts +39 -4
  299. package/src/workflows/executors/notify.ts +6 -1
  300. package/src/workflows/executors/system-one-decision.ts +31 -12
  301. package/src/workflows/executors/system-one-openai-wire.ts +168 -0
  302. package/src/workflows/executors/system-one-providers.ts +112 -11
  303. package/src/workflows/executors/system-one-wire.ts +79 -0
  304. package/src/workflows/recovery.ts +10 -1
  305. package/src/workflows/resume.ts +62 -3
  306. package/src/workflows/retry-poller.ts +3 -0
  307. package/templates/ai-toolbox.manifest.json +2 -2
  308. package/templates/skills/agent-browser/SKILL.md +5 -5
  309. package/templates/skills/agent-browser/config.json +1 -1
  310. package/templates/skills/agent-browser/content.md +4 -4
  311. package/templates/skills/artifacts/content.md +17 -7
  312. package/templates/skills/jira-interaction/config.json +1 -1
  313. package/templates/skills/jira-interaction/content.md +56 -205
  314. package/templates/skills/linear-interaction/config.json +1 -1
  315. package/templates/skills/linear-interaction/content.md +31 -50
  316. package/templates/skills/pages/content.md +8 -4
  317. package/templates/skills/qa/content.md +2 -2
  318. package/templates/skills/workflow-iterate/content.md +4 -4
  319. package/tsconfig.json +3 -3
  320. package/dist/boot-scrub-logs-t8xg7wd7.js +0 -99
  321. package/dist/cli-0a7r5331.js +0 -168
  322. package/dist/cli-93rxw9yd.js +0 -951
  323. package/dist/cli-cvk6yzw5.js +0 -40
  324. package/dist/cli-dgnj0zcc.js +0 -40
  325. package/dist/cli-qt5wzq6v.js +0 -256
  326. package/dist/cli-v7fqehqd.js +0 -9281
  327. package/dist/cli-vbr859rz.js +0 -572
  328. package/src/be/boot-scrub-logs.ts +0 -132
@@ -3,6 +3,10 @@ import {
3
3
  checkPrBody,
4
4
  isFixTitle,
5
5
  pickedChoices,
6
+ prBodyWarnings,
7
+ proseWordCount,
8
+ riskLevel,
9
+ templateLeadLines,
6
10
  templateSections,
7
11
  } from "../../scripts/check-pr-body";
8
12
 
@@ -40,6 +44,13 @@ describe("check-pr-body", () => {
40
44
  ]);
41
45
  });
42
46
 
47
+ test("headings inside HTML comments or still-open fences are not sections", () => {
48
+ const hidden = "<!--\n## Intent\n-->\n````md\n```\n## Intent\n````\n";
49
+ expect(checkPrBody(TEMPLATE, `${hidden}${URGENT}`, "feat: x")).toEqual([
50
+ "missing section: ## Intent",
51
+ ]);
52
+ });
53
+
43
54
  test("fix titles are detected by conventional-commit type", () => {
44
55
  expect(isFixTitle("fix: x")).toBe(true);
45
56
  expect(isFixTitle("fix(slack): x")).toBe(true);
@@ -91,15 +102,147 @@ describe("check-pr-body", () => {
91
102
  expect(checkPrBody(TEMPLATE, body)).toEqual(["missing section: ## Urgency"]);
92
103
  });
93
104
 
94
- test("the real template fails unedited and passes once every section is filled", async () => {
105
+ test("the real template fails unedited and passes once every part is filled", async () => {
95
106
  const template = await Bun.file(".github/pull_request_template.md").text();
107
+ const leads = templateLeadLines(template);
108
+ expect(leads).toEqual(["Why", "Risk"]);
96
109
  const sections = templateSections(template).filter((s) => s.when !== "optional");
97
110
  expect(sections.length).toBeGreaterThan(0);
98
- expect(checkPrBody(template, template, "fix: x")).toHaveLength(sections.length);
111
+ const all = { title: "fix: x", author: "desplega-bot", changedFiles: ["apps/ui/a.tsx"] };
112
+ // Unknown risk keeps the outline required, so every section and lead line is reported.
113
+ expect(checkPrBody(template, template, all)).toHaveLength(sections.length + leads.length);
114
+
115
+ const filled = [
116
+ "**Why:** because.",
117
+ "**Risk:** medium",
118
+ ...sections.map(
119
+ (s) => `## ${s.heading}\n\n${s.choices.length ? `- [x] ${s.choices[0]}` : "filled"}\n`,
120
+ ),
121
+ ].join("\n");
122
+ expect(checkPrBody(template, filled, all)).toEqual([]);
123
+ });
124
+
125
+ test("the real template's markers", async () => {
126
+ const template = await Bun.file(".github/pull_request_template.md").text();
127
+ const when = Object.fromEntries(templateSections(template).map((s) => [s.heading, s.when]));
128
+ expect(when).toMatchObject({
129
+ "Review map": "always",
130
+ "Change outline": "outline",
131
+ "Before / after": "ui",
132
+ Repro: "fix",
133
+ Urgency: "always",
134
+ "Swarm provenance": "bot",
135
+ });
136
+ });
137
+ });
138
+
139
+ describe("check-pr-body: lead lines, risk and conditional sections", () => {
140
+ const T = `<!-- top -->
141
+
142
+ **Why:** <!-- one sentence -->
143
+
144
+ **Risk:** <!-- low | medium | high -->
145
+
146
+ ## Outline <!-- outline -->
147
+
148
+ <!-- g -->
149
+
150
+ ## Before / after <!-- ui -->
151
+
152
+ <!-- g -->
153
+
154
+ ## Swarm provenance <!-- bot -->
155
+
156
+ <!-- g -->
157
+ `;
158
+ const human = { author: "octocat" };
159
+ const leads = (risk: string) => `**Why:** It broke.\n\n**Risk:** ${risk}\n\n`;
160
+ const outline = "## Outline\n\n```\n+ new\n```\n";
161
+
162
+ test("lead lines are read from the template preamble", () => {
163
+ expect(templateLeadLines(T)).toEqual(["Why", "Risk"]);
164
+ });
165
+
166
+ test("the Why and Risk lines are required and Risk must be a known level", () => {
167
+ expect(checkPrBody(T, outline, human)).toEqual([
168
+ "missing line: **Why:**",
169
+ "missing line: **Risk:**",
170
+ ]);
171
+ expect(checkPrBody(T, `**Why:**\n**Risk:** <!-- x -->\n${outline}`, human)).toEqual([
172
+ "empty line: **Why:**",
173
+ "empty line: **Risk:**",
174
+ ]);
175
+ expect(checkPrBody(T, `${leads("spicy")}${outline}`, human)).toEqual([
176
+ "**Risk:** must start with one of: low, medium, high",
177
+ ]);
178
+ expect(checkPrBody(T, `${leads("High (secrets)")}${outline}`, human)).toEqual([]);
179
+ });
180
+
181
+ test("a lead line after the first heading does not count", () => {
182
+ expect(checkPrBody(T, `${outline}\n**Why:** x\n**Risk:** low\n`, human)).toContain(
183
+ "missing line: **Why:**",
184
+ );
185
+ });
186
+
187
+ test("riskLevel parses the first word only", () => {
188
+ expect(riskLevel("**Risk:** high (secrets)")).toBe("high");
189
+ expect(riskLevel("**Risk:** medium")).toBe("medium");
190
+ expect(riskLevel("**Risk:** lowish")).toBeUndefined();
191
+ expect(riskLevel("no risk line")).toBeUndefined();
192
+ });
193
+
194
+ test("the outline may be skipped only when Risk is low and the diff is small", () => {
195
+ const missing = ["missing section: ## Outline"];
196
+ expect(checkPrBody(T, leads("medium"), { ...human, changedLines: 3 })).toEqual(missing);
197
+ expect(checkPrBody(T, leads("low"), { ...human, changedLines: 51 })).toEqual(missing);
198
+ expect(checkPrBody(T, leads("low"), { ...human, changedLines: 50 })).toEqual([]);
199
+ expect(checkPrBody(T, leads("low"), human)).toEqual([]);
200
+ });
201
+
202
+ test("Before / after is required only when the diff touches a UI app", () => {
203
+ const body = `${leads("medium")}${outline}`;
204
+ const missing = ["missing section: ## Before / after"];
205
+ expect(checkPrBody(T, body, { ...human, changedFiles: ["apps/ui/src/App.tsx"] })).toEqual(
206
+ missing,
207
+ );
208
+ expect(checkPrBody(T, body, { ...human, changedFiles: ["apps/templates-ui/x.ts"] })).toEqual(
209
+ missing,
210
+ );
211
+ expect(checkPrBody(T, body, { ...human, changedFiles: ["src/apps/ui-thing.ts"] })).toEqual([]);
212
+ expect(checkPrBody(T, body, human)).toEqual([]);
213
+ });
214
+
215
+ test("Swarm provenance is required for the bot and for a local run without an author", () => {
216
+ const body = `${leads("medium")}${outline}`;
217
+ const missing = ["missing section: ## Swarm provenance"];
218
+ expect(checkPrBody(T, body, { author: "desplega-bot" })).toEqual(missing);
219
+ expect(checkPrBody(T, body, {})).toEqual(missing);
220
+ expect(checkPrBody(T, body, human)).toEqual([]);
221
+ const withProvenance = `${body}\n## Swarm provenance\n- Task: link\n`;
222
+ expect(checkPrBody(T, withProvenance, { author: "desplega-bot" })).toEqual([]);
223
+ });
224
+ });
225
+
226
+ describe("check-pr-body: prose budget", () => {
227
+ test("code fences, tables, details, comments, headings and URLs do not count", () => {
228
+ const body = [
229
+ "**Why:** three words here.",
230
+ "## Review map",
231
+ "| Area | Depth | Why |",
232
+ "|---|---|---|",
233
+ "```",
234
+ "lots of code words here",
235
+ "```",
236
+ "<details>hidden words here</details>",
237
+ "<!-- hidden comment words -->",
238
+ "See https://example.com/a/b and #12.",
239
+ ].join("\n");
240
+ // "**Why:**", "three", "words", "here.", "See", "and". "#12." has no letter.
241
+ expect(proseWordCount(body)).toBe(6);
242
+ });
99
243
 
100
- const filled = sections
101
- .map((s) => `## ${s.heading}\n\n${s.choices.length ? `- [x] ${s.choices[0]}` : "filled"}\n`)
102
- .join("\n");
103
- expect(checkPrBody(template, filled, "fix: x")).toEqual([]);
244
+ test("a long body warns but never fails", () => {
245
+ expect(prBodyWarnings("word ".repeat(300))).toEqual([]);
246
+ expect(prBodyWarnings("word ".repeat(301))).toHaveLength(1);
104
247
  });
105
248
  });
@@ -0,0 +1,145 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
3
+ import { unlink } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import {
6
+ checkTextColumns,
7
+ listTextColumns,
8
+ TEXT_COLUMNS_PATH,
9
+ type TextColumnClassification,
10
+ } from "../../scripts/check-text-columns";
11
+ import {
12
+ closeDb,
13
+ createAgent,
14
+ createTask,
15
+ getDbClient,
16
+ initDb,
17
+ updateTaskProgress,
18
+ } from "../be/db";
19
+ import { runMigrations } from "../be/migrations/runner";
20
+ import { type SyntheticSecret, syntheticSecret } from "./synthetic-secret-helpers";
21
+
22
+ const repoRoot = join(import.meta.dir, "..", "..");
23
+
24
+ async function loadCommitted(): Promise<TextColumnClassification> {
25
+ return (await Bun.file(join(repoRoot, TEXT_COLUMNS_PATH)).json()) as TextColumnClassification;
26
+ }
27
+
28
+ describe("TEXT-column classification check", () => {
29
+ let db: Database;
30
+
31
+ beforeAll(() => {
32
+ db = new Database(":memory:");
33
+ const originalLog = console.log;
34
+ console.log = () => {};
35
+ try {
36
+ runMigrations(db);
37
+ } finally {
38
+ console.log = originalLog;
39
+ }
40
+ });
41
+
42
+ afterAll(() => {
43
+ db.close();
44
+ });
45
+
46
+ test("the committed classification covers the migrated schema exactly", async () => {
47
+ const classification = await loadCommitted();
48
+ expect(listTextColumns(db).length).toBeGreaterThan(100);
49
+ expect(checkTextColumns(db, classification)).toEqual([]);
50
+ });
51
+
52
+ test("a new TEXT column from a later migration fails until it is classified", async () => {
53
+ const classification = await loadCommitted();
54
+ const scratch = new Database(":memory:");
55
+ try {
56
+ const originalLog = console.log;
57
+ console.log = () => {};
58
+ try {
59
+ runMigrations(scratch);
60
+ } finally {
61
+ console.log = originalLog;
62
+ }
63
+ // Stand-in for a future migration that adds a free-text sink.
64
+ scratch.run("CREATE TABLE canary_notes (id TEXT PRIMARY KEY, body TEXT NOT NULL, n INTEGER)");
65
+
66
+ const violations = checkTextColumns(scratch, classification);
67
+ expect(violations).toEqual([
68
+ "canary_notes.id: unclassified TEXT column",
69
+ "canary_notes.body: unclassified TEXT column",
70
+ ]);
71
+
72
+ const classified: TextColumnClassification = {
73
+ ...classification,
74
+ canary_notes: { id: { exempt: "identifier" }, body: { pending: "test batch" } },
75
+ };
76
+ expect(checkTextColumns(scratch, classified)).toEqual([]);
77
+ } finally {
78
+ scratch.close();
79
+ }
80
+ });
81
+
82
+ test("stale and malformed entries are rejected", async () => {
83
+ const classification = await loadCommitted();
84
+ const firstTable = Object.keys(classification)[0] as string;
85
+ const firstColumn = Object.keys(classification[firstTable] ?? {})[0] as string;
86
+ const bad = {
87
+ ...classification,
88
+ [firstTable]: { ...classification[firstTable], [firstColumn]: { exempt: "" } },
89
+ gone_table: { note: "scrubbed" },
90
+ } as TextColumnClassification;
91
+
92
+ const violations = checkTextColumns(db, bad);
93
+ expect(violations).toContain("gone_table.note: stale entry, no such TEXT column");
94
+ expect(
95
+ violations.some((v) => v.startsWith(`${firstTable}.${firstColumn}: invalid entry`)),
96
+ ).toBe(true);
97
+ });
98
+ });
99
+
100
+ describe("ScrubbedText at a production writer", () => {
101
+ const dbPath = "./test-check-text-columns-writer.sqlite";
102
+ let secret: SyntheticSecret;
103
+
104
+ async function removeDbFiles(): Promise<void> {
105
+ for (const suffix of ["", "-wal", "-shm"]) {
106
+ try {
107
+ await unlink(dbPath + suffix);
108
+ } catch {}
109
+ }
110
+ }
111
+
112
+ beforeAll(async () => {
113
+ await removeDbFiles();
114
+ initDb(dbPath);
115
+ secret = syntheticSecret("textcol");
116
+ });
117
+
118
+ afterAll(async () => {
119
+ secret.cleanup();
120
+ closeDb();
121
+ await removeDbFiles();
122
+ });
123
+
124
+ test("a column classified scrubbed holds no secret after its real writer runs", async () => {
125
+ const committed = await loadCommitted();
126
+ expect(committed.agent_tasks?.progress).toBe("scrubbed");
127
+
128
+ const agent = await createAgent({
129
+ name: "text-columns-writer",
130
+ isLead: false,
131
+ status: "idle",
132
+ capabilities: [],
133
+ });
134
+ const task = await createTask(agent.id, "classification writer check");
135
+ await updateTaskProgress(task.id, `step 2 of 3 token=${secret.value} done`);
136
+
137
+ const row = await getDbClient().get<{ progress: string }>(
138
+ "SELECT progress FROM agent_tasks WHERE id = ?",
139
+ [task.id],
140
+ );
141
+ expect(row?.progress).toContain("step 2 of 3");
142
+ expect(row?.progress).toContain(`[REDACTED:${secret.name}]`);
143
+ expect(row?.progress.includes(secret.value)).toBe(false);
144
+ });
145
+ });
@@ -639,7 +639,7 @@ describe("preseedClaudeTrustDialog", () => {
639
639
  const data = JSON.parse(await readFile(join(homeDir, ".claude.json"), "utf-8"));
640
640
  expect(data.projects["/a"].hasTrustDialogAccepted).toBe(true);
641
641
  expect(data.projects["/b"].hasTrustDialogAccepted).toBe(true);
642
- expect(await readdir(homeDir)).toEqual([".claude.json", ".claude.json.swarm-lock"]);
642
+ expect((await readdir(homeDir)).sort()).toEqual([".claude.json", ".claude.json.swarm-lock"]);
643
643
  });
644
644
 
645
645
  test("many writers on an expired mkdir lock keep every entry of every round", async () => {
@@ -1,3 +1,5 @@
1
+ import { rename } from "node:fs/promises";
2
+
1
3
  const mode = process.env.CLAUDE_SDK_FIXTURE_MODE;
2
4
  const stateFile = process.env.CLAUDE_SDK_FIXTURE_STATE_FILE;
3
5
  const sessionId = "11111111-1111-4111-8111-111111111111";
@@ -13,6 +15,15 @@ if (!mode || !stateFile) {
13
15
  process.exit(2);
14
16
  }
15
17
 
18
+ // The test polls for the state file and parses it as soon as it exists.
19
+ // Bun.write creates the file before filling it, so write-then-rename keeps
20
+ // the reader from ever seeing an empty file.
21
+ async function writeStateFile(path: string, state: Record<string, unknown>): Promise<void> {
22
+ const temporaryPath = `${path}.tmp`;
23
+ await Bun.write(temporaryPath, JSON.stringify(state));
24
+ await rename(temporaryPath, path);
25
+ }
26
+
16
27
  function writeMessage(message: Record<string, unknown>): void {
17
28
  console.log(JSON.stringify(message));
18
29
  }
@@ -145,7 +156,7 @@ if (mode === "cancel") {
145
156
  stdout: "ignore",
146
157
  stderr: "ignore",
147
158
  });
148
- await Bun.write(stateFile, JSON.stringify({ fixturePid: process.pid, childPid: child.pid }));
159
+ await writeStateFile(stateFile, { fixturePid: process.pid, childPid: child.pid });
149
160
  }
150
161
 
151
162
  async function handleLine(line: string): Promise<boolean> {
@@ -191,7 +202,7 @@ async function handleLine(line: string): Promise<boolean> {
191
202
  }
192
203
 
193
204
  if (mode !== "queue" || userMessages.length < 3) return false;
194
- await Bun.write(stateFile, JSON.stringify({ argv, userMessages }));
205
+ await writeStateFile(stateFile, { argv, userMessages });
195
206
  for (let index = 0; index < userMessages.length; index++) {
196
207
  const turn = index + 1;
197
208
  writeMessage(assistantMessage(`accepted:${userMessages[index]}`, turn));
@@ -1,15 +1,8 @@
1
1
  import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
2
2
  import { createServer, type Server } from "node:http";
3
3
  import { decryptSecret, encryptSecret, getEncryptionKey } from "../be/crypto";
4
- import {
5
- closeDb,
6
- getDbClient,
7
- getKv,
8
- getSwarmConfigs,
9
- initDb,
10
- upsertKv,
11
- upsertSwarmConfig,
12
- } from "../be/db";
4
+ import { closeDb, getDbClient, getSwarmConfigs, initDb, upsertSwarmConfig } from "../be/db";
5
+ import { getCodexDeviceFlow, upsertCodexDeviceFlow } from "../be/db/codex-oauth-device-flows";
13
6
  import type { OnboardingState } from "../be/onboarding";
14
7
  import { handleCodexOAuthDevice } from "../http/codex-oauth-device";
15
8
  import { handleCore } from "../http/core";
@@ -97,7 +90,7 @@ afterAll(async () => {
97
90
  });
98
91
 
99
92
  beforeEach(async () => {
100
- await getDbClient().run("DELETE FROM kv_entries");
93
+ await getDbClient().run("DELETE FROM codex_oauth_device_flows");
101
94
  await getDbClient().run("DELETE FROM swarm_config");
102
95
  fetchImpl = async () => {
103
96
  throw new Error("Unexpected fetch");
@@ -221,8 +214,8 @@ describe("Codex device HTTP routes", () => {
221
214
 
222
215
  const started = await startFlow();
223
216
  expect(Number.isNaN(Date.parse(started.expiresAt))).toBe(false);
224
- const encrypted = await getKv("codex-oauth-device", started.flowId);
225
- expect(encrypted?.value).not.toContain("device-auth-full");
217
+ const encrypted = await getCodexDeviceFlow(started.flowId);
218
+ expect(encrypted?.state).not.toContain("device-auth-full");
226
219
 
227
220
  const poll = await request(`/api/codex-oauth/device/${started.flowId}/poll`);
228
221
  expect(await poll.json()).toEqual({ status: "complete", slot: 1 });
@@ -318,17 +311,15 @@ describe("Codex device HTTP routes", () => {
318
311
  const secondResponse = await request(path);
319
312
  const secondBody = await secondResponse.json();
320
313
  const callsBeforeRelease = { pollCalls, exchangeCalls };
321
- const entry = await getKv("codex-oauth-device", started.flowId);
322
- const state = JSON.parse(decryptSecret(String(entry!.value), getEncryptionKey())) as Record<
314
+ const entry = await getCodexDeviceFlow(started.flowId);
315
+ const state = JSON.parse(decryptSecret(entry!.state, getEncryptionKey())) as Record<
323
316
  string,
324
317
  unknown
325
318
  >;
326
319
  state.pollLeaseExpiresAt = Date.now() - 1;
327
- await upsertKv({
328
- namespace: "codex-oauth-device",
329
- key: started.flowId,
330
- value: encryptSecret(JSON.stringify(state), getEncryptionKey()),
331
- valueType: "string",
320
+ await upsertCodexDeviceFlow({
321
+ flowId: started.flowId,
322
+ state: encryptSecret(JSON.stringify(state), getEncryptionKey()),
332
323
  expiresAt: Number(state.expiresAt),
333
324
  });
334
325
  releaseExchange();
@@ -359,19 +350,17 @@ describe("Codex device HTTP routes", () => {
359
350
  }) as typeof fetch;
360
351
 
361
352
  const started = await startFlow();
362
- const entry = await getKv("codex-oauth-device", started.flowId);
363
- const state = JSON.parse(decryptSecret(String(entry!.value), getEncryptionKey())) as Record<
353
+ const entry = await getCodexDeviceFlow(started.flowId);
354
+ const state = JSON.parse(decryptSecret(entry!.state, getEncryptionKey())) as Record<
364
355
  string,
365
356
  unknown
366
357
  >;
367
358
  state.lastPolledAt = Date.now() - 10_000;
368
359
  state.pollLeaseId = crypto.randomUUID();
369
360
  state.pollLeaseExpiresAt = Date.now() - 1;
370
- await upsertKv({
371
- namespace: "codex-oauth-device",
372
- key: started.flowId,
373
- value: encryptSecret(JSON.stringify(state), getEncryptionKey()),
374
- valueType: "string",
361
+ await upsertCodexDeviceFlow({
362
+ flowId: started.flowId,
363
+ state: encryptSecret(JSON.stringify(state), getEncryptionKey()),
375
364
  expiresAt: Number(state.expiresAt),
376
365
  });
377
366
 
@@ -380,9 +369,9 @@ describe("Codex device HTTP routes", () => {
380
369
  });
381
370
  expect(pollCalls).toBe(1);
382
371
 
383
- const recovered = await getKv("codex-oauth-device", started.flowId);
372
+ const recovered = await getCodexDeviceFlow(started.flowId);
384
373
  const recoveredState = JSON.parse(
385
- decryptSecret(String(recovered!.value), getEncryptionKey()),
374
+ decryptSecret(recovered!.state, getEncryptionKey()),
386
375
  ) as Record<string, unknown>;
387
376
  expect(recoveredState.pollLeaseId).toBeNull();
388
377
  expect(recoveredState.pollLeaseExpiresAt).toBeNull();
@@ -403,17 +392,15 @@ describe("Codex device HTTP routes", () => {
403
392
  }) as typeof fetch;
404
393
 
405
394
  const started = await startFlow();
406
- const entry = await getKv("codex-oauth-device", started.flowId);
407
- const state = JSON.parse(decryptSecret(String(entry!.value), getEncryptionKey())) as Record<
395
+ const entry = await getCodexDeviceFlow(started.flowId);
396
+ const state = JSON.parse(decryptSecret(entry!.state, getEncryptionKey())) as Record<
408
397
  string,
409
398
  unknown
410
399
  >;
411
400
  state.expiresAt = Date.now() - 1;
412
- await upsertKv({
413
- namespace: "codex-oauth-device",
414
- key: started.flowId,
415
- value: encryptSecret(JSON.stringify(state), getEncryptionKey()),
416
- valueType: "string",
401
+ await upsertCodexDeviceFlow({
402
+ flowId: started.flowId,
403
+ state: encryptSecret(JSON.stringify(state), getEncryptionKey()),
417
404
  expiresAt: Number(state.expiresAt),
418
405
  });
419
406
 
@@ -482,8 +469,7 @@ describe("Codex device HTTP routes", () => {
482
469
  }) as typeof fetch;
483
470
  await getDbClient().run(
484
471
  `CREATE TRIGGER fail_device_flow_write
485
- BEFORE INSERT ON kv_entries
486
- WHEN NEW.namespace = 'codex-oauth-device'
472
+ BEFORE INSERT ON codex_oauth_device_flows
487
473
  BEGIN
488
474
  SELECT RAISE(ABORT, 'forced local write failure');
489
475
  END`,
@@ -255,6 +255,35 @@ describe("POST /api/oauth/keep-warm/codex", () => {
255
255
  expect(body.results).toEqual([]);
256
256
  });
257
257
 
258
+ it("loads slots through MCP_BASE_URL, not the public base URL", async () => {
259
+ const savedMcp = process.env.MCP_BASE_URL;
260
+ const savedPublic = process.env.PUBLIC_MCP_BASE_URL;
261
+ process.env.MCP_BASE_URL = "http://swarm-internal.example.test:3013";
262
+ process.env.PUBLIC_MCP_BASE_URL = "https://swarm-public.example.test";
263
+ installMockTransport(new Map());
264
+ const transport = globalThis.fetch;
265
+ const requested: string[] = [];
266
+ globalThis.fetch = (async (url: string | URL | Request, init?: RequestInit) => {
267
+ requested.push(typeof url === "string" ? url : url.toString());
268
+ return transport(url, init);
269
+ }) as typeof fetch;
270
+
271
+ try {
272
+ const { req, res, captured } = fakeReqRes();
273
+ await handleCodexOAuthKeepWarm(req, res, ["api", "oauth", "keep-warm", "codex"]);
274
+
275
+ expect(captured.status).toBe(200);
276
+ const configLoad = requested.find((u) => u.includes("/api/config/resolved"));
277
+ expect(configLoad).toBeDefined();
278
+ expect(new URL(configLoad as string).origin).toBe("http://swarm-internal.example.test:3013");
279
+ } finally {
280
+ if (savedMcp === undefined) delete process.env.MCP_BASE_URL;
281
+ else process.env.MCP_BASE_URL = savedMcp;
282
+ if (savedPublic === undefined) delete process.env.PUBLIC_MCP_BASE_URL;
283
+ else process.env.PUBLIC_MCP_BASE_URL = savedPublic;
284
+ }
285
+ });
286
+
258
287
  it("does not match unrelated paths", async () => {
259
288
  const { req, res } = fakeReqRes();
260
289
  const handled = await handleCodexOAuthKeepWarm(req, res, [
@@ -0,0 +1,141 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { inspect } from "node:util";
3
+ import { installConsoleLogBridge, uninstallConsoleLogBridge } from "../otel-impl";
4
+ import { installConsoleScrub, uninstallConsoleScrub } from "../utils/console-scrub";
5
+
6
+ // Built at runtime so no secret-shaped literal sits in the repo.
7
+ const SECRET = `ghp_${"Q".repeat(36)}`;
8
+ const REDACTED = "[REDACTED:";
9
+
10
+ const METHODS = ["log", "info", "warn", "error", "debug"] as const;
11
+ type Method = (typeof METHODS)[number];
12
+
13
+ let saved: Record<Method, (...args: unknown[]) => void>;
14
+ let written: { method: Method; args: unknown[] }[];
15
+
16
+ // Stand-ins for the underlying stream writers; the scrub wraps these.
17
+ beforeEach(() => {
18
+ written = [];
19
+ saved = { ...console } as typeof saved;
20
+ for (const method of METHODS) {
21
+ console[method] = (...args: unknown[]) => {
22
+ written.push({ method, args });
23
+ };
24
+ }
25
+ });
26
+
27
+ afterEach(() => {
28
+ uninstallConsoleLogBridge();
29
+ uninstallConsoleScrub();
30
+ for (const method of METHODS) console[method] = saved[method];
31
+ });
32
+
33
+ function lastLine(): string {
34
+ const entry = written.at(-1);
35
+ if (!entry) throw new Error("nothing written");
36
+ expect(entry.args).toHaveLength(1);
37
+ return entry.args[0] as string;
38
+ }
39
+
40
+ describe("installConsoleScrub", () => {
41
+ test("redacts strings, Error message and stack, and objects on every method", () => {
42
+ installConsoleScrub();
43
+ for (const method of METHODS) {
44
+ console[method]("token is", SECRET);
45
+ const line = lastLine();
46
+ expect(written.at(-1)?.method).toBe(method);
47
+ expect(line).toStartWith("token is ");
48
+ expect(line).not.toContain(SECRET);
49
+ expect(line).toContain(REDACTED);
50
+ }
51
+
52
+ const err = new Error(`auth failed with ${SECRET}`);
53
+ console.error("boom:", err);
54
+ const errLine = lastLine();
55
+ expect(errLine).toContain("auth failed with");
56
+ expect(errLine).toContain("console-scrub.test.ts"); // the stack survived
57
+ expect(errLine).not.toContain(SECRET);
58
+ expect(errLine).toContain(REDACTED);
59
+
60
+ console.log({ service: "github", nested: { value: SECRET } });
61
+ const objLine = lastLine();
62
+ expect(objLine).toContain("github");
63
+ expect(objLine).not.toContain(SECRET);
64
+ expect(objLine).toContain(REDACTED);
65
+ });
66
+
67
+ test("keeps printf-style substitution", () => {
68
+ installConsoleScrub();
69
+ console.log("%s has %d items and key %s", "queue", 3, SECRET);
70
+ const line = lastLine();
71
+ expect(line).toStartWith("queue has 3 items and key ");
72
+ expect(line).not.toContain(SECRET);
73
+ expect(line).not.toContain("%s");
74
+ });
75
+
76
+ test("a second install is a no-op", () => {
77
+ installConsoleScrub();
78
+ const wrapped = console.log;
79
+ installConsoleScrub();
80
+ expect(console.log).toBe(wrapped);
81
+ uninstallConsoleScrub();
82
+ console.log("plain", SECRET);
83
+ expect(written.at(-1)?.args).toEqual(["plain", SECRET]);
84
+ });
85
+
86
+ test("an OTel bridge installed afterwards forwards only scrubbed text", () => {
87
+ installConsoleScrub();
88
+ installConsoleLogBridge();
89
+ console.warn(`leaked ${SECRET}`);
90
+ const line = lastLine();
91
+ expect(line).not.toContain(SECRET);
92
+ expect(line).toContain(REDACTED);
93
+ });
94
+
95
+ test("a log written by a custom inspector during formatting fails closed", () => {
96
+ installConsoleScrub();
97
+ const noisy = {
98
+ [inspect.custom]() {
99
+ console.log("nested", SECRET);
100
+ return "noisy-object";
101
+ },
102
+ };
103
+ console.log("outer", noisy);
104
+ expect(written).toHaveLength(2);
105
+ const [nested, outer] = written.map((entry) => entry.args);
106
+ expect(nested).toEqual([
107
+ "[console-scrub] suppressed a log line written while formatting another",
108
+ ]);
109
+ expect(outer).toEqual(["outer noisy-object"]);
110
+ expect(JSON.stringify(written)).not.toContain(SECRET);
111
+ });
112
+
113
+ test("a formatting throw writes a fixed line, never the raw arguments", () => {
114
+ installConsoleScrub();
115
+ const broken = {
116
+ [inspect.custom]() {
117
+ throw new Error(`inspector failed near ${SECRET}`);
118
+ },
119
+ };
120
+ console.error("payload", SECRET, broken);
121
+ expect(written).toHaveLength(1);
122
+ expect(written[0]?.args).toEqual(["[console-scrub] failed to scrub log line"]);
123
+ expect(JSON.stringify(written)).not.toContain(SECRET);
124
+ });
125
+
126
+ test("zero arguments still print an empty line", () => {
127
+ installConsoleScrub();
128
+ console.log();
129
+ expect(written.at(-1)?.args).toEqual([]);
130
+ });
131
+
132
+ test("10k lines stay cheap", () => {
133
+ installConsoleScrub();
134
+ const started = performance.now();
135
+ for (let i = 0; i < 10_000; i++) console.log(`[worker] polling task ${i}`, { attempt: i });
136
+ const elapsedMs = performance.now() - started;
137
+ expect(written).toHaveLength(10_000);
138
+ // Generous bound for slow CI runners; measured about 450 ms in a worker container.
139
+ expect(elapsedMs).toBeLessThan(3_000);
140
+ });
141
+ });