@desplega.ai/agent-swarm 1.165.0 → 1.167.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 (296) hide show
  1. package/dist/acp-adapter-zpvy6321.js +34 -0
  2. package/dist/{actions-jna0b3wm.js → actions-fq2zrwtf.js} +30 -34
  3. package/dist/{amp-adapter-0eve81pz.js → amp-adapter-5gbk7501.js} +7 -7
  4. package/dist/{anthropic-messages-hst62ws9.js → anthropic-messages-13yn502s.js} +4 -4
  5. package/dist/{app-75hm5cfa.js → app-nctetn1e.js} +11 -11
  6. package/dist/{assistant-yzd51j33.js → assistant-0aq8gjvj.js} +31 -35
  7. package/dist/{azure-openai-responses-mh8tvf78.js → azure-openai-responses-9st34pfz.js} +5 -5
  8. package/dist/{boot-reembed-z158xq32.js → boot-reembed-1ze5853n.js} +14 -16
  9. package/dist/{boot-reembed-2ex2mcsh.js → boot-reembed-fmp5ww69.js} +15 -16
  10. package/dist/boot-scrub-sweep-cbp8xd4e.js +212 -0
  11. package/dist/{boot-seeds-2bmnh18p.js → boot-seeds-22tx8a53.js} +13 -12
  12. package/dist/{claude-adapter-jbj1mycz.js → claude-adapter-0jhgeh1d.js} +25 -25
  13. package/dist/{claude-managed-adapter-17ggz09t.js → claude-managed-adapter-tzwxy803.js} +5 -5
  14. package/dist/{claude-managed-setup-ptm1nzm2.js → claude-managed-setup-nrhfx5b0.js} +5 -5
  15. package/dist/{claude-sdk-session-9re1g7r2.js → claude-sdk-session-wh77rh83.js} +2632 -2614
  16. package/dist/{cli-zsh093hk.js → cli-014vns2y.js} +63 -1
  17. package/dist/{cli-qvfk83fy.js → cli-13xwcqvj.js} +2 -2
  18. package/dist/{cli-8gpbwy09.js → cli-1yrcfa5s.js} +5 -5
  19. package/dist/{cli-xtgtyafv.js → cli-577bxkf6.js} +212 -451
  20. package/dist/{cli-ma3zvh39.js → cli-5qkcdv8c.js} +6118 -5874
  21. package/dist/{cli-cwxhbf43.js → cli-5t5my0h8.js} +85 -2
  22. package/dist/{cli-zvckv750.js → cli-69egj654.js} +2 -2
  23. package/dist/{cli-9xb89xca.js → cli-6bpvv8h4.js} +9 -9
  24. package/dist/{cli-90v942q1.js → cli-6qxmjpem.js} +8 -8
  25. package/dist/{cli-9866bd07.js → cli-6vfz44sr.js} +1 -1
  26. package/dist/{cli-32bca7nr.js → cli-7avy4v8w.js} +1 -1
  27. package/dist/{cli-xb3c6aat.js → cli-7mhy3ths.js} +21 -18
  28. package/dist/{cli-q38n4bq0.js → cli-95zs84qw.js} +3 -3
  29. package/dist/cli-a35hzdve.js +33 -0
  30. package/dist/{cli-x3x0qwky.js → cli-bxqez1p0.js} +51 -23
  31. package/dist/{cli-prt9emct.js → cli-c2tpw9ck.js} +3 -3
  32. package/dist/{cli-0n4fasz0.js → cli-c8r99akr.js} +16 -16
  33. package/dist/{cli-5anb0ev3.js → cli-cexctgn5.js} +89 -26
  34. package/dist/{cli-bns5c8k9.js → cli-cf2vgn9d.js} +105 -91
  35. package/dist/{cli-ng97fjq7.js → cli-cmxkbzzs.js} +33 -7
  36. package/dist/{cli-nexwh8zg.js → cli-g7k7zbcz.js} +12 -5
  37. package/dist/{cli-mmzwwptd.js → cli-hf5s9pgf.js} +1 -1
  38. package/dist/{cli-nvvfd8bq.js → cli-jbzbr36n.js} +4091 -3793
  39. package/dist/{cli-zn2v3t8v.js → cli-jvmtxqtc.js} +27 -15
  40. package/dist/{cli-pkke6geq.js → cli-km4x66j0.js} +83 -65
  41. package/dist/cli-kw0jcfje.js +516 -0
  42. package/dist/{cli-28smfvbn.js → cli-m7vmeepg.js} +32 -19
  43. package/dist/{cli-hpryr6em.js → cli-mb75wpxf.js} +1 -1
  44. package/dist/{cli-qdv8kgz7.js → cli-mkdsy1nq.js} +14018 -3333
  45. package/dist/{cli-b0f70z24.js → cli-mn8qkd8v.js} +1 -1
  46. package/dist/{cli-420bdgw0.js → cli-mv2520nm.js} +1 -1
  47. package/dist/{cli-n9ehc28t.js → cli-mxry91sa.js} +31 -10
  48. package/dist/{cli-wfajtmf7.js → cli-mxvnnv5q.js} +596 -13
  49. package/dist/cli-nkqjj42q.js +104 -0
  50. package/dist/{cli-e7t4snpa.js → cli-p8rt8spp.js} +5 -5
  51. package/dist/{cli-gh5yqzpe.js → cli-pkj1w46r.js} +12 -7
  52. package/dist/cli-q1vnz8wv.js +96 -0
  53. package/dist/{cli-fj86xr3c.js → cli-qd97v6z2.js} +2 -2
  54. package/dist/{cli-ba66wyhy.js → cli-qzmzwegq.js} +60 -20
  55. package/dist/{cli-5b2fjjzh.js → cli-r7eeddkq.js} +14 -9
  56. package/dist/{cli-8ssjez7e.js → cli-rhy7z1rp.js} +22 -11
  57. package/dist/{cli-4mevd1hf.js → cli-s9y81s7j.js} +5 -5
  58. package/dist/{cli-59jn5twa.js → cli-tc5zec7v.js} +7 -7
  59. package/dist/{cli-ttbsse3f.js → cli-tmhsxhb8.js} +1 -1
  60. package/dist/{cli-k6xx3pve.js → cli-tx2zvn23.js} +4 -4
  61. package/dist/{cli-pjxnr1jn.js → cli-ty4bzkqr.js} +5 -5
  62. package/dist/{cli-nfygw434.js → cli-v1pwxreq.js} +55 -52
  63. package/dist/{cli-ftry7975.js → cli-v1w7pbd8.js} +3 -2
  64. package/dist/{cli-10zmp1p7.js → cli-vr5avem2.js} +27 -32
  65. package/dist/{cli-bzr0ftht.js → cli-wnb9b2g3.js} +19 -0
  66. package/dist/cli-wt7ccy4r.js +2654 -0
  67. package/dist/{cli-50eg13rj.js → cli-wydz3feg.js} +549 -321
  68. package/dist/{cli-krqm5f11.js → cli-xezn9nkh.js} +16 -13
  69. package/dist/{cli-8kt0bmgx.js → cli-xf713zz0.js} +2 -2
  70. package/dist/{cli-x7afk5wm.js → cli-xvc3q9br.js} +1 -1
  71. package/dist/{cli-9s3h836g.js → cli-xvdgasgy.js} +1 -1
  72. package/dist/{cli-j0pdrmwg.js → cli-y0kjpfm9.js} +3 -3
  73. package/dist/{cli-6zcfw0x7.js → cli-y195hv1a.js} +1 -1
  74. package/dist/{cli-amwhs8h2.js → cli-ykt8f7va.js} +22 -9
  75. package/dist/{cli-1kzfvd1z.js → cli-ymmc1zxg.js} +15 -11
  76. package/dist/{cli-xnzrzets.js → cli-ypgba1a8.js} +2 -2
  77. package/dist/{cli-bdv39kgj.js → cli-z1dqhc9y.js} +31 -20
  78. package/dist/{cli-fw6vdcq5.js → cli-zp67qg5x.js} +34 -1
  79. package/dist/{cli-qbksc292.js → cli-zy39d433.js} +2 -0
  80. package/dist/cli.js +36 -32
  81. package/dist/{cloudflare-workers-ai-system-one-tt484t61.js → cloudflare-workers-ai-system-one-sta45jf9.js} +7 -5
  82. package/dist/{codex-adapter-3qnxshje.js → codex-adapter-cgesnjkr.js} +20 -20
  83. package/dist/{codex-login-brkxhhdg.js → codex-login-g1cyt5zm.js} +2 -2
  84. package/dist/{codex-session-runner-612wnapp.js → codex-session-runner-8kd7ddhr.js} +20 -20
  85. package/dist/{commands-2ybfcxkj.js → commands-h6zaaxym.js} +11 -10
  86. package/dist/{cursor-adapter-p3jgdehv.js → cursor-adapter-v8mm6bp5.js} +7 -7
  87. package/dist/{db-f4ans0jj.js → db-qg3myn53.js} +13 -10
  88. package/dist/{devin-adapter-dhzt01ff.js → devin-adapter-bffjj5n4.js} +2 -2
  89. package/dist/{dispatcher-pk182apd.js → dispatcher-7vnwj9qw.js} +28 -32
  90. package/dist/{dsh-adapter-7twaeps1.js → dsh-adapter-tz6hfp5r.js} +7 -7
  91. package/dist/{e2b-y8t3jzyz.js → e2b-nd50eccg.js} +1 -1
  92. package/dist/{easter-egg-3d-mtpnw06s.js → easter-egg-3d-66zxervv.js} +2 -2
  93. package/dist/{event-bus-w7ac4n3c.js → event-bus-vnfthafg.js} +2 -2
  94. package/dist/{execute-s4ccjq3n.js → execute-h4jgevyw.js} +61 -11
  95. package/dist/{google-generative-ai-d6nphg3q.js → google-generative-ai-b8zxrpax.js} +5 -5
  96. package/dist/{google-vertex-cpshkref.js → google-vertex-nrdd697t.js} +5 -5
  97. package/dist/grok-adapter-jdt2dz2s.js +39 -0
  98. package/dist/{handlers-g2wnr4t9.js → handlers-2qtkpncr.js} +31 -35
  99. package/dist/{hook-f7jt9vnm.js → hook-51yfcgsv.js} +48 -39
  100. package/dist/{hook-bbdpnkb4.js → hook-qnbdgwvd.js} +15 -17
  101. package/dist/{http-w40q4w94.js → http-jsmtag08.js} +536 -231
  102. package/dist/{index-056j5kzn.js → index-2t9kesn1.js} +13 -12
  103. package/dist/{index-cj1r9q83.js → index-bt8wkv1w.js} +29 -33
  104. package/dist/{index-7n70jwbb.js → index-cx1sb9bq.js} +42 -35
  105. package/dist/{index-9bfssf54.js → index-ekywyv2v.js} +28 -32
  106. package/dist/{index-fdk1t4d6.js → index-n0rxa4ca.js} +11962 -9900
  107. package/dist/{index-k8xc083e.js → index-t4g06w93.js} +28 -32
  108. package/dist/{keepalive-1tj10tf4.js → keepalive-4aezx4bj.js} +16 -12
  109. package/dist/{lead-st0bg7v4.js → lead-r44bkpm5.js} +52 -56
  110. package/dist/{llama-cpp-classify-tgj35k8d.js → llama-cpp-classify-6erb7wwv.js} +3 -1
  111. package/dist/{maintenance-nz71zats.js → maintenance-grz5jrzh.js} +12 -13
  112. package/dist/{mistral-conversations-8445ddjn.js → mistral-conversations-059y51zt.js} +4 -4
  113. package/dist/{node-f0ykebbv.js → node-qm9eb6z0.js} +25 -7
  114. package/dist/{oauth-refresh-sweep-aefzwvvq.js → oauth-refresh-sweep-wm4ctjfj.js} +15 -11
  115. package/dist/{onboard-kgpr9mbn.js → onboard-v60zgqf0.js} +2 -2
  116. package/dist/{openai-codex-responses-9ynm88zy.js → openai-codex-responses-jzxkta9b.js} +8 -7
  117. package/dist/{openai-completions-nhqwjmaw.js → openai-completions-663yncfq.js} +4 -4
  118. package/dist/openai-decisions-vj9nkpf8.js +162 -0
  119. package/dist/{openai-responses-xx1c4kgm.js → openai-responses-m3g43rrt.js} +5 -5
  120. package/dist/{opencode-adapter-0c8e70dk.js → opencode-adapter-yjyqynzf.js} +8 -8
  121. package/dist/{openrouter-images-8yx3487g.js → openrouter-images-kr60n6b9.js} +1 -1
  122. package/dist/{otel-impl-x8zp4y5a.js → otel-impl-ghh4dyf8.js} +2 -2
  123. package/dist/{pi-messages-vxdt8v7j.js → pi-messages-xszk613n.js} +1 -1
  124. package/dist/{pi-mono-adapter-t280ah5c.js → pi-mono-adapter-0eg8kw2c.js} +69 -52
  125. package/dist/{pricing-refresh-9e0q7cwk.js → pricing-refresh-mjrqvnvw.js} +12 -11
  126. package/dist/{rbac-roles-790j3ga8.js → rbac-roles-axv4d74x.js} +11 -10
  127. package/dist/{rbac-roles-kcp6zge2.js → rbac-roles-fd5tskj9.js} +11 -10
  128. package/dist/{render-v2-tch4nefj.js → render-v2-k50j33qd.js} +11 -11
  129. package/dist/{runtime-v2t1r79e.js → runtime-h9ha1mq6.js} +4 -4
  130. package/dist/{setup-rcm303ns.js → setup-4syh0pd2.js} +2 -2
  131. package/dist/{typesafe-system-one-datfr82p.js → typesafe-system-one-e988vty7.js} +7 -5
  132. package/dist/{virtual-modules-wm8mxdxx.js → virtual-modules-ftr9kwe8.js} +11 -11
  133. package/dist/{work-objects-ctg7t70w.js → work-objects-jcdbg8rr.js} +32 -36
  134. package/dist/{worker-5dp0xm55.js → worker-x8j8c9sx.js} +52 -56
  135. package/dist/{x-v3yw66zs.js → x-ma4gnfgy.js} +2 -2
  136. package/openapi.json +5 -2
  137. package/package.json +15 -11
  138. package/src/be/automation-preflight.ts +9 -4
  139. package/src/be/boot-scrub-sweep.ts +302 -0
  140. package/src/be/crypto/key-bootstrap.ts +1 -1
  141. package/src/be/db/approvals.ts +15 -1
  142. package/src/be/db/codex-oauth-device-flows.ts +40 -0
  143. package/src/be/db/config-secrets.ts +31 -0
  144. package/src/be/db/runtime.ts +4 -0
  145. package/src/be/db/script-runs.ts +15 -0
  146. package/src/be/db/tasks/write.ts +1 -1
  147. package/src/be/db-queries/mcp-oauth.ts +5 -0
  148. package/src/be/db-queries/oauth.ts +77 -0
  149. package/src/be/db.ts +162 -59
  150. package/src/be/events.ts +17 -1
  151. package/src/be/memory/index-content.ts +2 -2
  152. package/src/be/memory/providers/openai-embedding.ts +7 -2
  153. package/src/be/memory/providers/sqlite-store.ts +63 -11
  154. package/src/be/memory/raters/llm-summarizer.ts +7 -2
  155. package/src/be/memory/types.ts +14 -0
  156. package/src/be/migrations/201_workflow_replay_columns.sql +10 -0
  157. package/src/be/migrations/202_codex_oauth_device_flows.sql +24 -0
  158. package/src/be/modelsdev-cache.json +61 -0
  159. package/src/be/scripts/db.ts +28 -4
  160. package/src/be/scripts/source-secrets.ts +37 -0
  161. package/src/be/scrub-json.ts +32 -0
  162. package/src/be/sealed-json.ts +59 -0
  163. package/src/be/secret-registry.ts +131 -0
  164. package/src/be/seed-pricing.ts +84 -53
  165. package/src/be/swarm-config-guard.ts +13 -0
  166. package/src/be/workflow-replay.ts +77 -0
  167. package/src/commands/provider-credentials.ts +28 -5
  168. package/src/commands/runner.ts +7 -12
  169. package/src/heartbeat/heartbeat.ts +91 -19
  170. package/src/hooks/hook.ts +72 -27
  171. package/src/http/codex-oauth-device.ts +8 -10
  172. package/src/http/db-query-bounded.ts +3 -0
  173. package/src/http/db-query-shared.ts +107 -15
  174. package/src/http/db-query.ts +10 -2
  175. package/src/http/index.ts +23 -6
  176. package/src/http/mcp.ts +5 -0
  177. package/src/http/onboarding.ts +2 -0
  178. package/src/http/script-runs.ts +9 -1
  179. package/src/http/scripts.ts +15 -0
  180. package/src/http/workflows.ts +3 -2
  181. package/src/oauth/mcp-wrapper.ts +1 -1
  182. package/src/providers/acp-adapter.ts +25 -11
  183. package/src/providers/acp-target-catalog.ts +35 -1
  184. package/src/providers/acp-targets.ts +78 -7
  185. package/src/providers/amp-adapter.ts +1 -1
  186. package/src/providers/claude-adapter.ts +26 -0
  187. package/src/providers/codex-adapter.ts +30 -3
  188. package/src/providers/codex-oauth/auth-json.ts +21 -4
  189. package/src/providers/codex-oauth/types.ts +2 -0
  190. package/src/providers/cursor-adapter.ts +1 -1
  191. package/src/providers/dsh-adapter.ts +10 -1
  192. package/src/providers/grok-adapter.ts +18 -2
  193. package/src/providers/opencode-adapter.ts +21 -5
  194. package/src/providers/pi-mono-extension.ts +56 -25
  195. package/src/providers/pricing-sources.md +2 -0
  196. package/src/script-workflows/supervisor.ts +11 -2
  197. package/src/scripts-runtime/eval-harness.ts +2 -12
  198. package/src/scripts-runtime/executors/quickjs-bundle.ts +152 -0
  199. package/src/scripts-runtime/executors/quickjs-prelude.ts +285 -0
  200. package/src/scripts-runtime/executors/quickjs-runner.ts +580 -0
  201. package/src/scripts-runtime/executors/quickjs-worker.ts +81 -0
  202. package/src/scripts-runtime/executors/quickjs.ts +196 -0
  203. package/src/scripts-runtime/executors/registry.ts +2 -0
  204. package/src/scripts-runtime/executors/sourcemap.ts +79 -0
  205. package/src/scripts-runtime/script-error-hints.ts +14 -0
  206. package/src/server.ts +66 -4
  207. package/src/tests/acp-adapter.test.ts +277 -12
  208. package/src/tests/boot-scrub-sweep.test.ts +390 -0
  209. package/src/tests/check-text-columns.test.ts +145 -0
  210. package/src/tests/ci-unit-test-report.test.ts +73 -0
  211. package/src/tests/claude-default-route.test.ts +75 -0
  212. package/src/tests/claude-hook-output.test.ts +233 -0
  213. package/src/tests/claude-sdk-process.fixture.ts +13 -2
  214. package/src/tests/codex-adapter-pool-auth-revalidate.test.ts +4 -2
  215. package/src/tests/codex-adapter.test.ts +166 -26
  216. package/src/tests/codex-oauth-auth-timestamps.test.ts +76 -0
  217. package/src/tests/codex-oauth-device.test.ts +23 -37
  218. package/src/tests/codex-oauth.test.ts +4 -1
  219. package/src/tests/context-window.test.ts +2 -2
  220. package/src/tests/credential-check.test.ts +51 -1
  221. package/src/tests/db-query-denylist.test.ts +202 -0
  222. package/src/tests/db-task-read-contract.test.ts +1 -0
  223. package/src/tests/db-task-write-contract.test.ts +1 -0
  224. package/src/tests/dsh-adapter.test.ts +23 -1
  225. package/src/tests/enabled-tools.test.ts +269 -0
  226. package/src/tests/entrypoint-acp-target-binary.test.ts +31 -1
  227. package/src/tests/entrypoint-codex-oauth-seed.test.ts +12 -0
  228. package/src/tests/events-scrub.test.ts +88 -0
  229. package/src/tests/fixtures/secret-corpus.ts +320 -0
  230. package/src/tests/grok-adapter.test.ts +13 -0
  231. package/src/tests/haiku-5-5-pricing.test.ts +38 -0
  232. package/src/tests/heartbeat-checklist.test.ts +158 -1
  233. package/src/tests/onboarding.test.ts +44 -0
  234. package/src/tests/opencode-adapter.test.ts +66 -3
  235. package/src/tests/opencode-plugin.test.ts +37 -0
  236. package/src/tests/openrouter-attribution-call-sites.test.ts +204 -0
  237. package/src/tests/openrouter-base-url.test.ts +30 -39
  238. package/src/tests/pi-mono-extension.test.ts +161 -2
  239. package/src/tests/pricing-refresh.test.ts +70 -1
  240. package/src/tests/reasoning-effort.test.ts +3 -1
  241. package/src/tests/script-connections.test.ts +5 -0
  242. package/src/tests/script-executor-conformance.test.ts +3 -0
  243. package/src/tests/script-executor-quickjs.test.ts +442 -0
  244. package/src/tests/script-executor-registry.test.ts +6 -1
  245. package/src/tests/script-runs-http.test.ts +88 -1
  246. package/src/tests/script-runs-scrub.test.ts +275 -0
  247. package/src/tests/scripts-http.test.ts +82 -1
  248. package/src/tests/scrubbed-text-12e.test.ts +289 -0
  249. package/src/tests/sealed-key-guard.test.ts +166 -0
  250. package/src/tests/secret-canary-sweep.test.ts +958 -0
  251. package/src/tests/secret-registry.test.ts +355 -0
  252. package/src/tests/secret-rules.test.ts +230 -0
  253. package/src/tests/secret-scrubber.test.ts +3 -2
  254. package/src/tests/telemetry-trigger.test.ts +12 -1
  255. package/src/tests/workflow-llm.test.ts +14 -0
  256. package/src/tests/workflow-replay.test.ts +518 -0
  257. package/src/tests/workflow-scrub.test.ts +242 -0
  258. package/src/tests/workflow-system-one-decision-providers.test.ts +33 -0
  259. package/src/tools/db-query.ts +1 -1
  260. package/src/tools/inject-learning.ts +1 -1
  261. package/src/tools/request-human-input.ts +7 -2
  262. package/src/tools/workflows/get-workflow-run.ts +3 -2
  263. package/src/types.ts +1 -1
  264. package/src/utils/enabled-tools.ts +27 -0
  265. package/src/utils/internal-ai/complete-structured.ts +11 -3
  266. package/src/utils/openrouter-base-url.ts +23 -0
  267. package/src/utils/secret-rules.generated.ts +2055 -0
  268. package/src/utils/secret-scrubber.ts +363 -29
  269. package/src/workflows/engine.ts +39 -4
  270. package/src/workflows/executors/raw-llm.ts +1 -0
  271. package/src/workflows/executors/system-one-decision.ts +2 -0
  272. package/src/workflows/executors/validate.ts +1 -0
  273. package/src/workflows/executors/workflow-llm.ts +9 -2
  274. package/src/workflows/recovery.ts +10 -1
  275. package/src/workflows/resume.ts +62 -3
  276. package/src/workflows/retry-poller.ts +3 -0
  277. package/templates/skills/agent-browser/SKILL.md +2 -0
  278. package/templates/skills/agent-browser/content.md +2 -0
  279. package/templates/skills/code-quality/content.md +1 -0
  280. package/templates/skills/heartbeat-runbook/content.md +1 -1
  281. package/templates/skills/work-on-task/SKILL.md +4 -0
  282. package/templates/skills/work-on-task/content.md +4 -0
  283. package/templates/skills/workflow-iterate/content.md +9 -0
  284. package/dist/acp-adapter-w6zws6me.js +0 -23
  285. package/dist/boot-scrub-logs-246yn076.js +0 -99
  286. package/dist/cli-3a4m1aws.js +0 -572
  287. package/dist/cli-3dpj7atn.js +0 -11
  288. package/dist/cli-a87b04k8.js +0 -951
  289. package/dist/cli-cvk6yzw5.js +0 -40
  290. package/dist/cli-gvcgmx69.js +0 -183
  291. package/dist/cli-h1spv8v4.js +0 -427
  292. package/dist/cli-jqt9hrnz.js +0 -168
  293. package/dist/cli-yhmwmv0g.js +0 -9281
  294. package/dist/grok-adapter-yt9pab3k.js +0 -29
  295. package/src/be/boot-scrub-logs.ts +0 -132
  296. package/dist/{codex-hook-d2rkvf39.js → codex-hook-15dgc4cb.js} +1 -1
