@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
@@ -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
 
@@ -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,23 @@
1
+ -- Single-use connect codes for the agent-swarm.dev ChatGPT connector.
2
+ --
3
+ -- `POST /api/users/:id/connector-codes` stores a code here. The connector
4
+ -- trades it once at `POST /api/connector/exchange` for a freshly minted
5
+ -- `aswt_` token (src/be/connector-codes.ts). The plaintext code is never
6
+ -- stored. The heartbeat cleanup sweep deletes rows 1h after they expire.
7
+
8
+ CREATE TABLE IF NOT EXISTS connector_codes (
9
+ -- sha256 hex of the plaintext code.
10
+ code_hash TEXT PRIMARY KEY,
11
+ user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
12
+ label TEXT NOT NULL,
13
+ -- IdentityActor that created the code. The token minted at exchange time
14
+ -- is attributed to this actor in user_identity_events.
15
+ actor_kind TEXT NOT NULL,
16
+ actor_id TEXT NOT NULL,
17
+ created_at TEXT NOT NULL,
18
+ expires_at TEXT NOT NULL,
19
+ used_at TEXT
20
+ );
21
+
22
+ CREATE INDEX IF NOT EXISTS idx_connector_codes_expires_at
23
+ ON connector_codes(expires_at);
@@ -0,0 +1,33 @@
1
+ -- Page guest sessions are an RBAC principal, so their decisions are audited.
2
+ -- SQLite cannot change a CHECK in place, so rebuild the table with the 108
3
+ -- shape plus the new principal type. Every row is copied unchanged.
4
+
5
+ CREATE TABLE permission_audit_new (
6
+ id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
7
+ ts TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
8
+ principalType TEXT NOT NULL CHECK (principalType IN ('agent','user','operator','guest')),
9
+ principalId TEXT,
10
+ originatorUserId TEXT,
11
+ verb TEXT NOT NULL,
12
+ resourceType TEXT,
13
+ resourceId TEXT,
14
+ decision TEXT NOT NULL CHECK (decision IN ('allow','deny')),
15
+ reason TEXT,
16
+ source TEXT NOT NULL CHECK (source IN ('mcp','http'))
17
+ );
18
+
19
+ INSERT INTO permission_audit_new (
20
+ id, ts, principalType, principalId, originatorUserId,
21
+ verb, resourceType, resourceId, decision, reason, source
22
+ )
23
+ SELECT
24
+ id, ts, principalType, principalId, originatorUserId,
25
+ verb, resourceType, resourceId, decision, reason, source
26
+ FROM permission_audit;
27
+
28
+ DROP TABLE permission_audit;
29
+ ALTER TABLE permission_audit_new RENAME TO permission_audit;
30
+
31
+ CREATE INDEX idx_permission_audit_ts ON permission_audit(ts);
32
+ CREATE INDEX idx_permission_audit_decision_ts ON permission_audit(decision, ts);
33
+ CREATE INDEX idx_permission_audit_principal_ts ON permission_audit(principalId, ts);
@@ -0,0 +1,20 @@
1
+ -- Backfill a TTL on legacy auto-generated memories.
2
+ --
3
+ -- task_completion and session_summary rows written before TTL_DEFAULTS existed
4
+ -- (or by paths that skipped computeExpiresAt) carry expiresAt = NULL, so
5
+ -- purgeExpired() never removes them and they keep competing for pre-task
6
+ -- recall slots indefinitely. Give each one a 7-day grace window from the
7
+ -- moment this migration runs; the regular purge then removes them.
8
+ --
9
+ -- Excluded:
10
+ -- * swarm-scope rows: promoted to shared knowledge, not per-agent residue.
11
+ -- * /longterm keys: tierSource() treats them as manual (no TTL).
12
+ --
13
+ -- Data-only. ISO-8601 with milliseconds, matching Date#toISOString() used by
14
+ -- computeExpiresAt() for new rows.
15
+ UPDATE agent_memory
16
+ SET expiresAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now', '+7 days')
17
+ WHERE source IN ('task_completion', 'session_summary')
18
+ AND expiresAt IS NULL
19
+ AND scope <> 'swarm'
20
+ AND (key IS NULL OR (key <> '/longterm' AND key NOT LIKE '/longterm/%'));
@@ -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';
@@ -30,6 +30,7 @@ import {
30
30
  harnessCatalogSection,
31
31
  harnessModelMismatch,
32
32
  isAlias,
33
+ isHarnessModelJudged,
33
34
  type ModelsDevCatalog,
34
35
  type ModelsDevModel,
35
36
  parseAlias,
@@ -340,7 +341,7 @@ export async function resolveTaskModel(
340
341
  input: ResolveTaskModelInput,
341
342
  ): Promise<TaskModelResolution | TaskModelHarnessMismatch | null> {
342
343
  const harness = input.harnessProvider ?? null;
343
- const judged = harness !== null && harnessCatalogSection(harness) !== null;
344
+ const judged = harness !== null && isHarnessModelJudged(harness);
344
345
  const sections = judged
345
346
  ? ((await loadResolutionCatalog()).catalog as HarnessCatalogSections)
346
347
  : {};
@@ -81,7 +81,7 @@ export interface ExplicitModelCheck {
81
81
  agentName?: string | null;
82
82
  }
83
83
 
84
- /** Null when `model` runs on `harness`, else the reason. Pure; only judges claude, claude-managed, codex. */
84
+ /** Null when `model` runs on `harness`, else the reason. Pure; only judges claude, claude-managed, codex and grok. */
85
85
  export const harnessModelError = harnessModelMismatch;
86
86
 
87
87
  /** `harnessModelError` against the current catalog, for callers with no agent row (claim path). */
@@ -66,6 +66,9 @@ const ROUTING_PREFIXES_BY_PROVIDER: Record<PricingProvider, readonly string[]> =
66
66
  amp: ["anthropic/", "openai/", "google-vertex/", "google/"],
67
67
  // cursor reports a bare Cursor model id, already the vendor's own id.
68
68
  cursor: [],
69
+ // The Grok CLI reports a bare xAI id (`grok-4.6`) or `openrouter/<vendor>/<id>`
70
+ // for an OpenRouter model; stripping the prefix lands it on the openrouter rows.
71
+ grok: ["openrouter/", "xai/"],
69
72
  };
70
73
 
71
74
  /** `gpt-5-nano-2025-08-07` -> `gpt-5-nano`. Anthropic's 8-digit dates (`-20251001`) are real catalog ids and are left alone. */
@@ -21,7 +21,7 @@ import { isEnvFlagEnabled } from "../utils/env-flag";
21
21
  import { getDbClient } from "./db";
22
22
 
23
23
  type AuditRow = {
24
- principalType: "agent" | "user" | "operator";
24
+ principalType: "agent" | "user" | "operator" | "guest";
25
25
  principalId: string | null;
26
26
  originatorUserId: string | null;
27
27
  verb: string;
@@ -61,6 +61,7 @@ function principalIdOf(principal: RbacCheck["principal"]): string | null {
61
61
  case "user":
62
62
  return principal.userId;
63
63
  case "operator":
64
+ case "guest":
64
65
  return null;
65
66
  }
66
67
  }
@@ -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
+ }
@@ -106,6 +106,26 @@ export interface KvEntry<T = unknown> {
106
106
  expiresAt: number | null;
107
107
  createdAt: number;
108
108
  updatedAt: number;
109
+ /** Present only on a path/offset/limit read. */
110
+ view?: KvView;
111
+ }
112
+
113
+ export interface KvViewArgs {
114
+ /** Dot path into the JSON value, e.g. "outcome.data.rows" or "rows.3". */
115
+ path?: string;
116
+ /** First array item / object key / string char. Default 0. */
117
+ offset?: number;
118
+ /** Max items / keys / chars. Unbounded in scripts. */
119
+ limit?: number;
120
+ }
121
+
122
+ export interface KvView {
123
+ path: string;
124
+ type: "object" | "array" | "string" | "number" | "boolean" | "null";
125
+ total?: number;
126
+ offset?: number;
127
+ returned?: number;
128
+ nextOffset?: number | null;
109
129
  }
