@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
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
-
|
|
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
|
-
:
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
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 {
|
|
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(
|
|
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
|
},
|
package/src/be/memory/types.ts
CHANGED
|
@@ -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",
|
package/src/be/scripts/db.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|