@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.
- package/dist/acp-adapter-zpvy6321.js +34 -0
- package/dist/{actions-jna0b3wm.js → actions-fq2zrwtf.js} +30 -34
- package/dist/{amp-adapter-0eve81pz.js → amp-adapter-5gbk7501.js} +7 -7
- package/dist/{anthropic-messages-hst62ws9.js → anthropic-messages-13yn502s.js} +4 -4
- package/dist/{app-75hm5cfa.js → app-nctetn1e.js} +11 -11
- package/dist/{assistant-yzd51j33.js → assistant-0aq8gjvj.js} +31 -35
- package/dist/{azure-openai-responses-mh8tvf78.js → azure-openai-responses-9st34pfz.js} +5 -5
- package/dist/{boot-reembed-z158xq32.js → boot-reembed-1ze5853n.js} +14 -16
- package/dist/{boot-reembed-2ex2mcsh.js → boot-reembed-fmp5ww69.js} +15 -16
- package/dist/boot-scrub-sweep-cbp8xd4e.js +212 -0
- package/dist/{boot-seeds-2bmnh18p.js → boot-seeds-22tx8a53.js} +13 -12
- package/dist/{claude-adapter-jbj1mycz.js → claude-adapter-0jhgeh1d.js} +25 -25
- package/dist/{claude-managed-adapter-17ggz09t.js → claude-managed-adapter-tzwxy803.js} +5 -5
- package/dist/{claude-managed-setup-ptm1nzm2.js → claude-managed-setup-nrhfx5b0.js} +5 -5
- package/dist/{claude-sdk-session-9re1g7r2.js → claude-sdk-session-wh77rh83.js} +2632 -2614
- package/dist/{cli-zsh093hk.js → cli-014vns2y.js} +63 -1
- package/dist/{cli-qvfk83fy.js → cli-13xwcqvj.js} +2 -2
- package/dist/{cli-8gpbwy09.js → cli-1yrcfa5s.js} +5 -5
- package/dist/{cli-xtgtyafv.js → cli-577bxkf6.js} +212 -451
- package/dist/{cli-ma3zvh39.js → cli-5qkcdv8c.js} +6118 -5874
- package/dist/{cli-cwxhbf43.js → cli-5t5my0h8.js} +85 -2
- package/dist/{cli-zvckv750.js → cli-69egj654.js} +2 -2
- package/dist/{cli-9xb89xca.js → cli-6bpvv8h4.js} +9 -9
- package/dist/{cli-90v942q1.js → cli-6qxmjpem.js} +8 -8
- package/dist/{cli-9866bd07.js → cli-6vfz44sr.js} +1 -1
- package/dist/{cli-32bca7nr.js → cli-7avy4v8w.js} +1 -1
- package/dist/{cli-xb3c6aat.js → cli-7mhy3ths.js} +21 -18
- package/dist/{cli-q38n4bq0.js → cli-95zs84qw.js} +3 -3
- package/dist/cli-a35hzdve.js +33 -0
- package/dist/{cli-x3x0qwky.js → cli-bxqez1p0.js} +51 -23
- package/dist/{cli-prt9emct.js → cli-c2tpw9ck.js} +3 -3
- package/dist/{cli-0n4fasz0.js → cli-c8r99akr.js} +16 -16
- package/dist/{cli-5anb0ev3.js → cli-cexctgn5.js} +89 -26
- package/dist/{cli-bns5c8k9.js → cli-cf2vgn9d.js} +105 -91
- package/dist/{cli-ng97fjq7.js → cli-cmxkbzzs.js} +33 -7
- package/dist/{cli-nexwh8zg.js → cli-g7k7zbcz.js} +12 -5
- package/dist/{cli-mmzwwptd.js → cli-hf5s9pgf.js} +1 -1
- package/dist/{cli-nvvfd8bq.js → cli-jbzbr36n.js} +4091 -3793
- package/dist/{cli-zn2v3t8v.js → cli-jvmtxqtc.js} +27 -15
- package/dist/{cli-pkke6geq.js → cli-km4x66j0.js} +83 -65
- package/dist/cli-kw0jcfje.js +516 -0
- package/dist/{cli-28smfvbn.js → cli-m7vmeepg.js} +32 -19
- package/dist/{cli-hpryr6em.js → cli-mb75wpxf.js} +1 -1
- package/dist/{cli-qdv8kgz7.js → cli-mkdsy1nq.js} +14018 -3333
- package/dist/{cli-b0f70z24.js → cli-mn8qkd8v.js} +1 -1
- package/dist/{cli-420bdgw0.js → cli-mv2520nm.js} +1 -1
- package/dist/{cli-n9ehc28t.js → cli-mxry91sa.js} +31 -10
- package/dist/{cli-wfajtmf7.js → cli-mxvnnv5q.js} +596 -13
- package/dist/cli-nkqjj42q.js +104 -0
- package/dist/{cli-e7t4snpa.js → cli-p8rt8spp.js} +5 -5
- package/dist/{cli-gh5yqzpe.js → cli-pkj1w46r.js} +12 -7
- package/dist/cli-q1vnz8wv.js +96 -0
- package/dist/{cli-fj86xr3c.js → cli-qd97v6z2.js} +2 -2
- package/dist/{cli-ba66wyhy.js → cli-qzmzwegq.js} +60 -20
- package/dist/{cli-5b2fjjzh.js → cli-r7eeddkq.js} +14 -9
- package/dist/{cli-8ssjez7e.js → cli-rhy7z1rp.js} +22 -11
- package/dist/{cli-4mevd1hf.js → cli-s9y81s7j.js} +5 -5
- package/dist/{cli-59jn5twa.js → cli-tc5zec7v.js} +7 -7
- package/dist/{cli-ttbsse3f.js → cli-tmhsxhb8.js} +1 -1
- package/dist/{cli-k6xx3pve.js → cli-tx2zvn23.js} +4 -4
- package/dist/{cli-pjxnr1jn.js → cli-ty4bzkqr.js} +5 -5
- package/dist/{cli-nfygw434.js → cli-v1pwxreq.js} +55 -52
- package/dist/{cli-ftry7975.js → cli-v1w7pbd8.js} +3 -2
- package/dist/{cli-10zmp1p7.js → cli-vr5avem2.js} +27 -32
- package/dist/{cli-bzr0ftht.js → cli-wnb9b2g3.js} +19 -0
- package/dist/cli-wt7ccy4r.js +2654 -0
- package/dist/{cli-50eg13rj.js → cli-wydz3feg.js} +549 -321
- package/dist/{cli-krqm5f11.js → cli-xezn9nkh.js} +16 -13
- package/dist/{cli-8kt0bmgx.js → cli-xf713zz0.js} +2 -2
- package/dist/{cli-x7afk5wm.js → cli-xvc3q9br.js} +1 -1
- package/dist/{cli-9s3h836g.js → cli-xvdgasgy.js} +1 -1
- package/dist/{cli-j0pdrmwg.js → cli-y0kjpfm9.js} +3 -3
- package/dist/{cli-6zcfw0x7.js → cli-y195hv1a.js} +1 -1
- package/dist/{cli-amwhs8h2.js → cli-ykt8f7va.js} +22 -9
- package/dist/{cli-1kzfvd1z.js → cli-ymmc1zxg.js} +15 -11
- package/dist/{cli-xnzrzets.js → cli-ypgba1a8.js} +2 -2
- package/dist/{cli-bdv39kgj.js → cli-z1dqhc9y.js} +31 -20
- package/dist/{cli-fw6vdcq5.js → cli-zp67qg5x.js} +34 -1
- package/dist/{cli-qbksc292.js → cli-zy39d433.js} +2 -0
- package/dist/cli.js +36 -32
- package/dist/{cloudflare-workers-ai-system-one-tt484t61.js → cloudflare-workers-ai-system-one-sta45jf9.js} +7 -5
- package/dist/{codex-adapter-3qnxshje.js → codex-adapter-cgesnjkr.js} +20 -20
- package/dist/{codex-login-brkxhhdg.js → codex-login-g1cyt5zm.js} +2 -2
- package/dist/{codex-session-runner-612wnapp.js → codex-session-runner-8kd7ddhr.js} +20 -20
- package/dist/{commands-2ybfcxkj.js → commands-h6zaaxym.js} +11 -10
- package/dist/{cursor-adapter-p3jgdehv.js → cursor-adapter-v8mm6bp5.js} +7 -7
- package/dist/{db-f4ans0jj.js → db-qg3myn53.js} +13 -10
- package/dist/{devin-adapter-dhzt01ff.js → devin-adapter-bffjj5n4.js} +2 -2
- package/dist/{dispatcher-pk182apd.js → dispatcher-7vnwj9qw.js} +28 -32
- package/dist/{dsh-adapter-7twaeps1.js → dsh-adapter-tz6hfp5r.js} +7 -7
- package/dist/{e2b-y8t3jzyz.js → e2b-nd50eccg.js} +1 -1
- package/dist/{easter-egg-3d-mtpnw06s.js → easter-egg-3d-66zxervv.js} +2 -2
- package/dist/{event-bus-w7ac4n3c.js → event-bus-vnfthafg.js} +2 -2
- package/dist/{execute-s4ccjq3n.js → execute-h4jgevyw.js} +61 -11
- package/dist/{google-generative-ai-d6nphg3q.js → google-generative-ai-b8zxrpax.js} +5 -5
- package/dist/{google-vertex-cpshkref.js → google-vertex-nrdd697t.js} +5 -5
- package/dist/grok-adapter-jdt2dz2s.js +39 -0
- package/dist/{handlers-g2wnr4t9.js → handlers-2qtkpncr.js} +31 -35
- package/dist/{hook-f7jt9vnm.js → hook-51yfcgsv.js} +48 -39
- package/dist/{hook-bbdpnkb4.js → hook-qnbdgwvd.js} +15 -17
- package/dist/{http-w40q4w94.js → http-jsmtag08.js} +536 -231
- package/dist/{index-056j5kzn.js → index-2t9kesn1.js} +13 -12
- package/dist/{index-cj1r9q83.js → index-bt8wkv1w.js} +29 -33
- package/dist/{index-7n70jwbb.js → index-cx1sb9bq.js} +42 -35
- package/dist/{index-9bfssf54.js → index-ekywyv2v.js} +28 -32
- package/dist/{index-fdk1t4d6.js → index-n0rxa4ca.js} +11962 -9900
- package/dist/{index-k8xc083e.js → index-t4g06w93.js} +28 -32
- package/dist/{keepalive-1tj10tf4.js → keepalive-4aezx4bj.js} +16 -12
- package/dist/{lead-st0bg7v4.js → lead-r44bkpm5.js} +52 -56
- package/dist/{llama-cpp-classify-tgj35k8d.js → llama-cpp-classify-6erb7wwv.js} +3 -1
- package/dist/{maintenance-nz71zats.js → maintenance-grz5jrzh.js} +12 -13
- package/dist/{mistral-conversations-8445ddjn.js → mistral-conversations-059y51zt.js} +4 -4
- package/dist/{node-f0ykebbv.js → node-qm9eb6z0.js} +25 -7
- package/dist/{oauth-refresh-sweep-aefzwvvq.js → oauth-refresh-sweep-wm4ctjfj.js} +15 -11
- package/dist/{onboard-kgpr9mbn.js → onboard-v60zgqf0.js} +2 -2
- package/dist/{openai-codex-responses-9ynm88zy.js → openai-codex-responses-jzxkta9b.js} +8 -7
- package/dist/{openai-completions-nhqwjmaw.js → openai-completions-663yncfq.js} +4 -4
- package/dist/openai-decisions-vj9nkpf8.js +162 -0
- package/dist/{openai-responses-xx1c4kgm.js → openai-responses-m3g43rrt.js} +5 -5
- package/dist/{opencode-adapter-0c8e70dk.js → opencode-adapter-yjyqynzf.js} +8 -8
- package/dist/{openrouter-images-8yx3487g.js → openrouter-images-kr60n6b9.js} +1 -1
- package/dist/{otel-impl-x8zp4y5a.js → otel-impl-ghh4dyf8.js} +2 -2
- package/dist/{pi-messages-vxdt8v7j.js → pi-messages-xszk613n.js} +1 -1
- package/dist/{pi-mono-adapter-t280ah5c.js → pi-mono-adapter-0eg8kw2c.js} +69 -52
- package/dist/{pricing-refresh-9e0q7cwk.js → pricing-refresh-mjrqvnvw.js} +12 -11
- package/dist/{rbac-roles-790j3ga8.js → rbac-roles-axv4d74x.js} +11 -10
- package/dist/{rbac-roles-kcp6zge2.js → rbac-roles-fd5tskj9.js} +11 -10
- package/dist/{render-v2-tch4nefj.js → render-v2-k50j33qd.js} +11 -11
- package/dist/{runtime-v2t1r79e.js → runtime-h9ha1mq6.js} +4 -4
- package/dist/{setup-rcm303ns.js → setup-4syh0pd2.js} +2 -2
- package/dist/{typesafe-system-one-datfr82p.js → typesafe-system-one-e988vty7.js} +7 -5
- package/dist/{virtual-modules-wm8mxdxx.js → virtual-modules-ftr9kwe8.js} +11 -11
- package/dist/{work-objects-ctg7t70w.js → work-objects-jcdbg8rr.js} +32 -36
- package/dist/{worker-5dp0xm55.js → worker-x8j8c9sx.js} +52 -56
- package/dist/{x-v3yw66zs.js → x-ma4gnfgy.js} +2 -2
- package/openapi.json +5 -2
- package/package.json +15 -11
- package/src/be/automation-preflight.ts +9 -4
- package/src/be/boot-scrub-sweep.ts +302 -0
- package/src/be/crypto/key-bootstrap.ts +1 -1
- package/src/be/db/approvals.ts +15 -1
- package/src/be/db/codex-oauth-device-flows.ts +40 -0
- package/src/be/db/config-secrets.ts +31 -0
- package/src/be/db/runtime.ts +4 -0
- package/src/be/db/script-runs.ts +15 -0
- package/src/be/db/tasks/write.ts +1 -1
- package/src/be/db-queries/mcp-oauth.ts +5 -0
- package/src/be/db-queries/oauth.ts +77 -0
- package/src/be/db.ts +162 -59
- package/src/be/events.ts +17 -1
- package/src/be/memory/index-content.ts +2 -2
- package/src/be/memory/providers/openai-embedding.ts +7 -2
- package/src/be/memory/providers/sqlite-store.ts +63 -11
- package/src/be/memory/raters/llm-summarizer.ts +7 -2
- package/src/be/memory/types.ts +14 -0
- package/src/be/migrations/201_workflow_replay_columns.sql +10 -0
- package/src/be/migrations/202_codex_oauth_device_flows.sql +24 -0
- package/src/be/modelsdev-cache.json +61 -0
- package/src/be/scripts/db.ts +28 -4
- package/src/be/scripts/source-secrets.ts +37 -0
- package/src/be/scrub-json.ts +32 -0
- package/src/be/sealed-json.ts +59 -0
- package/src/be/secret-registry.ts +131 -0
- package/src/be/seed-pricing.ts +84 -53
- package/src/be/swarm-config-guard.ts +13 -0
- package/src/be/workflow-replay.ts +77 -0
- package/src/commands/provider-credentials.ts +28 -5
- package/src/commands/runner.ts +7 -12
- package/src/heartbeat/heartbeat.ts +91 -19
- package/src/hooks/hook.ts +72 -27
- package/src/http/codex-oauth-device.ts +8 -10
- package/src/http/db-query-bounded.ts +3 -0
- package/src/http/db-query-shared.ts +107 -15
- package/src/http/db-query.ts +10 -2
- package/src/http/index.ts +23 -6
- package/src/http/mcp.ts +5 -0
- package/src/http/onboarding.ts +2 -0
- package/src/http/script-runs.ts +9 -1
- package/src/http/scripts.ts +15 -0
- package/src/http/workflows.ts +3 -2
- package/src/oauth/mcp-wrapper.ts +1 -1
- package/src/providers/acp-adapter.ts +25 -11
- package/src/providers/acp-target-catalog.ts +35 -1
- package/src/providers/acp-targets.ts +78 -7
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/claude-adapter.ts +26 -0
- package/src/providers/codex-adapter.ts +30 -3
- package/src/providers/codex-oauth/auth-json.ts +21 -4
- package/src/providers/codex-oauth/types.ts +2 -0
- package/src/providers/cursor-adapter.ts +1 -1
- package/src/providers/dsh-adapter.ts +10 -1
- package/src/providers/grok-adapter.ts +18 -2
- package/src/providers/opencode-adapter.ts +21 -5
- package/src/providers/pi-mono-extension.ts +56 -25
- package/src/providers/pricing-sources.md +2 -0
- package/src/script-workflows/supervisor.ts +11 -2
- package/src/scripts-runtime/eval-harness.ts +2 -12
- package/src/scripts-runtime/executors/quickjs-bundle.ts +152 -0
- package/src/scripts-runtime/executors/quickjs-prelude.ts +285 -0
- package/src/scripts-runtime/executors/quickjs-runner.ts +580 -0
- package/src/scripts-runtime/executors/quickjs-worker.ts +81 -0
- package/src/scripts-runtime/executors/quickjs.ts +196 -0
- package/src/scripts-runtime/executors/registry.ts +2 -0
- package/src/scripts-runtime/executors/sourcemap.ts +79 -0
- package/src/scripts-runtime/script-error-hints.ts +14 -0
- package/src/server.ts +66 -4
- package/src/tests/acp-adapter.test.ts +277 -12
- package/src/tests/boot-scrub-sweep.test.ts +390 -0
- package/src/tests/check-text-columns.test.ts +145 -0
- package/src/tests/ci-unit-test-report.test.ts +73 -0
- package/src/tests/claude-default-route.test.ts +75 -0
- package/src/tests/claude-hook-output.test.ts +233 -0
- package/src/tests/claude-sdk-process.fixture.ts +13 -2
- package/src/tests/codex-adapter-pool-auth-revalidate.test.ts +4 -2
- package/src/tests/codex-adapter.test.ts +166 -26
- package/src/tests/codex-oauth-auth-timestamps.test.ts +76 -0
- package/src/tests/codex-oauth-device.test.ts +23 -37
- package/src/tests/codex-oauth.test.ts +4 -1
- package/src/tests/context-window.test.ts +2 -2
- package/src/tests/credential-check.test.ts +51 -1
- package/src/tests/db-query-denylist.test.ts +202 -0
- package/src/tests/db-task-read-contract.test.ts +1 -0
- package/src/tests/db-task-write-contract.test.ts +1 -0
- package/src/tests/dsh-adapter.test.ts +23 -1
- package/src/tests/enabled-tools.test.ts +269 -0
- package/src/tests/entrypoint-acp-target-binary.test.ts +31 -1
- package/src/tests/entrypoint-codex-oauth-seed.test.ts +12 -0
- package/src/tests/events-scrub.test.ts +88 -0
- package/src/tests/fixtures/secret-corpus.ts +320 -0
- package/src/tests/grok-adapter.test.ts +13 -0
- package/src/tests/haiku-5-5-pricing.test.ts +38 -0
- package/src/tests/heartbeat-checklist.test.ts +158 -1
- package/src/tests/onboarding.test.ts +44 -0
- package/src/tests/opencode-adapter.test.ts +66 -3
- package/src/tests/opencode-plugin.test.ts +37 -0
- package/src/tests/openrouter-attribution-call-sites.test.ts +204 -0
- package/src/tests/openrouter-base-url.test.ts +30 -39
- package/src/tests/pi-mono-extension.test.ts +161 -2
- package/src/tests/pricing-refresh.test.ts +70 -1
- package/src/tests/reasoning-effort.test.ts +3 -1
- package/src/tests/script-connections.test.ts +5 -0
- package/src/tests/script-executor-conformance.test.ts +3 -0
- package/src/tests/script-executor-quickjs.test.ts +442 -0
- package/src/tests/script-executor-registry.test.ts +6 -1
- package/src/tests/script-runs-http.test.ts +88 -1
- package/src/tests/script-runs-scrub.test.ts +275 -0
- package/src/tests/scripts-http.test.ts +82 -1
- package/src/tests/scrubbed-text-12e.test.ts +289 -0
- package/src/tests/sealed-key-guard.test.ts +166 -0
- package/src/tests/secret-canary-sweep.test.ts +958 -0
- package/src/tests/secret-registry.test.ts +355 -0
- package/src/tests/secret-rules.test.ts +230 -0
- package/src/tests/secret-scrubber.test.ts +3 -2
- package/src/tests/telemetry-trigger.test.ts +12 -1
- package/src/tests/workflow-llm.test.ts +14 -0
- package/src/tests/workflow-replay.test.ts +518 -0
- package/src/tests/workflow-scrub.test.ts +242 -0
- package/src/tests/workflow-system-one-decision-providers.test.ts +33 -0
- package/src/tools/db-query.ts +1 -1
- package/src/tools/inject-learning.ts +1 -1
- package/src/tools/request-human-input.ts +7 -2
- package/src/tools/workflows/get-workflow-run.ts +3 -2
- package/src/types.ts +1 -1
- package/src/utils/enabled-tools.ts +27 -0
- package/src/utils/internal-ai/complete-structured.ts +11 -3
- package/src/utils/openrouter-base-url.ts +23 -0
- package/src/utils/secret-rules.generated.ts +2055 -0
- package/src/utils/secret-scrubber.ts +363 -29
- package/src/workflows/engine.ts +39 -4
- package/src/workflows/executors/raw-llm.ts +1 -0
- package/src/workflows/executors/system-one-decision.ts +2 -0
- package/src/workflows/executors/validate.ts +1 -0
- package/src/workflows/executors/workflow-llm.ts +9 -2
- package/src/workflows/recovery.ts +10 -1
- package/src/workflows/resume.ts +62 -3
- package/src/workflows/retry-poller.ts +3 -0
- package/templates/skills/agent-browser/SKILL.md +2 -0
- package/templates/skills/agent-browser/content.md +2 -0
- package/templates/skills/code-quality/content.md +1 -0
- package/templates/skills/heartbeat-runbook/content.md +1 -1
- package/templates/skills/work-on-task/SKILL.md +4 -0
- package/templates/skills/work-on-task/content.md +4 -0
- package/templates/skills/workflow-iterate/content.md +9 -0
- package/dist/acp-adapter-w6zws6me.js +0 -23
- package/dist/boot-scrub-logs-246yn076.js +0 -99
- package/dist/cli-3a4m1aws.js +0 -572
- package/dist/cli-3dpj7atn.js +0 -11
- package/dist/cli-a87b04k8.js +0 -951
- package/dist/cli-cvk6yzw5.js +0 -40
- package/dist/cli-gvcgmx69.js +0 -183
- package/dist/cli-h1spv8v4.js +0 -427
- package/dist/cli-jqt9hrnz.js +0 -168
- package/dist/cli-yhmwmv0g.js +0 -9281
- package/dist/grok-adapter-yt9pab3k.js +0 -29
- package/src/be/boot-scrub-logs.ts +0 -132
- 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
|
|
package/src/be/db/approvals.ts
CHANGED
|
@@ -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
|
-
[
|
|
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
|
+
}
|
package/src/be/db/runtime.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/be/db/tasks/write.ts
CHANGED
|
@@ -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 = ?,
|