@@ -0,0 +1,302 @@
1
+ /**
2
+ * Versioned boot retro-sweep: re-scrub stored free-text columns whenever the
3
+ * redaction rules change.
4
+ *
5
+ * Rows written before a scrubber rule existed keep the secret it would now
6
+ * catch. This sweep re-runs `scrubSecrets` over every target column once per
7
+ * `SCRUBBER_RULES_VERSION` and redacts matching rows IN PLACE (irreversible;
8
+ * a redaction, not a deletion). The done marker is
9
+ * `seed_state(kind='maintenance', key='boot-scrub-v<N>')`, so bumping the
10
+ * version re-runs it on the next boot.
11
+ *
12
+ * Restart-safe: each target keeps an id cursor in seed_state
13
+ * (`boot-scrub-v<N>:<table>:cursor`), saved in the same transaction as the
14
+ * batch's writes, so a restart resumes after the last committed batch.
15
+ *
16
+ * Non-blocking: the regexes run OUTSIDE the write lock. Each batch is read,
17
+ * scrubbed in JS, and only the changed rows plus the cursor go into one short
18
+ * transaction. Every UPDATE is compare-and-set on the value that was read, so
19
+ * a row a concurrent writer moved in between is skipped, never clobbered. The
20
+ * loop yields between batches so /health and probes stay responsive.
21
+ *
22
+ * Replay state: `workflow_run_steps.output` and `workflow_runs.context` are
23
+ * also replayed by resume and recovery. A row written before the sealed
24
+ * `*_replay` columns existed has only the plain value, so before redacting it
25
+ * the sweep seals the original into the empty replay column (same CAS write).
26
+ *
27
+ * Logs counts only, never content.
28
+ */
29
+
30
+ import { contentSha256 } from "@/commands/profile-sync";
31
+ import { SCRUBBER_RULES_VERSION, scrubSecrets } from "../utils/secret-scrubber";
32
+ import { getDbClient } from "./db";
33
+ import type { DbExecutor, DbParam } from "./db-client";
34
+ import { getMemoryStore } from "./memory";
35
+ import type { MemoryScrubFields } from "./memory/types";
36
+ import { breaksJsonValidity } from "./scrub-json";
37
+ import { sealJson } from "./sealed-json";
38
+
39
+ const DEFAULT_BATCH_SIZE = 200;
40
+
41
+ /** Yield to the event loop so probes can respond. */
42
+ const yieldTick = () => new Promise<void>((r) => setTimeout(r, 5));
43
+
44
+ type SweepTarget = {
45
+ table: string;
46
+ columns: string[];
47
+ /** Recompute `hashColumn = sha256(sourceColumn)` when the source changes. */
48
+ hash?: { column: string; of: string };
49
+ /** Route writes through the memory store (FTS sync + embedding reset). */
50
+ memory?: boolean;
51
+ /**
52
+ * `column -> sealed replay column`. When the sweep redacts `column` and the
53
+ * replay column is NULL, it seals the original there first.
54
+ */
55
+ replay?: Record<string, string>;
56
+ };
57
+
58
+ /** Swept in this order. Every table has a TEXT `id` primary key. */
59
+ export const BOOT_SCRUB_TARGETS: readonly SweepTarget[] = [
60
+ { table: "session_logs", columns: ["content"] },
61
+ { table: "agent_tasks", columns: ["task", "output", "failureReason", "progress"] },
62
+ { table: "agent_memory", columns: ["name", "content", "summary"], memory: true },
63
+ {
64
+ table: "agent_memory_version",
65
+ columns: ["content"],
66
+ hash: { column: "contentHash", of: "content" },
67
+ },
68
+ { table: "events", columns: ["data"] },
69
+ {
70
+ table: "workflow_run_steps",
71
+ columns: ["input", "output", "error", "diagnostics"],
72
+ replay: { output: "output_replay" },
73
+ },
74
+ {
75
+ table: "workflow_runs",
76
+ columns: ["triggerData", "context", "error"],
77
+ replay: { context: "context_replay" },
78
+ },
79
+ ];
80
+
81
+ export type BootScrubTableStats = {
82
+ table: string;
83
+ scanned: number;
84
+ changed: number;
85
+ skippedInvalidJson: number;
86
+ };
87
+
88
+ export type BootScrubSweepOptions = {
89
+ /** Rules version to key on. Defaults to `SCRUBBER_RULES_VERSION` (tests inject). */
90
+ version?: number;
91
+ batchSize?: number;
92
+ /** Scrub function. Defaults to `scrubSecrets` (tests inject to force edge cases). */
93
+ scrub?: (text: string) => string;
94
+ /**
95
+ * Test hook, called after each batch commits with the ids it scanned. A
96
+ * throw aborts the run with the cursor already saved.
97
+ */
98
+ afterBatch?: (info: { table: string; ids: string[] }) => void | Promise<void>;
99
+ /** Re-embed redacted memory rows after the sweep. Default true. */
100
+ reembed?: boolean;
101
+ };
102
+
103
+ export function bootScrubDoneKey(version: number): string {
104
+ return `boot-scrub-v${version}`;
105
+ }
106
+
107
+ export function bootScrubCursorKey(version: number, table: string): string {
108
+ return `boot-scrub-v${version}:${table}:cursor`;
109
+ }
110
+
111
+ /**
112
+ * Run the sweep for the current rules version. Returns per-table stats, or
113
+ * null when this version was already swept.
114
+ */
115
+ export async function runBootScrubSweep(
116
+ opts: BootScrubSweepOptions = {},
117
+ ): Promise<BootScrubTableStats[] | null> {
118
+ const version = opts.version ?? SCRUBBER_RULES_VERSION;
119
+ const doneKey = bootScrubDoneKey(version);
120
+ const done = await getDbClient().get<{ key: string }>(
121
+ "SELECT key FROM seed_state WHERE kind = 'maintenance' AND key = ?",
122
+ [doneKey],
123
+ );
124
+ if (done) return null;
125
+
126
+ const tag = `boot-scrub-v${version}`;
127
+ console.log(`[${tag}] starting`);
128
+
129
+ const stats: BootScrubTableStats[] = [];
130
+ for (const target of BOOT_SCRUB_TARGETS) {
131
+ const tableStats = await sweepTarget(target, version, opts);
132
+ stats.push(tableStats);
133
+ console.log(
134
+ `${tag}: ${target.table} scanned=${tableStats.scanned} changed=${tableStats.changed} skipped_invalid_json=${tableStats.skippedInvalidJson}`,
135
+ );
136
+ }
137
+
138
+ await getDbClient().transaction(async (tx) => {
139
+ await tx.run(
140
+ `INSERT INTO seed_state (kind, key, seededHash, seededAt)
141
+ VALUES ('maintenance', ?, 'done', datetime('now'))
142
+ ON CONFLICT (kind, key) DO UPDATE SET seededHash = 'done', seededAt = datetime('now')`,
143
+ [doneKey],
144
+ );
145
+ await tx.run("DELETE FROM seed_state WHERE kind = 'maintenance' AND key LIKE ? ESCAPE '!'", [
146
+ `${doneKey.replace(/[!%_]/g, "!$&")}:%:cursor`,
147
+ ]);
148
+ });
149
+ console.log(`[${tag}] complete`);
150
+
151
+ const memoryChanged = stats.find((s) => s.table === "agent_memory")?.changed ?? 0;
152
+ if (memoryChanged > 0 && opts.reembed !== false) {
153
+ // Redacted memory rows lost their embedding; regenerate from scrubbed text.
154
+ const { runBootReembed } = await import("./memory/boot-reembed");
155
+ await runBootReembed();
156
+ }
157
+
158
+ return stats;
159
+ }
160
+
161
+ async function sweepTarget(
162
+ target: SweepTarget,
163
+ version: number,
164
+ opts: BootScrubSweepOptions,
165
+ ): Promise<BootScrubTableStats> {
166
+ const client = getDbClient();
167
+ const scrub = opts.scrub ?? scrubSecrets;
168
+ const batchSize = opts.batchSize ?? DEFAULT_BATCH_SIZE;
169
+ const cursorKey = bootScrubCursorKey(version, target.table);
170
+ const stats: BootScrubTableStats = {
171
+ table: target.table,
172
+ scanned: 0,
173
+ changed: 0,
174
+ skippedInvalidJson: 0,
175
+ };
176
+
177
+ let cursor =
178
+ (
179
+ await client.get<{ seededHash: string }>(
180
+ "SELECT seededHash FROM seed_state WHERE kind = 'maintenance' AND key = ?",
181
+ [cursorKey],
182
+ )
183
+ )?.seededHash ?? "";
184
+
185
+ const selectCols = [
186
+ "id",
187
+ ...target.columns,
188
+ ...(target.hash && !target.columns.includes(target.hash.column) ? [target.hash.column] : []),
189
+ ...Object.values(target.replay ?? {}),
190
+ ];
191
+
192
+ for (;;) {
193
+ const rows = await client.query<Record<string, string | null>>(
194
+ `SELECT ${selectCols.join(", ")} FROM ${target.table}
195
+ WHERE id > ? ORDER BY id ASC LIMIT ?`,
196
+ [cursor, batchSize],
197
+ );
198
+ if (rows.length === 0) break;
199
+
200
+ // Scrub outside the transaction: regexes never run under the write lock.
201
+ const pending: {
202
+ id: string;
203
+ before: Record<string, string | null>;
204
+ after: Record<string, string | null>;
205
+ }[] = [];
206
+ for (const row of rows) {
207
+ const after: Record<string, string | null> = {};
208
+ let differs = false;
209
+ let breaksJson = false;
210
+ for (const col of target.columns) {
211
+ const value = row[col];
212
+ if (typeof value !== "string" || value.length === 0) {
213
+ after[col] = value ?? null;
214
+ continue;
215
+ }
216
+ const cleaned = scrub(value);
217
+ after[col] = cleaned;
218
+ if (cleaned !== value) {
219
+ differs = true;
220
+ if (breaksJsonValidity(value, cleaned)) breaksJson = true;
221
+ }
222
+ }
223
+ if (!differs) continue;
224
+ if (breaksJson) {
225
+ stats.skippedInvalidJson++;
226
+ continue;
227
+ }
228
+ for (const [col, replayCol] of Object.entries(target.replay ?? {})) {
229
+ const value = row[col];
230
+ if (after[col] === value || typeof value !== "string" || row[replayCol] != null) continue;
231
+ try {
232
+ after[replayCol] = sealJson(JSON.parse(value));
233
+ } catch {
234
+ // Not JSON, so nothing replays it.
235
+ }
236
+ }
237
+ pending.push({ id: row.id as string, before: row, after });
238
+ }
239
+
240
+ const batchLastId = rows[rows.length - 1]!.id as string;
241
+ await client.transaction(async (tx) => {
242
+ for (const { id, before, after } of pending) {
243
+ const wrote = target.memory
244
+ ? await rewriteMemoryRow(id, before, after)
245
+ : await rewriteRow(tx, target, id, before, after);
246
+ if (wrote) stats.changed++;
247
+ }
248
+ await tx.run(
249
+ `INSERT INTO seed_state (kind, key, seededHash, seededAt)
250
+ VALUES ('maintenance', ?, ?, datetime('now'))
251
+ ON CONFLICT (kind, key) DO UPDATE SET seededHash = excluded.seededHash, seededAt = datetime('now')`,
252
+ [cursorKey, batchLastId],
253
+ );
254
+ });
255
+
256
+ stats.scanned += rows.length;
257
+ cursor = batchLastId;
258
+ await opts.afterBatch?.({ table: target.table, ids: rows.map((r) => r.id as string) });
259
+ await yieldTick();
260
+ }
261
+
262
+ return stats;
263
+ }
264
+
265
+ async function rewriteRow(
266
+ tx: DbExecutor,
267
+ target: SweepTarget,
268
+ id: string,
269
+ before: Record<string, string | null>,
270
+ after: Record<string, string | null>,
271
+ ): Promise<boolean> {
272
+ const changedCols = target.columns.filter((c) => after[c] !== before[c]);
273
+ // Replay columns the sweep fills: only while still NULL (guarded below).
274
+ const sealedCols = Object.values(target.replay ?? {}).filter((c) => after[c] !== undefined);
275
+ const sets = [...changedCols, ...sealedCols].map((c) => `${c} = ?`);
276
+ const params: DbParam[] = [...changedCols, ...sealedCols].map((c) => after[c] ?? null);
277
+ if (target.hash && changedCols.includes(target.hash.of)) {
278
+ sets.push(`${target.hash.column} = ?`);
279
+ params.push(contentSha256(after[target.hash.of] ?? ""));
280
+ }
281
+ // Compare-and-set on every changed column.
282
+ const guards = [...changedCols.map((c) => `${c} IS ?`), ...sealedCols.map((c) => `${c} IS NULL`)];
283
+ params.push(id, ...changedCols.map((c) => before[c] ?? null));
284
+ const result = await tx.run(
285
+ `UPDATE ${target.table} SET ${sets.join(", ")} WHERE id = ? AND ${guards.join(" AND ")}`,
286
+ params,
287
+ );
288
+ return result.changes > 0;
289
+ }
290
+
291
+ async function rewriteMemoryRow(
292
+ id: string,
293
+ before: Record<string, string | null>,
294
+ after: Record<string, string | null>,
295
+ ): Promise<boolean> {
296
+ const pick = (r: Record<string, string | null>): MemoryScrubFields => ({
297
+ name: r.name ?? "",
298
+ content: r.content ?? "",
299
+ summary: r.summary ?? null,
300
+ });
301
+ return getMemoryStore().rewriteForScrub(id, pick(before), pick(after));
302
+ }
@@ -116,7 +116,7 @@ export function resolveEncryptionKey(
116
116
 
117
117
  if (!allowGenerate) {
118
118
  throw new Error(
119
- `Refusing to auto-generate ${KEY_FILENAME} for an existing database with encrypted secret rows. Restore ${ENV_KEY}, ${ENV_KEY_FILE}, or ${keyFilePath} before booting.`,
119
+ `Refusing to auto-generate ${KEY_FILENAME} for an existing database with encrypted secret rows or sealed replay values. Restore ${ENV_KEY}, ${ENV_KEY_FILE}, or ${keyFilePath} before booting.`,
120
120
  );
121
121
  }
122
122
 
@@ -1,4 +1,6 @@
1
+ import { type ScrubbedText, scrubObject, scrubSecrets } from "../../utils/secret-scrubber";
1
2
  import { normalizeDate, normalizeDateRequired } from "../date-utils";
3
+ import { breaksJsonValidity } from "../scrub-json";
2
4
  import { getDbClient } from "./runtime";
3
5
 
4
6
  /**
@@ -126,7 +128,19 @@ export async function recordApprovalVotes(id: string, approvals: ApprovalVote[])
126
128
  SET approvals = ?, updatedAt = ?
127
129
  WHERE id = ? AND status = 'pending'
128
130
  RETURNING id`,
129
- [JSON.stringify(approvals), new Date().toISOString(), id],
131
+ [scrubApprovalVotesJson(approvals), new Date().toISOString(), id],
130
132
  );
131
133
  return row !== null && row !== undefined;
132
134
  }
135
+
136
+ /**
137
+ * Serializes votes for the `approvals` column with free-text answers scrubbed.
138
+ * Falls back to a per-value scrub when a redaction would break the JSON, so
139
+ * readers can always parse the column.
140
+ */
141
+ export function scrubApprovalVotesJson(approvals: ApprovalVote[]): ScrubbedText {
142
+ const raw = JSON.stringify(approvals);
143
+ const scrubbed = scrubSecrets(raw);
144
+ if (!breaksJsonValidity(raw, scrubbed)) return scrubbed;
145
+ return JSON.stringify(scrubObject(approvals)) as ScrubbedText;
146
+ }
@@ -0,0 +1,40 @@
1
+ import { getDbClient } from "./runtime";
2
+
3
+ /**
4
+ * Encrypted Codex device-login flow state. Kept out of `kv_entries` so
5
+ * db-query can deny it by table. Returns null if missing or expired; an
6
+ * expired row is deleted inline.
7
+ */
8
+ export async function getCodexDeviceFlow(
9
+ flowId: string,
10
+ ): Promise<{ state: string; expiresAt: number } | null> {
11
+ const row = await getDbClient().get<{ state: string; expires_at: number }>(
12
+ "SELECT state, expires_at FROM codex_oauth_device_flows WHERE flow_id = ?",
13
+ [flowId],
14
+ );
15
+ if (!row) return null;
16
+ if (row.expires_at <= Date.now()) {
17
+ await getDbClient().run("DELETE FROM codex_oauth_device_flows WHERE flow_id = ?", [flowId]);
18
+ return null;
19
+ }
20
+ return { state: row.state, expiresAt: row.expires_at };
21
+ }
22
+
23
+ /** Upsert one device-login flow and drop every expired flow. */
24
+ export async function upsertCodexDeviceFlow(input: {
25
+ flowId: string;
26
+ state: string;
27
+ expiresAt: number;
28
+ }): Promise<void> {
29
+ const now = Date.now();
30
+ await getDbClient().run("DELETE FROM codex_oauth_device_flows WHERE expires_at <= ?", [now]);
31
+ await getDbClient().run(
32
+ `INSERT INTO codex_oauth_device_flows (flow_id, state, expires_at, created_at, updated_at)
33
+ VALUES (?, ?, ?, ?, ?)
34
+ ON CONFLICT(flow_id) DO UPDATE SET
35
+ state = excluded.state,
36
+ expires_at = excluded.expires_at,
37
+ updated_at = excluded.updated_at`,
38
+ [input.flowId, input.state, input.expiresAt, now, now],
39
+ );
40
+ }
@@ -0,0 +1,31 @@
1
+ import { decryptSecret, getEncryptionKey } from "../crypto";
2
+ import { isReservedConfigKey } from "../swarm-config-guard";
3
+ import { getDbClient } from "./runtime";
4
+
5
+ /**
6
+ * Every stored secret config value (all scopes, incl. derived
7
+ * `connection.<slug>.secret` rows), decrypted, for the secret registry
8
+ * (src/be/secret-registry.ts). Reserved env-only keys are skipped: their live
9
+ * value is in env. A row that fails to decrypt is counted, not thrown, so one
10
+ * bad row cannot hide the rest.
11
+ */
12
+ export async function listStoredConfigSecrets(): Promise<{
13
+ secrets: { key: string; value: string }[];
14
+ failed: number;
15
+ }> {
16
+ const rows = await getDbClient().query<{ key: string; value: string; encrypted: number }>(
17
+ "SELECT key, value, encrypted FROM swarm_config WHERE isSecret = 1 OR encrypted = 1",
18
+ );
19
+ const secrets: { key: string; value: string }[] = [];
20
+ let failed = 0;
21
+ for (const row of rows) {
22
+ if (isReservedConfigKey(row.key)) continue;
23
+ try {
24
+ const value = row.encrypted === 1 ? decryptSecret(row.value, getEncryptionKey()) : row.value;
25
+ if (value) secrets.push({ key: row.key, value });
26
+ } catch {
27
+ failed++;
28
+ }
29
+ }
30
+ return { secrets, failed };
31
+ }
@@ -7,6 +7,7 @@ import { resolveEncryptionKey } from "../crypto";
7
7
  import { createBunSqliteClient, type DbClient } from "../db-client";
8
8
  import { runMigrations } from "../migrations/runner";
9
9
  import { autoEncryptLegacyOAuthSecrets } from "../oauth-encryption-backfill";
10
+ import { sealedReplayExistsSql } from "../sealed-json";
10
11
 
11
12
  let db: Database | null = null;
12
13
  let sqliteVecAvailable = false;
@@ -233,6 +234,9 @@ export function initDb(dbPath = "./agent-swarm-db.sqlite"): Database {
233
234
  WHERE clientSecretEncrypted = 1 AND clientSecret IS NOT NULL
234
235
  UNION ALL
235
236
  SELECT 1 FROM oauth_authorizations WHERE tokensEncrypted = 1
237
+ ${sealedReplayExistsSql()
238
+ .map((arm) => `UNION ALL ${arm}`)
239
+ .join("\n ")}
236
240
  ) AS present`,
237
241
  )
238
242
  .get()?.present ?? 0) === 1;
@@ -0,0 +1,15 @@
1
+ import { openSealedJson } from "../sealed-json";
2
+ import { getDbClient } from "./runtime";
3
+
4
+ /**
5
+ * The exact args a run executes with, or `undefined` when the run has no row.
6
+ * Only the supervisor may call this: everything else reads the redacted view
7
+ * on `ScriptRun.args`.
8
+ */
9
+ export async function getScriptRunExecutionArgs(id: string): Promise<unknown> {
10
+ const row = await getDbClient().get<{ args: string }>(
11
+ "SELECT args FROM script_runs WHERE id = ?",
12
+ [id],
13
+ );
14
+ return row ? openSealedJson(row.args) : undefined;
15
+ }
@@ -87,7 +87,7 @@ export async function createTask(
87
87
  id,
88
88
  defaultAssetKey("task", id),
89
89
  agentId,
90
- task,
90
+ scrubSecrets(task),
91
91
  "pending",
92
92
  source,
93
93
  options?.slackChannelId ?? null,
@@ -2,6 +2,7 @@ import { scrubSecrets } from "../../utils/secret-scrubber";
2
2
  import { decryptSecret, encryptSecret, getEncryptionKey } from "../crypto";
3
3
  import { normalizeDateRequired } from "../date-utils";
4
4
  import { getDbClient } from "../db";
5
+ import { oauthSecretName, registerStoredSecret } from "../secret-registry";
5
6
  import {
6
7
  type OAuthAuthorizationStatus,
7
8
  updateAuthorizationTokens,
@@ -266,6 +267,10 @@ async function upsertMcpApp(input: {
266
267
  input.dcrClientSecret == null || input.dcrClientSecret === ""
267
268
  ? (existing?.clientSecret ?? null)
268
269
  : encryptSecret(input.dcrClientSecret, getEncryptionKey());
270
+ registerStoredSecret(
271
+ input.dcrClientSecret,
272
+ oauthSecretName(`mcp-${input.mcpServerId}`, "client_secret"),
273
+ );
269
274
  // MCP OAuth applications are DCR-owned storage rows even when the client
270
275
  // credentials were supplied manually or preregistered. Preserve that exact
271
276
  // distinction in metadata for the legacy adapter boundary.
@@ -2,6 +2,7 @@ import type { OAuthApp, OAuthTokens } from "../../tracker/types";
2
2
  import { decryptSecret, encryptSecret, getEncryptionKey } from "../crypto";
3
3
  import { normalizeDateRequired } from "../date-utils";
4
4
  import { getDbClient } from "../db";
5
+ import { oauthSecretName, registerStoredSecret } from "../secret-registry";
5
6
 
6
7
  type OAuthAppRow = Omit<
7
8
  OAuthApp,
@@ -241,6 +242,7 @@ async function writeOAuthApp(
241
242
  const metadataProvided = data.metadata !== undefined;
242
243
  const lifted = metadataProvided ? storageMetadata(data.metadata as string) : null;
243
244
  const encryptedSecret = encryptSecret(data.clientSecret, getEncryptionKey());
245
+ registerStoredSecret(data.clientSecret, oauthSecretName(provider, "client_secret"));
244
246
  const scopeSeparator =
245
247
  data.scopeSeparator ?? existing?.scopeSeparator ?? (provider === "linear" ? "," : " ");
246
248
  const rotation =
@@ -373,6 +375,79 @@ export async function getAuthorizationById(id: string): Promise<OAuthAuthorizati
373
375
  return row ? normalizeAuthorization(row) : null;
374
376
  }
375
377
 
378
+ /** Register an authorization's new plaintext tokens with the secret registry,
379
+ * named by the owning app's provider (`mcp-<serverId>` for MCP apps). */
380
+ async function registerAuthorizationSecrets(
381
+ appId: string,
382
+ accessToken: string,
383
+ refreshToken: string | null | undefined,
384
+ ): Promise<void> {
385
+ const app = await getDbClient().get<{ provider: string }>(
386
+ "SELECT provider FROM oauth_apps WHERE id = ?",
387
+ [appId],
388
+ );
389
+ const provider = app?.provider ?? appId;
390
+ registerStoredSecret(accessToken, oauthSecretName(provider, "access_token"));
391
+ registerStoredSecret(refreshToken, oauthSecretName(provider, "refresh_token"));
392
+ }
393
+
394
+ export type StoredOAuthSecretKind = "client_secret" | "access_token" | "refresh_token";
395
+
396
+ /**
397
+ * Every stored OAuth credential, decrypted, for the secret registry
398
+ * (src/be/secret-registry.ts): client secrets of all apps (provider and
399
+ * MCP-managed) and the tokens of all authorizations, whatever their status.
400
+ * A row that fails to decrypt is counted, not thrown.
401
+ */
402
+ export async function listStoredOAuthSecrets(): Promise<{
403
+ secrets: { provider: string; kind: StoredOAuthSecretKind; value: string }[];
404
+ failed: number;
405
+ }> {
406
+ const secrets: { provider: string; kind: StoredOAuthSecretKind; value: string }[] = [];
407
+ let failed = 0;
408
+
409
+ const apps = await getDbClient().query<OAuthAppRow>("SELECT * FROM oauth_apps");
410
+ for (const row of apps) {
411
+ try {
412
+ const app = normalizeOAuthApp(row);
413
+ if (app.clientSecret) {
414
+ secrets.push({ provider: app.provider, kind: "client_secret", value: app.clientSecret });
415
+ }
416
+ } catch {
417
+ failed++;
418
+ }
419
+ }
420
+
421
+ const authorizations = await getDbClient().query<OAuthAuthorizationRow & { provider: string }>(
422
+ `SELECT z.*, a.provider AS provider
423
+ FROM oauth_authorizations z
424
+ JOIN oauth_apps a ON a.id = z.appId`,
425
+ );
426
+ for (const row of authorizations) {
427
+ try {
428
+ const authorization = normalizeAuthorization(row);
429
+ if (authorization.accessToken) {
430
+ secrets.push({
431
+ provider: row.provider,
432
+ kind: "access_token",
433
+ value: authorization.accessToken,
434
+ });
435
+ }
436
+ if (authorization.refreshToken) {
437
+ secrets.push({
438
+ provider: row.provider,
439
+ kind: "refresh_token",
440
+ value: authorization.refreshToken,
441
+ });
442
+ }
443
+ } catch {
444
+ failed++;
445
+ }
446
+ }
447
+
448
+ return { secrets, failed };
449
+ }
450
+
376
451
  export async function upsertAuthorization(data: {
377
452
  id?: string;
378
453
  appId: string;
@@ -402,6 +477,7 @@ export async function upsertAuthorization(data: {
402
477
  : data.refreshToken == null
403
478
  ? null
404
479
  : encryptSecret(data.refreshToken, key);
480
+ await registerAuthorizationSecrets(data.appId, data.accessToken, data.refreshToken);
405
481
 
406
482
  if (existing) {
407
483
  await getDbClient().run(
@@ -486,6 +562,7 @@ export async function updateAuthorizationTokens(
486
562
  : data.refreshToken == null
487
563
  ? null
488
564
  : encryptSecret(data.refreshToken, key);
565
+ await registerAuthorizationSecrets(existing.appId, data.accessToken, data.refreshToken);
489
566
  const result = await getDbClient().run(
490
567
  `UPDATE oauth_authorizations SET
491
568
  accessToken = ?, refreshToken = ?, expiresAt = ?, scope = ?,