@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
@@ -1,12 +1,14 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
2
  import {
3
3
  clearVolatileSecretsForTesting,
4
+ isSensitiveKey,
4
5
  refreshSecretScrubberCache,
5
6
  registerSensitiveKeyName,
6
7
  registerVolatileSecret,
7
8
  scrubObject,
8
9
  scrubSecrets,
9
10
  } from "../utils/secret-scrubber";
11
+ import { randomToken } from "./synthetic-secret-helpers";
10
12
 
11
13
  // Snapshot/restore process.env between tests so env-derived cache entries
12
14
  // don't leak across cases.
@@ -233,7 +235,8 @@ describe("scrubSecrets — regex patterns", () => {
233
235
 
234
236
  test("regex patterns catch tokens even when env is empty", () => {
235
237
  // Fresh env — no secrets registered — regex should still catch well-known shapes.
236
- const out = scrubSecrets("token=example-ghp_1234567890abcdefABCDEF1234567890ABCD");
238
+ // `note=` is not a sensitive key, so only the token-shape pass can fire.
239
+ const out = scrubSecrets(`note=example-${"ghp_"}1234567890abcdefABCDEF1234567890ABCD`);
237
240
  expect(out).toContain("[REDACTED:github_token]");
238
241
  });
239
242
 
@@ -538,6 +541,32 @@ describe("scrubSecrets — env dumps and escaped forms", () => {
538
541
  expect(scrubSecrets(jsonBody(s))).toBe(jsonBody(want));
539
542
  });
540
543
 
544
+ // retry: wall-clock bound on a shared CI runner; a quadratic rescan takes seconds.
545
+ test(
546
+ "an unterminated escaped JSON value resumes the key scan where it stopped",
547
+ () => {
548
+ // The value walk stops on the bare quote; the next key still redacts.
549
+ const s = String.raw`log \"token\":\"abc\\"password\":\"s3cr3tval\" end`;
550
+ expect(scrubSecrets(s)).toBe(
551
+ String.raw`log \"token\":\"abc\\"password\":\"[REDACTED:password]\" end`,
552
+ );
553
+
554
+ // ~200 KB single lines of escaped key fragments whose values never close.
555
+ for (const fragment of [
556
+ String.raw`\\\"token\\\":\\\"`,
557
+ String.raw`\"token\":\"\\\"`,
558
+ String.raw`\"token\":\"\\`,
559
+ String.raw`\"token\":\"`,
560
+ ]) {
561
+ const input = fragment.repeat(Math.ceil(200_000 / fragment.length));
562
+ const start = performance.now();
563
+ scrubSecrets(input);
564
+ expect(performance.now() - start).toBeLessThan(50);
565
+ }
566
+ },
567
+ { retry: 2 },
568
+ );
569
+
541
570
  test("a quoted value closed at end of line may span lines", () => {
542
571
  const dump = `${declareX("DEMO_MULTI_TOKEN", "line1\nline2")}\n${declareX("USER", "deploy")}`;
543
572
  const want = `declare -x DEMO_MULTI_TOKEN="[REDACTED:DEMO_MULTI_TOKEN]"\n${declareX("USER", "deploy")}`;
@@ -558,3 +587,290 @@ describe("scrubSecrets — env dumps and escaped forms", () => {
558
587
  }
559
588
  });
560
589
  });
590
+
591
+ // Every credential below is built at runtime; nothing secret-shaped is a literal.
592
+ function pemBlock(label: string): string {
593
+ const body = Array.from({ length: 5 }, () => randomToken(64)).join("\n");
594
+ return `-----BEGIN ${label}PRIVATE KEY-----\n${body}\n-----END ${label}PRIVATE KEY-----`;
595
+ }
596
+
597
+ interface ProbeCase {
598
+ shape: string;
599
+ input: string;
600
+ want: string;
601
+ secret: string;
602
+ }
603
+
604
+ function probeCases(): ProbeCase[] {
605
+ const cases: ProbeCase[] = [];
606
+ const add = (shape: string, secret: string, input: string, want: string) =>
607
+ cases.push({ shape, input, want, secret });
608
+ let s = randomToken(24);
609
+ add("bare PRIVATE_KEY=", s, `PRIVATE_KEY=${s}`, "PRIVATE_KEY=[REDACTED:PRIVATE_KEY]");
610
+ s = randomToken(24);
611
+ add("bare SECRET=", s, `SECRET=${s} next`, "SECRET=[REDACTED:SECRET] next");
612
+ s = randomToken(10);
613
+ add("bare PASSWORD=", s, `PASSWORD='${s}'`, "PASSWORD='[REDACTED:PASSWORD]'");
614
+ s = randomToken(32);
615
+ add("lowercase api_key=", s, `api_key=${s}`, "api_key=[REDACTED:api_key]");
616
+ s = randomToken(32);
617
+ add(
618
+ "dotted SLACK.BOT.TOKEN=",
619
+ s,
620
+ `SLACK.BOT.TOKEN=${s}`,
621
+ "SLACK.BOT.TOKEN=[REDACTED:SLACK.BOT.TOKEN]",
622
+ );
623
+ s = randomToken(40);
624
+ add(
625
+ "AWS_SECRET_ACCESS_KEY=",
626
+ s,
627
+ `export AWS_SECRET_ACCESS_KEY=${s}`,
628
+ "export AWS_SECRET_ACCESS_KEY=[REDACTED:AWS_SECRET_ACCESS_KEY]",
629
+ );
630
+ s = randomToken(40);
631
+ add(
632
+ "INI aws_secret_access_key = …",
633
+ s,
634
+ `[default]\naws_secret_access_key = ${s}\nregion = us-east-1`,
635
+ "[default]\naws_secret_access_key = [REDACTED:aws_secret_access_key]\nregion = us-east-1",
636
+ );
637
+ s = randomToken(20);
638
+ add(
639
+ "DATABASE_URL with userinfo",
640
+ s,
641
+ `DATABASE_URL=postgres://app:${s}@db.example:5432/x`,
642
+ "DATABASE_URL=postgres://app:[REDACTED:url_password]@db.example:5432/x",
643
+ );
644
+ s = randomToken(20);
645
+ add(
646
+ "https URL with userinfo",
647
+ s,
648
+ `git clone https://bob:${s}@git.example/r.git`,
649
+ "git clone https://bob:[REDACTED:url_password]@git.example/r.git",
650
+ );
651
+ for (const label of ["RSA ", "OPENSSH "]) {
652
+ const pem = pemBlock(label);
653
+ add(
654
+ `${label}PEM block`,
655
+ pem.split("\n")[2] ?? "",
656
+ `key:\n${pem}\ndone`,
657
+ `key:\n-----BEGIN ${label}PRIVATE KEY-----[REDACTED:private_key]-----END ${label}PRIVATE KEY-----\ndone`,
658
+ );
659
+ }
660
+ const gcpPem = pemBlock("");
661
+ add(
662
+ "GCP service-account JSON private_key",
663
+ gcpPem.split("\n")[2] ?? "",
664
+ JSON.stringify({
665
+ type: "service_account",
666
+ private_key: `${gcpPem}\n`,
667
+ client_email: "bot@x.example",
668
+ }),
669
+ '{"type":"service_account","private_key":"-----BEGIN PRIVATE KEY-----[REDACTED:private_key]-----END PRIVATE KEY-----\\n","client_email":"bot@x.example"}',
670
+ );
671
+ s = `${randomToken(30)}.${randomToken(20)}`;
672
+ add(
673
+ "Authorization: Bearer (raw)",
674
+ s,
675
+ `Authorization: Bearer ${s}\nAccept: */*`,
676
+ "Authorization: Bearer [REDACTED:authorization]\nAccept: */*",
677
+ );
678
+ s = randomToken(40);
679
+ add(
680
+ "Authorization: Bearer (JSON headers)",
681
+ s,
682
+ JSON.stringify({ headers: { Authorization: `Bearer ${s}`, Accept: "x" } }),
683
+ '{"headers":{"Authorization":"Bearer [REDACTED:authorization]","Accept":"x"}}',
684
+ );
685
+ s = randomToken(32);
686
+ add("x-api-key header", s, `x-api-key: ${s}`, "x-api-key: [REDACTED:x_api_key]");
687
+ s = randomToken(40);
688
+ add(
689
+ "curl -H 'Authorization: token …'",
690
+ s,
691
+ `curl -H 'Authorization: token ${s}' https://api.example/x`,
692
+ "curl -H 'Authorization: token [REDACTED:authorization]' https://api.example/x",
693
+ );
694
+ s = randomToken(14);
695
+ add(
696
+ "curl --password",
697
+ s,
698
+ `curl --user bob --password ${s} https://x.example`,
699
+ "curl --user bob --password [REDACTED:curl_password] https://x.example",
700
+ );
701
+ s = randomToken(14);
702
+ add(
703
+ "curl -u user:pass",
704
+ s,
705
+ `curl -u bob:${s} https://x.example`,
706
+ "curl -u bob:[REDACTED:curl_password] https://x.example",
707
+ );
708
+ s = `${randomToken(8)}\\"${randomToken(8)}`;
709
+ add(
710
+ 'JSON "password" (with an escaped quote)',
711
+ s,
712
+ `{"user":"bob","password":"${s}"}`,
713
+ '{"user":"bob","password":"[REDACTED:password]"}',
714
+ );
715
+ s = randomToken(32);
716
+ add('JSON "apiKey"', s, JSON.stringify({ apiKey: s }), '{"apiKey":"[REDACTED:apiKey]"}');
717
+ s = randomToken(32);
718
+ add(
719
+ 'JSON "client_secret"',
720
+ s,
721
+ JSON.stringify({ client_id: "abc", client_secret: s }),
722
+ '{"client_id":"abc","client_secret":"[REDACTED:client_secret]"}',
723
+ );
724
+ s = randomToken(32);
725
+ add(
726
+ "YAML token:",
727
+ s,
728
+ `auth:\n token: ${s}\n user: bob\n`,
729
+ "auth:\n token: [REDACTED:token]\n user: bob\n",
730
+ );
731
+ return cases;
732
+ }
733
+
734
+ describe("scrubSecrets — structured credentials (audit probe)", () => {
735
+ for (const { shape, input, want, secret } of probeCases()) {
736
+ test(shape, () => {
737
+ expect(secret.length).toBeGreaterThan(8);
738
+ // Raw, and JSON-escaped as the line lands in session_logs.
739
+ for (const [text, expected] of [
740
+ [input, want],
741
+ [jsonBody(input), jsonBody(want)],
742
+ ]) {
743
+ const out = scrubSecrets(text);
744
+ expect(out).not.toContain(secret);
745
+ expect(out).toContain("[REDACTED:");
746
+ expect(out).toBe(expected as string);
747
+ expect(scrubSecrets(out)).toBe(out);
748
+ }
749
+ });
750
+ }
751
+
752
+ test("a JSON private_key value is consumed whole across \\n escapes", () => {
753
+ const tail = randomToken(24);
754
+ const input = JSON.stringify({ private_key: `${randomToken(16)}\n${tail}\n` });
755
+ expect(scrubSecrets(input)).toBe('{"private_key":"[REDACTED:private_key]"}');
756
+ });
757
+
758
+ test("a sensitive key nested under a dotted path still counts", () => {
759
+ const s = randomToken(20);
760
+ expect(scrubSecrets(`config.SECRETS_ENCRYPTION_KEY=${s}`)).toBe(
761
+ "config.SECRETS_ENCRYPTION_KEY=[REDACTED:config.SECRETS_ENCRYPTION_KEY]",
762
+ );
763
+ });
764
+
765
+ test("isSensitiveKey normalizes case, camelCase, dots and dashes", () => {
766
+ for (const key of [
767
+ "apiKey",
768
+ "client_secret",
769
+ "x-api-key",
770
+ "slack.bot.token",
771
+ "secretAccessKey",
772
+ "SENTRY_DSN",
773
+ "DEPLOY_CREDENTIALS",
774
+ "passwd",
775
+ ]) {
776
+ expect(isSensitiveKey(key)).toBe(true);
777
+ }
778
+ for (const key of [
779
+ "PWD",
780
+ "SORT_KEY",
781
+ "primaryKey",
782
+ "contextKey",
783
+ "max_tokens",
784
+ "token_count",
785
+ "nextPageToken",
786
+ "GOOGLE_APPLICATION_CREDENTIALS",
787
+ "MCP_BASE_URL",
788
+ ]) {
789
+ expect(isSensitiveKey(key)).toBe(false);
790
+ }
791
+ });
792
+ });
793
+
794
+ describe("scrubSecrets — over-redaction guards", () => {
795
+ test("non-secret text stays byte-identical, raw and JSON-escaped", () => {
796
+ const sha = randomToken(40)
797
+ .toLowerCase()
798
+ .replace(/[^0-9a-f]/g, "0");
799
+ const png = `data:image/png;base64,${Buffer.from(randomToken(60)).toString("base64")}`;
800
+ const lines = [
801
+ "max_tokens=4096",
802
+ '"input_tokens": 1234',
803
+ "token_count: 12",
804
+ "PWD=/workspace",
805
+ "SORT_KEY=abc",
806
+ "primaryKey: id",
807
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: testing workflow syntax
808
+ "password: ${{ secrets.DB_PASSWORD }}",
809
+ "token: <your-token>",
810
+ "Authorization: Bearer [REDACTED:x]",
811
+ `commit ${sha}`,
812
+ crypto.randomUUID(),
813
+ `task: ${crypto.randomUUID()}`,
814
+ png,
815
+ "https://github.com/org/repo",
816
+ "const password = getPassword()",
817
+ // Code and prose that share a key name with a credential.
818
+ " token: string;",
819
+ " password: process.env.DB_PASSWORD",
820
+ "token = get_token()",
821
+ " password = pwd",
822
+ "Authorization: Bearer followed by the token",
823
+ '{"nextPageToken":"CAoQAA","secret":false,"token":null}',
824
+ "docker run -u 1000:1000 -u node:node img",
825
+ "echo $PW | docker login --password-stdin",
826
+ "http://localhost:3013/x?email=a@b.example",
827
+ "GOOGLE_APPLICATION_CREDENTIALS=/home/worker/.config/gcloud/sa.json",
828
+ " - uses: actions/checkout@v4",
829
+ "postgres://app:***@db.example/x",
830
+ ];
831
+ for (const line of lines) {
832
+ expect(scrubSecrets(line)).toBe(line);
833
+ expect(scrubSecrets(jsonBody(line))).toBe(jsonBody(line));
834
+ }
835
+ const all = lines.join("\n");
836
+ expect(scrubSecrets(all)).toBe(all);
837
+ expect(scrubSecrets(jsonBody(all))).toBe(jsonBody(all));
838
+ });
839
+
840
+ // Each input is ~200 KB and built to make a naive pattern backtrack.
841
+ // Allow shared-runner headroom; a real ReDoS on these inputs takes seconds.
842
+ // retry: tolerate transient contention without weakening the per-input guard.
843
+ test(
844
+ "new key-context rules stay linear on 200 KB adversarial input",
845
+ () => {
846
+ const inputs = [
847
+ `-----BEGIN RSA PRIVATE KEY-----${"A".repeat(200_000)}`,
848
+ "-----BEGIN RSA PRIVATE KEY-----\n".repeat(6_000),
849
+ `"password": "${"\\\\".repeat(100_000)}`,
850
+ `\\"password\\": \\"${"\\\\".repeat(100_000)}`,
851
+ `"password": "${"a".repeat(200_000)}`,
852
+ `\\"password\\": \\"${"a".repeat(200_000)}`,
853
+ `"password": "`.repeat(14_000),
854
+ `token: ${"a".repeat(200_000)} x`,
855
+ "token: a\n".repeat(25_000),
856
+ `password = ${"a".repeat(200_000)} x`,
857
+ "Authorization: Bearer ".repeat(9_000),
858
+ `Authorization: Bearer ${"A".repeat(200_000)}[`,
859
+ "a".repeat(200_000),
860
+ `https://${"u".repeat(200_000)}`,
861
+ "a://a:".repeat(33_000),
862
+ " -u a:".repeat(33_000),
863
+ `curl -u ${"a".repeat(200_000)}`,
864
+ "a.".repeat(100_000),
865
+ "\\n".repeat(100_000),
866
+ `"${"a".repeat(63)}":`.repeat(3_000),
867
+ ];
868
+ for (const input of inputs) {
869
+ const start = performance.now();
870
+ scrubSecrets(input);
871
+ expect(performance.now() - start).toBeLessThan(500);
872
+ }
873
+ },
874
+ { retry: 2 },
875
+ );
876
+ });
@@ -344,6 +344,23 @@ describe("Phase 3 — session cost recompute golden fixtures", () => {
344
344
  expect(auto.cost.costSource).toBe("unpriced");
345
345
  });
346
346
 
347
+ // Boot-seeded rows only: grok rows project the models.dev xAI section.
348
+ test("grok runs price from the seeded xAI rates", async () => {
349
+ const body = await postCost({
350
+ sessionId: "grok-4-6",
351
+ provider: "grok",
352
+ model: "grok-4.6",
353
+ totalCostUsd: 0,
354
+ inputTokens: 2_000,
355
+ outputTokens: 300,
356
+ cacheReadTokens: 1_000,
357
+ });
358
+ const expected = (2_000 * 2 + 1_000 * 0.5 + 300 * 6) / 1_000_000;
359
+
360
+ expect(body.cost.costSource).toBe("pricing-table");
361
+ expectExact(body.cost.totalCostUsd, expected);
362
+ });
363
+
347
364
  test("legacy cache writes without a TTL split keep the 5m class", async () => {
348
365
  await seedModelRates("claude", "claude-legacy-cache-write", {
349
366
  input: 5,
@@ -0,0 +1,216 @@
1
+ import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test";
2
+ import { App, webApi } from "@slack/bolt";
3
+ import { sendKapsoText } from "../integrations/kapso/client";
4
+ import { installSlackEgressScrub } from "../slack/egress-scrub";
5
+ import type { ExecutorDependencies } from "../workflows/executors/base";
6
+ import { NotifyExecutor } from "../workflows/executors/notify";
7
+
8
+ // Built at runtime so no secret-shaped literal sits in the repo.
9
+ const SECRET = `ghp_${"Z".repeat(36)}`;
10
+ const REDACTED = "[REDACTED:";
11
+
12
+ type Captured = { method: string; body: string; params: URLSearchParams };
13
+
14
+ const captured: Captured[] = [];
15
+ const uploads: Uint8Array[] = [];
16
+ let server: ReturnType<typeof Bun.serve>;
17
+ let baseUrl = "";
18
+
19
+ beforeAll(() => {
20
+ installSlackEgressScrub();
21
+ server = Bun.serve({
22
+ port: 0,
23
+ async fetch(req) {
24
+ const { pathname } = new URL(req.url);
25
+ if (pathname.startsWith("/upload/")) {
26
+ // web-api posts the file as multipart form data under the `body` field.
27
+ const file = (await req.formData()).get("body") as Blob;
28
+ uploads.push(new Uint8Array(await file.arrayBuffer()));
29
+ return new Response("OK");
30
+ }
31
+ const method = pathname.replace(/^\/api\//, "");
32
+ const body = await req.text();
33
+ captured.push({ method, body, params: new URLSearchParams(body) });
34
+ if (method === "files.getUploadURLExternal") {
35
+ return Response.json({ ok: true, upload_url: `${baseUrl}upload/F1`, file_id: "F1" });
36
+ }
37
+ if (method === "files.completeUploadExternal") {
38
+ return Response.json({ ok: true, files: [{ id: "F1" }] });
39
+ }
40
+ return Response.json({ ok: true, ts: "1.000001", channel: "C1", messages: [] });
41
+ },
42
+ });
43
+ baseUrl = `http://localhost:${server.port}/`;
44
+ });
45
+
46
+ afterAll(() => {
47
+ server.stop(true);
48
+ });
49
+
50
+ afterEach(() => {
51
+ captured.length = 0;
52
+ uploads.length = 0;
53
+ });
54
+
55
+ function client(): InstanceType<typeof webApi.WebClient> {
56
+ return new webApi.WebClient("xoxb-test", {
57
+ slackApiUrl: `${baseUrl}api/`,
58
+ retryConfig: { retries: 0 },
59
+ });
60
+ }
61
+
62
+ function lastCall(method: string): Captured {
63
+ const call = captured.findLast((c) => c.method === method);
64
+ if (!call) throw new Error(`no ${method} call captured`);
65
+ return call;
66
+ }
67
+
68
+ function expectRedacted(text: string | null | undefined): void {
69
+ expect(text).toBeTruthy();
70
+ expect(text).not.toContain(SECRET);
71
+ expect(text).toContain(REDACTED);
72
+ }
73
+
74
+ describe("Slack client egress scrub", () => {
75
+ test("chat.postMessage scrubs text and keeps Block Kit structure", async () => {
76
+ await client().chat.postMessage({
77
+ channel: "C1",
78
+ text: `deploy key ${SECRET}`,
79
+ unfurl_links: false,
80
+ unfurl_media: false,
81
+ blocks: [
82
+ { type: "section", text: { type: "mrkdwn", text: `token: ${SECRET} end` } },
83
+ { type: "divider" },
84
+ ],
85
+ });
86
+ const call = lastCall("chat.postMessage");
87
+ expect(call.body).not.toContain(SECRET);
88
+ expectRedacted(call.params.get("text"));
89
+ const blocks = JSON.parse(call.params.get("blocks") ?? "null");
90
+ expect(Array.isArray(blocks)).toBe(true);
91
+ expect(blocks).toHaveLength(2);
92
+ expect(blocks[0].type).toBe("section");
93
+ expect(blocks[0].text.text).toStartWith("token: ");
94
+ expect(blocks[0].text.text).toEndWith(" end");
95
+ expectRedacted(blocks[0].text.text);
96
+ expect(blocks[1]).toEqual({ type: "divider" });
97
+ expect(call.params.get("channel")).toBe("C1");
98
+ });
99
+
100
+ test("chat.update and chat.postEphemeral are scrubbed", async () => {
101
+ const c = client();
102
+ await c.chat.update({
103
+ channel: "C1",
104
+ ts: "1.0",
105
+ text: `edit ${SECRET}`,
106
+ unfurl_links: false,
107
+ unfurl_media: false,
108
+ });
109
+ await c.chat.postEphemeral({ channel: "C1", user: "U1", text: `psst ${SECRET}` });
110
+ expectRedacted(lastCall("chat.update").params.get("text"));
111
+ expectRedacted(lastCall("chat.postEphemeral").params.get("text"));
112
+ });
113
+
114
+ test("files.uploadV2 scrubs text content, comment and title", async () => {
115
+ await client().files.uploadV2({
116
+ channel_id: "C1",
117
+ content: `line one\nexport GH=${SECRET}\n`,
118
+ filename: "report.txt",
119
+ title: `report ${SECRET}`,
120
+ initial_comment: `see ${SECRET}`,
121
+ });
122
+ expect(uploads).toHaveLength(1);
123
+ const uploaded = new TextDecoder().decode(uploads[0]);
124
+ expect(uploaded).toStartWith("line one\n");
125
+ expectRedacted(uploaded);
126
+ const complete = lastCall("files.completeUploadExternal");
127
+ expect(complete.body).not.toContain(SECRET);
128
+ expectRedacted(complete.params.get("initial_comment"));
129
+ expectRedacted(complete.params.get("files"));
130
+ });
131
+
132
+ test("binary uploads pass through byte-identical", async () => {
133
+ const bytes = Buffer.concat([Buffer.from([0, 255, 1, 254]), Buffer.from(SECRET)]);
134
+ await client().files.uploadV2({ channel_id: "C1", file: bytes, filename: "blob.bin" });
135
+ expect(uploads).toHaveLength(1);
136
+ expect(Buffer.from(uploads[0]).equals(bytes)).toBe(true);
137
+ });
138
+
139
+ test("a second install does not double-wrap", async () => {
140
+ const proto = webApi.WebClient.prototype;
141
+ const before = proto.apiCall;
142
+ installSlackEgressScrub();
143
+ expect(proto.apiCall).toBe(before);
144
+ });
145
+
146
+ test("a Bolt App built after install scrubs app.client sends", async () => {
147
+ const app = new App({
148
+ token: "xoxb-test",
149
+ signingSecret: "test-signing-secret",
150
+ tokenVerificationEnabled: false,
151
+ clientOptions: { slackApiUrl: `${baseUrl}api/`, retryConfig: { retries: 0 } },
152
+ });
153
+ await app.client.chat.postMessage({
154
+ channel: "C1",
155
+ text: `bolt ${SECRET}`,
156
+ unfurl_links: false,
157
+ unfurl_media: false,
158
+ });
159
+ expectRedacted(lastCall("chat.postMessage").params.get("text"));
160
+ });
161
+ });
162
+
163
+ describe("WhatsApp and notify egress scrub", () => {
164
+ const originalFetch = globalThis.fetch;
165
+ afterEach(() => {
166
+ globalThis.fetch = originalFetch;
167
+ });
168
+
169
+ test("sendKapsoText scrubs the message body", async () => {
170
+ let sent = "";
171
+ globalThis.fetch = (async (_url: string, init: RequestInit) => {
172
+ sent = init.body as string;
173
+ return Response.json({ messages: [{ id: "wamid.X" }] });
174
+ }) as typeof fetch;
175
+ const result = await sendKapsoText({
176
+ apiBaseUrl: "https://kapso.invalid",
177
+ apiKey: "k",
178
+ phoneNumberId: "1",
179
+ to: "2",
180
+ body: `hola ${SECRET}`,
181
+ });
182
+ expect(result.ok).toBe(true);
183
+ const body = JSON.parse(sent) as { text: { body: string } };
184
+ expect(body.text.body).toStartWith("hola ");
185
+ expectRedacted(body.text.body);
186
+ });
187
+
188
+ test("notify executor scrubs the message before posting and in its output", async () => {
189
+ const posted: string[] = [];
190
+ const deps = {
191
+ db: {
192
+ postMessage: (_channel: string, _agent: string | null, content: string) => {
193
+ posted.push(content);
194
+ return { id: "m1" };
195
+ },
196
+ },
197
+ eventBus: { emit: () => {}, on: () => {}, off: () => {} },
198
+ interpolate: (template: string, ctx: Record<string, unknown>) =>
199
+ template.replace("{{v}}", String(ctx.v)),
200
+ } as unknown as ExecutorDependencies;
201
+ const result = await new NotifyExecutor(deps).run({
202
+ config: { channel: "swarm", target: "chan", template: "value {{v}}" },
203
+ context: { v: SECRET },
204
+ meta: {
205
+ runId: "00000000-0000-0000-0000-000000000001",
206
+ stepId: "00000000-0000-0000-0000-000000000002",
207
+ nodeId: "n",
208
+ workflowId: "00000000-0000-0000-0000-000000000003",
209
+ dryRun: false,
210
+ },
211
+ });
212
+ expect(result.status).toBe("success");
213
+ expectRedacted(posted[0]);
214
+ expectRedacted((result.output as { message: string }).message);
215
+ });
216
+ });