@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
@@ -1,4 +1,5 @@
1
1
  import OpenAI from "openai";
2
+ import { getOpenRouterAttributionHeaders } from "../../../utils/openrouter-base-url";
2
3
  import { DEFAULT_EMBEDDING_MODEL, EMBEDDING_DIMENSIONS } from "../constants";
3
4
  import type { EmbeddingProvider } from "../types";
4
5
 
@@ -34,11 +35,15 @@ export class OpenAIEmbeddingProvider implements EmbeddingProvider {
34
35
 
35
36
  private getClient(): OpenAI | null {
36
37
  if (!this.apiKey) return null;
37
- if (!this.client)
38
+ if (!this.client) {
39
+ const baseURL = process.env.EMBEDDING_API_BASE_URL; // Optional custom base URL
38
40
  this.client = new OpenAI({
39
- baseURL: process.env.EMBEDDING_API_BASE_URL, // Optional custom base URL
41
+ baseURL,
40
42
  apiKey: this.apiKey,
43
+ // Only an openrouter.ai base URL gets attribution; OpenAI and gateways get none.
44
+ defaultHeaders: getOpenRouterAttributionHeaders(baseURL ?? ""),
41
45
  });
46
+ }
42
47
  return this.client;
43
48
  }
44
49
 
@@ -2,6 +2,7 @@ import { getDb, getDbClient, isSqliteVecAvailable } from "@/be/db";
2
2
  import { cosineSimilarity, deserializeEmbedding, serializeEmbedding } from "@/be/embedding";
3
3
  import { contentSha256 } from "@/commands/profile-sync";
4
4
  import type { AgentMemory, AgentMemoryScope, AgentMemorySource } from "@/types";
5
+ import { scrubSecrets } from "@/utils/secret-scrubber";
5
6
  import {
6
7
  EMBEDDING_DIMENSIONS,
7
8
  isHybridSearchEnabled,
@@ -19,6 +20,7 @@ import type {
19
20
  MemoryInput,
20
21
  MemoryListOptions,
21
22
  MemoryRetrievalSource,
23
+ MemoryScrubFields,
22
24
  MemorySearchOptions,
23
25
  MemoryStats,
24
26
  MemoryStore,
@@ -437,7 +439,12 @@ export class SqliteMemoryStore implements MemoryStore {
437
439
  const now = new Date().toISOString();
438
440
  const key = input.key ?? `${input.scope}/${input.source}/${id}`;
439
441
  const expiresAt = computeExpiresAt(input.source, key);
440
- const contentHash = contentSha256(input.content);
442
+ // Scrub before the hash, the FTS sync and the caller's embedding (callers
443
+ // embed the returned row's content), so all of them see the same text.
444
+ const name = scrubSecrets(input.name);
445
+ const content = scrubSecrets(input.content);
446
+ const summary = input.summary == null ? null : scrubSecrets(input.summary);
447
+ const contentHash = contentSha256(content);
441
448
  const version = 1;
442
449
 
443
450
  const row = await getDbClient().transaction(async (tx) => {
@@ -449,15 +456,15 @@ export class SqliteMemoryStore implements MemoryStore {
449
456
  input.agentId ?? null,
450
457
  input.scope,
451
458
  key,
452
- input.name,
453
- input.content,
454
- input.summary ?? null,
459
+ name,
460
+ content,
461
+ summary,
455
462
  input.source,
456
463
  input.sourceTaskId ?? null,
457
464
  input.sourcePath ?? null,
458
465
  input.chunkIndex ?? 0,
459
466
  input.totalChunks ?? 1,
460
- JSON.stringify(input.tags ?? []),
467
+ JSON.stringify((input.tags ?? []).map((tag) => scrubSecrets(tag))),
461
468
  now,
462
469
  now,
463
470
  now,
@@ -478,7 +485,7 @@ export class SqliteMemoryStore implements MemoryStore {
478
485
  crypto.randomUUID(),
479
486
  inserted.id,
480
487
  version,
481
- input.content,
488
+ content,
482
489
  contentHash,
483
490
  input.intent ?? "create memory",
484
491
  input.agentId ?? null,
@@ -1009,13 +1016,17 @@ export class SqliteMemoryStore implements MemoryStore {
1009
1016
 
1010
1017
  const previousVersion = row.version ?? 1;
1011
1018
  const moving = input.newKey !== undefined && input.newKey !== row.key;
1019
+ // Scrub the edited text before the hash and FTS sync; callers embed the
1020
+ // returned content, so the vector matches the stored row.
1012
1021
  const nextContent = moveOnly
1013
1022
  ? row.content
1014
- : applyEditMode(input.mode, row.content, {
1015
- content: input.content,
1016
- oldString: input.oldString,
1017
- newString: input.newString,
1018
- });
1023
+ : scrubSecrets(
1024
+ applyEditMode(input.mode, row.content, {
1025
+ content: input.content,
1026
+ oldString: input.oldString,
1027
+ newString: input.newString,
1028
+ }),
1029
+ );
1019
1030
 
1020
1031
  const nextHash = contentSha256(nextContent);
1021
1032
  const contentChanged = !moveOnly && nextHash !== row.contentHash;
@@ -1268,6 +1279,47 @@ export class SqliteMemoryStore implements MemoryStore {
1268
1279
  }
1269
1280
  }
1270
1281
 
1282
+ async rewriteForScrub(
1283
+ id: string,
1284
+ before: MemoryScrubFields,
1285
+ after: MemoryScrubFields,
1286
+ ): Promise<boolean> {
1287
+ const contentChanged = after.content !== before.content;
1288
+ return getDbClient().transaction(async (tx) => {
1289
+ // Compare-and-set: the sweep scrubbed `before` outside the write lock, so
1290
+ // skip the row if a concurrent write moved it since.
1291
+ const result = await tx.run(
1292
+ `UPDATE agent_memory
1293
+ SET name = ?, content = ?, summary = ?, contentHash = ?,
1294
+ embedding = CASE WHEN ? = 1 THEN NULL ELSE embedding END,
1295
+ embeddingModel = CASE WHEN ? = 1 THEN NULL ELSE embeddingModel END
1296
+ WHERE id = ? AND name IS ? AND content IS ? AND summary IS ?`,
1297
+ [
1298
+ after.name,
1299
+ after.content,
1300
+ after.summary,
1301
+ contentSha256(after.content),
1302
+ contentChanged ? 1 : 0,
1303
+ contentChanged ? 1 : 0,
1304
+ id,
1305
+ before.name,
1306
+ before.content,
1307
+ before.summary,
1308
+ ],
1309
+ );
1310
+ if (result.changes === 0) return false;
1311
+ // The vector embeds the old (secret-bearing) content. Drop it; the
1312
+ // re-embed backfill picks the row up via `embedding IS NULL`.
1313
+ if (contentChanged && this.vecInitialized && this.getVecTableSchema()) {
1314
+ await tx.run("DELETE FROM memory_vec WHERE memory_id = ?", [id]);
1315
+ }
1316
+ if (after.name !== before.name || contentChanged) {
1317
+ await this.syncFtsRow(id, after.name, after.content);
1318
+ }
1319
+ return true;
1320
+ });
1321
+ }
1322
+
1271
1323
  async getStats(agentId: string): Promise<MemoryStats> {
1272
1324
  const db = getDbClient();
1273
1325
 
@@ -12,7 +12,10 @@
12
12
  */
13
13
  import { z } from "zod";
14
14
  import { MEMORY_RATER_DEFAULT_MODEL } from "../../../utils/internal-ai/models";
15
- import { getOpenRouterBaseUrl } from "../../../utils/openrouter-base-url";
15
+ import {
16
+ getOpenRouterAttributionHeaders,
17
+ getOpenRouterBaseUrl,
18
+ } from "../../../utils/openrouter-base-url";
16
19
  import { type SummaryWithRatings, SummaryWithRatingsSchema } from "./llm";
17
20
 
18
21
  /**
@@ -150,11 +153,13 @@ export async function runMemoryRater(opts: RunMemoryRaterOpts): Promise<RunMemor
150
153
  const model = opts.model ?? getMemoryRaterModel();
151
154
  const responseLogCap = opts.responseLogCap ?? 200;
152
155
 
156
+ const url = openRouterChatCompletionsUrl();
153
157
  let res: Response;
154
158
  try {
155
- res = await fetchFn(openRouterChatCompletionsUrl(), {
159
+ res = await fetchFn(url, {
156
160
  method: "POST",
157
161
  headers: {
162
+ ...getOpenRouterAttributionHeaders(url),
158
163
  "Content-Type": "application/json",
159
164
  Authorization: `Bearer ${opts.apiKey}`,
160
165
  },
@@ -40,6 +40,17 @@ export interface MemoryStore {
40
40
  deleteBySourcePath(sourcePath: string, agentId: string): Promise<number>;
41
41
  purgeExpired(): Promise<number>;
42
42
  updateEmbedding(id: string, embedding: Float32Array, model: string): Promise<void>;
43
+ /**
44
+ * Redact a row in place (boot retro-sweep). Writes only if the row still
45
+ * holds `before`; keeps FTS in sync and drops the embedding when the
46
+ * content changed so the re-embed backfill regenerates it. Returns whether
47
+ * the row was written.
48
+ */
49
+ rewriteForScrub(
50
+ id: string,
51
+ before: MemoryScrubFields,
52
+ after: MemoryScrubFields,
53
+ ): Promise<boolean>;
43
54
  getStats(agentId: string): Promise<MemoryStats>;
44
55
  getHealth(): MemoryHealth;
45
56
  }
@@ -48,6 +59,9 @@ export interface MemoryStore {
48
59
  // Supporting types
49
60
  // ============================================================================
50
61
 
62
+ /** The free-text `agent_memory` columns the boot retro-sweep redacts. */
63
+ export type MemoryScrubFields = { name: string; content: string; summary: string | null };
64
+
51
65
  export interface MemoryInput {
52
66
  agentId: string | null;
53
67
  scope: AgentMemoryScope;
@@ -0,0 +1,10 @@
1
+ -- Byte-exact replay state for workflow runs, sealed at rest.
2
+ --
3
+ -- `workflow_run_steps.output` and `workflow_runs.context` are scrubbed at write
4
+ -- for display and SQL filters (`json_extract(context, '$.swarm.requestedByUserId')`).
5
+ -- Resume, retry, dedup and recovery rebuild the live ctx from these values, so
6
+ -- they need the exact value: these columns hold it, encrypted with `sealJson`
7
+ -- (src/be/sealed-json.ts). NULL for rows written before this migration; their
8
+ -- readers fall back to the plain column.
9
+ ALTER TABLE workflow_run_steps ADD COLUMN output_replay TEXT;
10
+ ALTER TABLE workflow_runs ADD COLUMN context_replay TEXT;
@@ -0,0 +1,24 @@
1
+ -- Codex device-login flow state, moved out of `kv_entries`.
2
+ --
3
+ -- `state` is the encrypted flow (device auth id, user code, poll lease). It
4
+ -- lived in `kv_entries` namespace 'codex-oauth-device', which db-query must
5
+ -- keep readable for general KV debugging. A dedicated table lets db-query deny
6
+ -- this store by table name, with no SQL-text parsing.
7
+ --
8
+ -- expires_at: unix-ms. Expired rows are deleted on read and on every write.
9
+
10
+ CREATE TABLE IF NOT EXISTS codex_oauth_device_flows (
11
+ flow_id TEXT PRIMARY KEY,
12
+ state TEXT NOT NULL,
13
+ expires_at INTEGER NOT NULL,
14
+ created_at INTEGER NOT NULL DEFAULT (CAST(unixepoch('subsec') * 1000 AS INTEGER)),
15
+ updated_at INTEGER NOT NULL DEFAULT (CAST(unixepoch('subsec') * 1000 AS INTEGER))
16
+ );
17
+
18
+ INSERT OR IGNORE INTO codex_oauth_device_flows (flow_id, state, expires_at, created_at, updated_at)
19
+ SELECT key, value, expires_at, created_at, updated_at
20
+ FROM kv_entries
21
+ WHERE namespace = 'codex-oauth-device'
22
+ AND expires_at IS NOT NULL;
23
+
24
+ DELETE FROM kv_entries WHERE namespace = 'codex-oauth-device';
@@ -147175,6 +147175,67 @@
147175
147175
  "name": "Anthropic",
147176
147176
  "doc": "https://docs.anthropic.com/en/docs/about-claude/models",
147177
147177
  "models": {
147178
+ "claude-haiku-5-5": {
147179
+ "id": "claude-haiku-5-5",
147180
+ "name": "Claude Haiku 5.5",
147181
+ "description": "Fast Claude model for responsive assistance, classification, and lightweight agents",
147182
+ "family": "claude-haiku",
147183
+ "attachment": true,
147184
+ "reasoning": true,
147185
+ "reasoning_options": [
147186
+ {
147187
+ "type": "toggle"
147188
+ },
147189
+ {
147190
+ "type": "effort",
147191
+ "values": [
147192
+ "low",
147193
+ "medium",
147194
+ "high",
147195
+ "xhigh",
147196
+ "max"
147197
+ ]
147198
+ }
147199
+ ],
147200
+ "tool_call": true,
147201
+ "structured_output": true,
147202
+ "temperature": false,
147203
+ "knowledge": "2026-06",
147204
+ "modalities": {
147205
+ "input": [
147206
+ "text",
147207
+ "image",
147208
+ "pdf"
147209
+ ],
147210
+ "output": [
147211
+ "text"
147212
+ ]
147213
+ },
147214
+ "open_weights": false,
147215
+ "limit": {
147216
+ "context": 1000000,
147217
+ "output": 128000
147218
+ },
147219
+ "cost": {
147220
+ "input": 0.1,
147221
+ "output": 0.5,
147222
+ "cache_read": 0.01,
147223
+ "cache_write": 0.125,
147224
+ "tiers": [
147225
+ {
147226
+ "input": 0.5,
147227
+ "output": 2.5,
147228
+ "cache_read": 0.05,
147229
+ "cache_write": 0.625,
147230
+ "tier": {
147231
+ "type": "context",
147232
+ "size": 100000
147233
+ }
147234
+ }
147235
+ ]
147236
+ },
147237
+ "canonical_model_id": "anthropic/claude-haiku-5-5"
147238
+ },
147178
147239
  "claude-sonnet-5-5": {
147179
147240
  "id": "claude-sonnet-5-5",
147180
147241
  "name": "Claude Sonnet 5.5",
@@ -8,10 +8,10 @@ import type {
8
8
  ScriptScope,
9
9
  ScriptVersionRecord,
10
10
  } from "../../types";
11
- import { registerVolatileSecret } from "../../utils/secret-scrubber";
12
11
  import { generateBearerToken, generateShortId } from "../../utils/short-id";
13
12
  import { decryptSecret, encryptSecret, getEncryptionKey } from "../crypto";
14
13
  import { computeContentHash, getDbClient } from "../db";
14
+ import { registerStoredSecret, scriptApiSecretName } from "../secret-registry";
15
15
  import { embedScript } from "./embeddings";
16
16
 
17
17
  type ScriptRow = Omit<ScriptRecord, "isScratch" | "typeChecked"> & {
@@ -517,7 +517,7 @@ export async function createScriptApi(args: {
517
517
  ],
518
518
  );
519
519
  if (!row) throw new Error("Failed to create script API endpoint");
520
- if (token) registerVolatileSecret(token, `script-api:${id}`);
520
+ registerStoredSecret(token, scriptApiSecretName(id));
521
521
  return { ...rowToScriptApi(row), token };
522
522
  } catch (err) {
523
523
  if (isUniqueConstraintError(err) && attempt < 4) continue;
@@ -527,6 +527,30 @@ export async function createScriptApi(args: {
527
527
  throw new Error("Failed to allocate a unique script API endpoint id");
528
528
  }
529
529
 
530
+ /**
531
+ * Every stored script API bearer token, decrypted, for the secret registry
532
+ * (src/be/secret-registry.ts). A row that fails to decrypt is counted, not thrown.
533
+ */
534
+ export async function listStoredScriptApiSecrets(): Promise<{
535
+ secrets: { id: string; token: string }[];
536
+ failed: number;
537
+ }> {
538
+ const rows = await getDbClient().query<{ id: string; bearerTokenEncrypted: string }>(
539
+ "SELECT id, bearerTokenEncrypted FROM script_apis WHERE bearerTokenEncrypted IS NOT NULL",
540
+ );
541
+ const secrets: { id: string; token: string }[] = [];
542
+ let failed = 0;
543
+ for (const row of rows) {
544
+ try {
545
+ const token = decryptSecret(row.bearerTokenEncrypted, getEncryptionKey());
546
+ if (token) secrets.push({ id: row.id, token });
547
+ } catch {
548
+ failed++;
549
+ }
550
+ }
551
+ return { secrets, failed };
552
+ }
553
+
530
554
  export async function listScriptApisForScript(scriptId: string): Promise<ScriptApiRecord[]> {
531
555
  const rows = await getDbClient().query<ScriptApiRow>(
532
556
  "SELECT * FROM script_apis WHERE scriptId = ? ORDER BY createdAt DESC",
@@ -554,7 +578,7 @@ export async function getScriptApiSecret(id: string): Promise<string | null> {
554
578
  );
555
579
  if (!row?.bearerTokenEncrypted) return null;
556
580
  const token = decryptSecret(row.bearerTokenEncrypted, getEncryptionKey());
557
- registerVolatileSecret(token, `script-api:${id}`);
581
+ registerStoredSecret(token, scriptApiSecretName(id));
558
582
  return token;
559
583
  }
560
584
 
@@ -604,7 +628,7 @@ export async function rotateScriptApiSecret(
604
628
  [encrypted, updatedBy ?? null, id],
605
629
  );
606
630
  if (!row) return null;
607
- registerVolatileSecret(token, `script-api:${id}`);
631
+ registerStoredSecret(token, scriptApiSecretName(id));
608
632
  return { ...rowToScriptApi(row), token };
609
633
  }
610
634
 
@@ -0,0 +1,37 @@
1
+ import { findSecretsIn } from "../../utils/secret-scrubber";
2
+
3
+ /**
4
+ * Executable source is stored and served verbatim (`scripts.source`,
5
+ * `script_versions.source`, `script_runs.source`): redacting it would change
6
+ * what runs, and sealing it would hide reviewed code. So a source that embeds
7
+ * a secret is refused at write instead, before anything runs or persists.
8
+ *
9
+ * Checked: every registered secret value, in each encoded form the known-value
10
+ * matcher holds, and the gitleaks vendor-token rules. The refusal names the
11
+ * secret's key or the rule id, never the value. Rows written before this check
12
+ * are left as they are.
13
+ */
14
+ export interface SourceSecretRefusal {
15
+ error: "source_contains_secret";
16
+ message: string;
17
+ findings: { kind: "registered-secret" | "gitleaks-rule"; id: string }[];
18
+ }
19
+
20
+ export function refuseSourceWithSecrets(source: string): SourceSecretRefusal | null {
21
+ const { knownValues, gitleaksRules } = findSecretsIn(source);
22
+ if (knownValues.length === 0 && gitleaksRules.length === 0) return null;
23
+ const findings = [
24
+ ...knownValues.map((id) => ({ kind: "registered-secret" as const, id })),
25
+ ...gitleaksRules.map((id) => ({ kind: "gitleaks-rule" as const, id })),
26
+ ];
27
+ const named = findings
28
+ .map((f) =>
29
+ f.kind === "registered-secret" ? `registered secret ${f.id}` : `gitleaks rule ${f.id}`,
30
+ )
31
+ .join(", ");
32
+ return {
33
+ error: "source_contains_secret",
34
+ message: `Script source contains a secret (${named}); nothing was saved or run. Source is stored and shown verbatim, so read secrets at run time instead: a credential binding (ctx.api.<slug> / ctx.mcp.<slug>) or ctx.swarm.config.get("<KEY>").`,
35
+ findings,
36
+ };
37
+ }
@@ -0,0 +1,32 @@
1
+ import { type ScrubbedText, scrubObject, scrubSecrets } from "../utils/secret-scrubber";
2
+
3
+ /** True when `before` parsed as JSON and `after` no longer does. */
4
+ export function breaksJsonValidity(before: string, after: string): boolean {
5
+ try {
6
+ JSON.parse(before);
7
+ } catch {
8
+ return false;
9
+ }
10
+ try {
11
+ JSON.parse(after);
12
+ return false;
13
+ } catch {
14
+ return true;
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Serialize `value` for a JSON TEXT column with secrets redacted.
20
+ *
21
+ * Scrubs the serialized text first, so context-keyed passes (`"password":
22
+ * "..."`, auth headers) see the key next to its value. When a redaction breaks
23
+ * JSON validity (a marker swallowed a quote or brace), falls back to scrubbing
24
+ * each string leaf, which always serializes cleanly. Readers keep `JSON.parse`.
25
+ */
26
+ export function scrubJsonValue(value: unknown): ScrubbedText {
27
+ const raw = JSON.stringify(value);
28
+ const scrubbed = scrubSecrets(raw);
29
+ if (!breaksJsonValidity(raw, scrubbed)) return scrubbed;
30
+ // Every string leaf went through scrubSecrets; the brand holds.
31
+ return JSON.stringify(scrubObject(value)) as ScrubbedText;
32
+ }
@@ -0,0 +1,59 @@
1
+ import { decryptSecret, encryptSecret, getEncryptionKey } from "./crypto";
2
+ import { scrubJsonValue } from "./scrub-json";
3
+
4
+ /**
5
+ * Sealed JSON: byte-exact replay state, encrypted at rest.
6
+ *
7
+ * Some JSON columns are execution inputs, not records. The supervisor relaunches
8
+ * a durable script run from `script_runs.args`, and the harness replays
9
+ * `script_run_journal.result` as a step's return value. Redacting them would
10
+ * change what the script sees after a restart, and storing them raw leaves a
11
+ * secret at rest. Sealing keeps the exact bytes for the replay readers and
12
+ * nothing readable for anything that scans the database.
13
+ *
14
+ * Readers pick a view: `openSealedJson` (replay, exact value) or
15
+ * `sealedJsonForDisplay` (API/UI, redacted). Rows written before sealing are
16
+ * plain JSON and both readers still accept them.
17
+ */
18
+
19
+ const SEALED_PREFIX = "sealed:v1:";
20
+
21
+ /**
22
+ * Every column a writer fills with `sealJson`. Boot counts a sealed value here
23
+ * as encrypted data, like an encrypted config or OAuth row: with the key
24
+ * missing, startup refuses instead of generating a replacement key that could
25
+ * never open them. Add a column here when a writer starts sealing it.
26
+ */
27
+ export const SEALED_REPLAY_COLUMNS: readonly { table: string; column: string }[] = [
28
+ { table: "script_runs", column: "args" },
29
+ { table: "script_run_journal", column: "result" },
30
+ { table: "workflow_run_steps", column: "output_replay" },
31
+ { table: "workflow_runs", column: "context_replay" },
32
+ { table: "approval_requests", column: "responses" },
33
+ ];
34
+
35
+ /** SQL `EXISTS` arms matching a sealed value in each `SEALED_REPLAY_COLUMNS` entry. */
36
+ export function sealedReplayExistsSql(): string[] {
37
+ return SEALED_REPLAY_COLUMNS.map(
38
+ ({ table, column }) => `SELECT 1 FROM ${table} WHERE ${column} GLOB '${SEALED_PREFIX}*'`,
39
+ );
40
+ }
41
+
42
+ export function sealJson(value: unknown): string {
43
+ return SEALED_PREFIX + encryptSecret(JSON.stringify(value), getEncryptionKey());
44
+ }
45
+
46
+ export function isSealedJson(text: string): boolean {
47
+ return text.startsWith(SEALED_PREFIX);
48
+ }
49
+
50
+ /** The exact stored value. Only execution and replay paths may call this. */
51
+ export function openSealedJson(text: string): unknown {
52
+ if (!isSealedJson(text)) return JSON.parse(text);
53
+ return JSON.parse(decryptSecret(text.slice(SEALED_PREFIX.length), getEncryptionKey()));
54
+ }
55
+
56
+ /** The stored value with secrets redacted, for API responses and the UI. */
57
+ export function sealedJsonForDisplay(text: string): unknown {
58
+ return JSON.parse(scrubJsonValue(openSealedJson(text)));
59
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * API-side registry of every stored secret value.
3
+ *
4
+ * The scrubber's exact-match pass only knows sensitive env values and values
5
+ * registered at runtime. Without this registry, a secret stored before the
6
+ * last restart (an agent-scoped config secret, an OAuth or MCP OAuth token, a
7
+ * script API bearer token) stays unknown until something reads it again, and
8
+ * the boot retro-sweep cannot redact a value it does not know.
9
+ *
10
+ * At boot, `loadSecretRegistry()` decrypts every stored secret and registers
11
+ * it. Each encrypt site calls `registerStoredSecret()` with the new plaintext,
12
+ * so a write after boot is covered without a reload.
13
+ *
14
+ * Feeds the scrubber only through `registerVolatileSecret`, so worker-side
15
+ * code is untouched. Append-only: a deleted or rotated secret stays
16
+ * registered, since redacting a revoked value is still correct. Never logs a
17
+ * value: names are source markers (`config:<KEY>`, `oauth:<provider>:…`,
18
+ * `script-api:<id>`), and load results are counts.
19
+ */
20
+ import { registerVolatileSecret } from "../utils/secret-scrubber";
21
+ import { listStoredConfigSecrets } from "./db/config-secrets";
22
+ import { listStoredOAuthSecrets, type StoredOAuthSecretKind } from "./db-queries/oauth";
23
+ import { listStoredScriptApiSecrets } from "./scripts/db";
24
+
25
+ export function configSecretName(key: string): string {
26
+ return `config:${key}`;
27
+ }
28
+
29
+ export function oauthSecretName(provider: string, kind: StoredOAuthSecretKind): string {
30
+ return `oauth:${provider}:${kind}`;
31
+ }
32
+
33
+ export function scriptApiSecretName(id: string): string {
34
+ return `script-api:${id}`;
35
+ }
36
+
37
+ /**
38
+ * Base64 text that encodes `value` wherever it sits in a larger byte string.
39
+ * Base64 maps 3 bytes to 4 chars, so the chars depend on the value's byte
40
+ * offset mod 3. For each offset, keep only the chars computed from the value's
41
+ * bytes alone: drop the leading chars that mix in preceding bytes and the
42
+ * trailing chars that depend on following bytes. Offset 0 also keeps the whole
43
+ * padded encoding (the value encoded on its own). This catches
44
+ * `Basic base64(user:secret)` and a secret inside an encoded JSON blob.
45
+ */
46
+ function base64Forms(value: string): string[] {
47
+ const bytes = Buffer.from(value, "utf8");
48
+ const forms = [bytes.toString("base64")];
49
+ for (const offset of [0, 1, 2]) {
50
+ const shifted = Buffer.concat([Buffer.alloc(offset), bytes]);
51
+ // Offset 1 mixes the prefix into the first 2 chars, offset 2 into the first 3.
52
+ const skip = offset === 0 ? 0 : offset + 1;
53
+ const end = Math.floor(shifted.length / 3) * 4;
54
+ forms.push(shifted.toString("base64").slice(skip, end));
55
+ }
56
+ const urlSafe = forms.map((form) =>
57
+ form.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""),
58
+ );
59
+ return [...forms, ...urlSafe];
60
+ }
61
+
62
+ /**
63
+ * Encoded forms of a known value: base64 (std and url-safe, see
64
+ * `base64Forms`) and URL percent-encoding. Shell and JSON escaping are not
65
+ * repeated here: `registerVolatileSecret` already adds those forms (and
66
+ * drops any form shorter than the scrubber's minimum length).
67
+ */
68
+ export function encodedForms(value: string): string[] {
69
+ const forms = new Set(base64Forms(value));
70
+ const uriEncoded = encodeURIComponent(value);
71
+ if (uriEncoded !== value) forms.add(uriEncoded);
72
+ forms.delete(value);
73
+ forms.delete("");
74
+ return [...forms];
75
+ }
76
+
77
+ /** Register a stored secret value, plus its encoded forms, with the scrubber. */
78
+ export function registerStoredSecret(value: string | null | undefined, name: string): void {
79
+ if (!value) return;
80
+ registerVolatileSecret(value, name);
81
+ for (const form of encodedForms(value)) registerVolatileSecret(form, name);
82
+ }
83
+
84
+ export interface SecretRegistryLoadResult {
85
+ config: number;
86
+ oauth: number;
87
+ scriptApi: number;
88
+ /** Rows that could not be read or decrypted (wrong or missing key). */
89
+ failed: number;
90
+ }
91
+
92
+ /**
93
+ * Register every stored secret. Called once at API boot, before the boot
94
+ * retro-sweep. A row that fails to decrypt is counted and skipped; a source
95
+ * that fails as a whole (e.g. no encryption key resolved) is counted as one
96
+ * failure and the other sources still load.
97
+ */
98
+ export async function loadSecretRegistry(): Promise<SecretRegistryLoadResult> {
99
+ const result: SecretRegistryLoadResult = { config: 0, oauth: 0, scriptApi: 0, failed: 0 };
100
+
101
+ try {
102
+ const { secrets, failed } = await listStoredConfigSecrets();
103
+ for (const { key, value } of secrets) registerStoredSecret(value, configSecretName(key));
104
+ result.config = secrets.length;
105
+ result.failed += failed;
106
+ } catch {
107
+ result.failed += 1;
108
+ }
109
+
110
+ try {
111
+ const { secrets, failed } = await listStoredOAuthSecrets();
112
+ for (const { provider, kind, value } of secrets) {
113
+ registerStoredSecret(value, oauthSecretName(provider, kind));
114
+ }
115
+ result.oauth = secrets.length;
116
+ result.failed += failed;
117
+ } catch {
118
+ result.failed += 1;
119
+ }
120
+
121
+ try {
122
+ const { secrets, failed } = await listStoredScriptApiSecrets();
123
+ for (const { id, token } of secrets) registerStoredSecret(token, scriptApiSecretName(id));
124
+ result.scriptApi = secrets.length;
125
+ result.failed += failed;
126
+ } catch {
127
+ result.failed += 1;
128
+ }
129
+
130
+ return result;
131
+ }