110
130
 
111
131
  export interface KvSdkSuccess<T, TStatus extends number = 200> {
@@ -175,8 +195,9 @@ export interface SwarmSdk {
175
195
  task_defer(args: { taskId: string; delayMs?: number; runAt?: string; wakeOn?: { event: "task.completed" | "task.failed" | "settled"; taskId: string }; summary: string; note: string; checks?: string[] }): Promise<unknown>;
176
196
  task_poll(args?: Record<string, unknown>): Promise<unknown>;
177
197
  // --- kv ---
178
- kv_get<T = unknown>(args: { key: string; namespace?: string }): Promise<KvSdkResponse<KvEntry<T>>>;
179
- kv_getOrNull<T = unknown>(args: { key: string; namespace?: string }): Promise<KvEntry<T> | null>;
198
+ /** path/offset/limit return a view: \`value\` is the slice at a dot path (JSON strings count as JSON), paged over items/keys/chars; \`view\` describes it. */
199
+ kv_get<T = unknown>(args: { key: string; namespace?: string } & KvViewArgs): Promise<KvSdkResponse<KvEntry<T>>>;
200
+ kv_getOrNull<T = unknown>(args: { key: string; namespace?: string } & KvViewArgs): Promise<KvEntry<T> | null>;
180
201
  kv_set<T>(args: KvSetArgsBase & { value: T; valueType?: "json" }): Promise<KvSdkResponse<KvEntry<T>>>;
181
202
  kv_set(args: KvSetArgsBase & { value: string; valueType: "string" }): Promise<KvSdkResponse<KvEntry<string>>>;
182
203
  kv_set(args: KvSetArgsBase & { value: number | string; valueType: "integer" }): Promise<KvSdkResponse<KvEntry<number>>>;
@@ -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
+ }