@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.
- package/README.md +1 -1
- package/dist/acp-adapter-yzfxn8vj.js +23 -0
- package/dist/{actions-hp2h32dx.js → actions-qz3m692w.js} +24 -28
- package/dist/{amp-adapter-zfd7djyx.js → amp-adapter-j1xbmsqz.js} +7 -7
- package/dist/{app-w2d26h4f.js → app-43vrdmv8.js} +9 -10
- package/dist/{assistant-45pc4en7.js → assistant-ertffmka.js} +25 -29
- package/dist/{boot-reembed-k1q2s4fc.js → boot-reembed-998jtpdg.js} +12 -15
- package/dist/{boot-reembed-6rhcpsjf.js → boot-reembed-s7k7c6v2.js} +13 -15
- package/dist/boot-scrub-sweep-nepsk4t1.js +211 -0
- package/dist/{boot-seeds-ex93edx7.js → boot-seeds-vvzs1zhk.js} +12 -12
- package/dist/{claude-adapter-hcxd0szm.js → claude-adapter-2zvmekrp.js} +15 -17
- package/dist/{claude-managed-adapter-fz7xps2r.js → claude-managed-adapter-s49wbk8t.js} +5 -5
- package/dist/{claude-managed-setup-xx6f15h9.js → claude-managed-setup-cvhqq8qn.js} +4 -4
- package/dist/{claude-sdk-session-6nb9raem.js → claude-sdk-session-kkbxtw5q.js} +2624 -2606
- package/dist/{cli-etbrt8x1.js → cli-0nhdwxd5.js} +4 -4
- package/dist/{cli-jeefgppq.js → cli-0qjcsxgz.js} +7 -7
- package/dist/{cli-6behs2bt.js → cli-32bca7nr.js} +1 -1
- package/dist/{cli-gt8v96qx.js → cli-3ddybbbx.js} +4 -4
- package/dist/{acp-adapter-rxvek4ym.js → cli-3qn4r334.js} +317 -35
- package/dist/{cli-1d3w4xm3.js → cli-4r1sjfc4.js} +14 -6
- package/dist/{cli-691f2rxm.js → cli-66zh26sp.js} +68 -16
- package/dist/{cli-et8wdyqh.js → cli-68r1j4sg.js} +9 -9
- package/dist/{cli-xb7kyanp.js → cli-8kgvmkak.js} +3 -3
- package/dist/{cli-fdpkw8vk.js → cli-8t229cmr.js} +8 -8
- package/dist/{cli-gc54qc0p.js → cli-aqcey741.js} +1 -1
- package/dist/{cli-4q4cmr4t.js → cli-cpfmqz3n.js} +8 -8
- package/dist/{cli-a9bw0xxt.js → cli-df56tqdw.js} +997 -408
- package/dist/{cli-n7h1kqpf.js → cli-dmtrxqhg.js} +6 -1
- package/dist/{cli-xfb7333f.js → cli-dw2q9f7g.js} +5 -5
- package/dist/{cli-4drstbb9.js → cli-ecknp7p1.js} +81 -23
- package/dist/{cli-v0pvvhfq.js → cli-efnd2kce.js} +36 -1
- package/dist/{cli-1wetf34q.js → cli-f2n17zhp.js} +1 -1
- package/dist/{cli-2c8eky5y.js → cli-feh3dkah.js} +1 -1
- package/dist/cli-fn697bnt.js +126 -0
- package/dist/{cli-v5pr2fgt.js → cli-gt8azyet.js} +3 -3
- package/dist/{cli-dbszf3rw.js → cli-h3x613ze.js} +4 -4
- package/dist/{cli-nwhb8y2e.js → cli-hf5s9pgf.js} +1 -1
- package/dist/cli-j2jbr8af.js +516 -0
- package/dist/{cli-agjzafns.js → cli-jhm6hzjn.js} +593 -10
- package/dist/{cli-j0tmc908.js → cli-jvj6a0cv.js} +484 -477
- package/dist/{cli-xxz89c2n.js → cli-mb75wpxf.js} +1 -1
- package/dist/{cli-07s39g1n.js → cli-mv2520nm.js} +1 -1
- package/dist/{cli-v90vfwyb.js → cli-mwegs4af.js} +5 -5
- package/dist/{cli-bz1gbac9.js → cli-p4ztb80h.js} +2 -2
- package/dist/{cli-1wd1n2ya.js → cli-rfphkzh5.js} +1 -1
- package/dist/{cli-976r1zqx.js → cli-rggdq8wq.js} +5 -5
- package/dist/{cli-3ajw5v5v.js → cli-rsy933yz.js} +6 -6
- package/dist/{cli-yj790e19.js → cli-tadfgd0t.js} +8 -1
- package/dist/{cli-cn53e7vc.js → cli-ttbsse3f.js} +6 -1
- package/dist/{cli-dyqsrqcf.js → cli-v2a8x45d.js} +4 -4
- package/dist/{cli-wbvnn6tk.js → cli-v6vv7dy8.js} +91 -9
- package/dist/{cli-z2ap2c0c.js → cli-vzc73wet.js} +90 -55
- package/dist/cli-wt7ccy4r.js +2654 -0
- package/dist/{cli-4cn19jvn.js → cli-x7kpr72a.js} +3 -3
- package/dist/{cli-003785v9.js → cli-xvc3q9br.js} +1 -1
- package/dist/{cli-v0e7jpr5.js → cli-xvdgasgy.js} +13 -4
- package/dist/{cli-asqeg4ve.js → cli-y59h2pk9.js} +2 -2
- package/dist/{cli-0wn3ypqv.js → cli-yshaazs1.js} +14121 -3478
- package/dist/{cli-wg8yjqtw.js → cli-ysnyhws9.js} +16 -3
- package/dist/cli-zp67qg5x.js +92 -0
- package/dist/{cli-g3qgrzck.js → cli-zsh093hk.js} +71 -3
- package/dist/{cli-ma3zvh39.js → cli-zy7vwsne.js} +6066 -5829
- package/dist/cli.js +79 -26
- package/dist/{codex-adapter-pcxdn10v.js → codex-adapter-dw1wxqt8.js} +10 -10
- package/dist/{codex-hook-b1fb7c2k.js → codex-hook-d2rkvf39.js} +1 -1
- package/dist/{codex-login-059nm145.js → codex-login-terbdsqq.js} +2 -2
- package/dist/{codex-session-runner-xzwwddp8.js → codex-session-runner-3cgb8y1g.js} +10 -10
- package/dist/{commands-98at8mkn.js → commands-mhxam6z2.js} +9 -9
- package/dist/{cursor-adapter-dkg6qnkp.js → cursor-adapter-2qda3st7.js} +6 -6
- package/dist/{db-01g8wtvw.js → db-sjks418k.js} +11 -9
- package/dist/{devin-adapter-qmttt2fz.js → devin-adapter-bffjj5n4.js} +2 -2
- package/dist/{dispatcher-nn2k1wxd.js → dispatcher-1es759rn.js} +22 -26
- package/dist/{dsh-adapter-w4d4vwdq.js → dsh-adapter-bek4cyw9.js} +6 -6
- package/dist/{e2b-9e4g2hvt.js → e2b-nd50eccg.js} +1 -1
- package/dist/{event-bus-z7v2t26e.js → event-bus-vnfthafg.js} +2 -2
- package/dist/grok-adapter-ar9cw9jg.js +29 -0
- package/dist/{handlers-b0kqsbg8.js → handlers-1gkbhktr.js} +25 -29
- package/dist/{hook-sap7v1aj.js → hook-ds2zxq9v.js} +16 -18
- package/dist/{hook-pz50gqep.js → hook-vxa3tcsy.js} +10 -12
- package/dist/{http-ncr8rmd0.js → http-3h21z9md.js} +679 -227
- package/dist/{index-c4aqcyz1.js → index-9b65j1m5.js} +11 -11
- package/dist/{index-r46s01nq.js → index-bn6cy8ps.js} +22 -26
- package/dist/{index-38kqdt3e.js → index-dkfxpkmm.js} +23 -27
- package/dist/{index-630kj5vd.js → index-f8xnqrye.js} +55 -43
- package/dist/{index-fdk1t4d6.js → index-n0rxa4ca.js} +11962 -9900
- package/dist/{index-9nrj7797.js → index-n99fycq7.js} +22 -26
- package/dist/{keepalive-31613r2h.js → keepalive-3xspa9jw.js} +14 -11
- package/dist/{lead-mx59tr5s.js → lead-6qphc124.js} +43 -43
- package/dist/{maintenance-55ff39ca.js → maintenance-0teem1bb.js} +10 -12
- package/dist/{node-f0ykebbv.js → node-9sg7ajby.js} +25 -7
- package/dist/{oauth-refresh-sweep-anma954w.js → oauth-refresh-sweep-sh9q25xp.js} +13 -10
- package/dist/{onboard-b2fy8ftb.js → onboard-54qmnqxe.js} +4 -3
- package/dist/{opencode-adapter-p1s92tmy.js → opencode-adapter-wjfa2wvd.js} +6 -6
- package/dist/{otel-impl-9brvt7xv.js → otel-impl-ycqzp2a1.js} +2 -2
- package/dist/{pi-mono-adapter-kz4rrwvd.js → pi-mono-adapter-e77e9zst.js} +6 -6
- package/dist/{pricing-refresh-7w8mbmva.js → pricing-refresh-atnkngqs.js} +10 -10
- package/dist/{rbac-roles-ejp7qwvb.js → rbac-roles-9pjphh9q.js} +10 -10
- package/dist/{rbac-roles-nhk4r7bc.js → rbac-roles-j78wdksm.js} +10 -10
- package/dist/{render-v2-2gzt8skq.js → render-v2-pxjs49fx.js} +9 -10
- package/dist/{setup-5et8t412.js → setup-ymej0xtx.js} +2 -2
- package/dist/{work-objects-tb0er7qa.js → work-objects-3nh4ymzb.js} +26 -30
- package/dist/{worker-62cv075d.js → worker-qa9fkz5s.js} +43 -43
- package/dist/{x-vbxb31jr.js → x-ma4gnfgy.js} +2 -2
- package/openapi.json +457 -31
- package/package.json +14 -6
- package/plugin/commands/create-pr.md +7 -1
- package/plugin/pi-skills/create-pr/SKILL.md +7 -1
- package/src/agentmail/app.ts +5 -4
- package/src/be/automation-preflight.ts +11 -9
- package/src/be/boot-scrub-sweep.ts +302 -0
- package/src/be/connector-codes.ts +81 -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/sqlite-store.ts +63 -11
- package/src/be/memory/types.ts +14 -0
- package/src/be/migrations/197_connector_codes.sql +23 -0
- package/src/be/migrations/198_permission_audit_guest.sql +33 -0
- package/src/be/migrations/200_backfill_auto_memory_expiry.sql +20 -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/model-tier-resolution.ts +2 -1
- package/src/be/model-validation.ts +1 -1
- package/src/be/pricing-normalize.ts +3 -0
- package/src/be/rbac-audit.ts +2 -1
- package/src/be/scripts/db.ts +28 -4
- package/src/be/scripts/source-secrets.ts +37 -0
- package/src/be/scripts/typecheck.ts +23 -2
- 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 +14 -0
- package/src/be/users.ts +7 -1
- package/src/be/workflow-replay.ts +77 -0
- package/src/cli.tsx +7 -0
- package/src/commands/provider-credentials.ts +46 -8
- package/src/commands/runner.ts +2 -10
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/github/app.ts +4 -4
- package/src/heartbeat/heartbeat.ts +20 -8
- package/src/hooks/pr-body-guard.ts +1 -1
- package/src/http/agents.ts +1 -0
- package/src/http/approval-requests.ts +1 -0
- package/src/http/apps.ts +3 -0
- package/src/http/auth.ts +4 -1
- package/src/http/codex-oauth-device.ts +8 -10
- package/src/http/codex-oauth-keep-warm.ts +2 -2
- package/src/http/config-values.ts +59 -0
- package/src/http/config.ts +5 -0
- 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/extensions.ts +1 -0
- package/src/http/index.ts +23 -6
- package/src/http/integrations.ts +1 -38
- package/src/http/ip-rate-limit.ts +82 -0
- package/src/http/kv.ts +38 -4
- package/src/http/mcp-servers.ts +1 -0
- package/src/http/memory.ts +9 -0
- package/src/http/models-catalog.ts +2 -0
- package/src/http/operator-actor.ts +4 -0
- package/src/http/page-proxy.ts +22 -0
- package/src/http/pages.ts +4 -2
- package/src/http/realtime.ts +1 -1
- package/src/http/request-principal.ts +14 -1
- package/src/http/script-connections.ts +8 -0
- package/src/http/script-runs.ts +12 -4
- package/src/http/scripts.ts +15 -0
- package/src/http/session-cost-recompute.ts +3 -2
- package/src/http/tasks.ts +9 -0
- package/src/http/users.ts +226 -0
- package/src/http/workflows.ts +3 -2
- package/src/http.ts +1 -0
- package/src/integrations/kapso/client.ts +3 -1
- package/src/jira/app.ts +2 -2
- package/src/kv-view.ts +324 -0
- package/src/linear/app.ts +2 -2
- package/src/oauth/mcp-wrapper.ts +1 -1
- package/src/prompts/memories.ts +42 -1
- package/src/providers/acp-adapter.ts +85 -24
- package/src/providers/acp-target-catalog.ts +54 -1
- package/src/providers/acp-targets.ts +358 -4
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/cursor-adapter.ts +1 -1
- package/src/providers/grok-adapter.ts +186 -0
- package/src/providers/index.ts +5 -1
- package/src/providers/pricing-sources.md +6 -0
- package/src/providers/reasoning-effort.ts +17 -1
- package/src/providers/types.ts +2 -1
- package/src/rbac/can.ts +4 -3
- package/src/rbac/legacy-policy.ts +3 -3
- package/src/rbac/types.ts +6 -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/scripts-runtime/swarm-sdk.ts +9 -2
- package/src/scripts-runtime/types/stdlib.d.ts +27 -8
- package/src/scripts-runtime/types/swarm-extension.d.ts +3 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +27 -8
- package/src/server-user.ts +2 -20
- package/src/server.ts +3 -1
- package/src/slack/app.ts +4 -0
- package/src/slack/config.ts +3 -1
- package/src/slack/egress-scrub.ts +87 -0
- package/src/tests/acp-adapter.test.ts +191 -2
- package/src/tests/boot-scrub-sweep.test.ts +390 -0
- package/src/tests/budget-user-scope.test.ts +7 -21
- package/src/tests/check-pr-body.test.ts +149 -6
- package/src/tests/check-text-columns.test.ts +145 -0
- package/src/tests/claude-adapter-binary.test.ts +1 -1
- package/src/tests/claude-sdk-process.fixture.ts +13 -2
- package/src/tests/codex-oauth-device.test.ts +23 -37
- package/src/tests/codex-oauth-keep-warm.test.ts +29 -0
- package/src/tests/console-scrub.test.ts +141 -0
- package/src/tests/credential-check.test.ts +90 -0
- 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/docs-index-footprint.test.js +3 -3
- package/src/tests/entrypoint-acp-target-binary.test.ts +44 -1
- package/src/tests/events-scrub.test.ts +88 -0
- package/src/tests/explicit-model-validation.test.ts +81 -0
- package/src/tests/extensions-pre-task.test.ts +33 -1
- package/src/tests/fixtures/grok/acp-updates-recorded.jsonl +53 -0
- package/src/tests/fixtures/grok/prompt-response-meta.json +36 -0
- package/src/tests/fixtures/secret-corpus.ts +320 -0
- package/src/tests/grok-adapter.test.ts +647 -0
- package/src/tests/heartbeat-checklist.test.ts +44 -0
- package/src/tests/integration-disable-flags.test.ts +156 -0
- package/src/tests/kv-http.test.ts +34 -0
- package/src/tests/kv-tool.test.ts +274 -1
- package/src/tests/kv-view.test.ts +147 -0
- package/src/tests/mcp-user-route.test.ts +257 -15
- package/src/tests/memory-injection-outcome.test.ts +143 -0
- package/src/tests/model-tier-resolution.test.ts +76 -3
- package/src/tests/page-guest-session.test.ts +424 -0
- package/src/tests/page-proxy.test.ts +1 -1
- package/src/tests/page-viewer-auth.test.ts +22 -1
- package/src/tests/pr-body-leaks.test.ts +157 -2
- package/src/tests/preload.ts +10 -0
- package/src/tests/provider-registration.test.ts +3 -1
- package/src/tests/rbac-audit.test.ts +32 -0
- package/src/tests/rbac-mcp-admission-e2e.test.ts +4 -62
- package/src/tests/script-connections.test.ts +9 -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 +126 -1
- package/src/tests/script-runs-scrub.test.ts +275 -0
- package/src/tests/scripts-http.test.ts +82 -1
- package/src/tests/scripts-mcp-e2e.test.ts +42 -6
- package/src/tests/scripts-runtime.test.ts +37 -0
- 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 +317 -1
- package/src/tests/session-costs-golden.test.ts +17 -0
- package/src/tests/slack-egress-scrub.test.ts +216 -0
- package/src/tests/swarm-tool-result-gate.test.ts +143 -30
- package/src/tests/task-tools-ctx.test.ts +3 -0
- package/src/tests/user-token-routes.test.ts +342 -3
- 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 +737 -0
- package/src/tools/db-query.ts +1 -1
- package/src/tools/inject-learning.ts +1 -1
- package/src/tools/kv/kv-get.ts +136 -6
- package/src/tools/send-task.ts +61 -17
- package/src/tools/utils.ts +186 -43
- package/src/tools/workflows/get-workflow-run.ts +3 -2
- package/src/types.ts +16 -1
- package/src/utils/console-scrub-install.ts +5 -0
- package/src/utils/console-scrub.ts +57 -0
- package/src/utils/credentials.ts +8 -0
- package/src/utils/page-session.ts +6 -2
- package/src/utils/pr-body-leaks.ts +149 -10
- package/src/utils/request-auth-context.ts +2 -0
- package/src/utils/secret-rules.generated.ts +2055 -0
- package/src/utils/secret-scrubber.ts +662 -36
- package/src/workflows/engine.ts +39 -4
- package/src/workflows/executors/notify.ts +6 -1
- package/src/workflows/executors/system-one-decision.ts +31 -12
- package/src/workflows/executors/system-one-openai-wire.ts +168 -0
- package/src/workflows/executors/system-one-providers.ts +112 -11
- package/src/workflows/executors/system-one-wire.ts +79 -0
- package/src/workflows/recovery.ts +10 -1
- package/src/workflows/resume.ts +62 -3
- package/src/workflows/retry-poller.ts +3 -0
- package/templates/ai-toolbox.manifest.json +2 -2
- package/templates/skills/agent-browser/SKILL.md +5 -5
- package/templates/skills/agent-browser/config.json +1 -1
- package/templates/skills/agent-browser/content.md +4 -4
- package/templates/skills/artifacts/content.md +17 -7
- package/templates/skills/jira-interaction/config.json +1 -1
- package/templates/skills/jira-interaction/content.md +56 -205
- package/templates/skills/linear-interaction/config.json +1 -1
- package/templates/skills/linear-interaction/content.md +31 -50
- package/templates/skills/pages/content.md +8 -4
- package/templates/skills/qa/content.md +2 -2
- package/templates/skills/workflow-iterate/content.md +4 -4
- package/tsconfig.json +3 -3
- package/dist/boot-scrub-logs-t8xg7wd7.js +0 -99
- package/dist/cli-0a7r5331.js +0 -168
- package/dist/cli-93rxw9yd.js +0 -951
- package/dist/cli-cvk6yzw5.js +0 -40
- package/dist/cli-dgnj0zcc.js +0 -40
- package/dist/cli-qt5wzq6v.js +0 -256
- package/dist/cli-v7fqehqd.js +0 -9281
- package/dist/cli-vbr859rz.js +0 -572
- package/src/be/boot-scrub-logs.ts +0 -132
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterEach, describe, expect, spyOn, test } from "bun:test";
|
|
2
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
2
|
+
import { chmodSync, existsSync, mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
|
3
3
|
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
@@ -22,7 +22,12 @@ import {
|
|
|
22
22
|
sanitizeAcpConfigOptions,
|
|
23
23
|
toAcpMcpServers,
|
|
24
24
|
} from "../providers/acp-adapter";
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
AcpTargetResolutionError,
|
|
27
|
+
COPILOT_INSTRUCTIONS_FILE,
|
|
28
|
+
copilotInstructionsDir,
|
|
29
|
+
resolveAcpTarget,
|
|
30
|
+
} from "../providers/acp-targets";
|
|
26
31
|
import type { ProviderEvent, ProviderSessionConfig } from "../providers/types";
|
|
27
32
|
import { listenOnFreePort } from "./test-net";
|
|
28
33
|
|
|
@@ -536,6 +541,190 @@ new AgentSideConnection((connection) => new FakeAgent(connection), stream);
|
|
|
536
541
|
});
|
|
537
542
|
});
|
|
538
543
|
|
|
544
|
+
test("Gemini preset supplies command, credentials, trust, and a model environment fallback", () => {
|
|
545
|
+
const config = baseConfig({
|
|
546
|
+
model: "gemini-3-flash-preview",
|
|
547
|
+
env: {
|
|
548
|
+
PATH: "/bin",
|
|
549
|
+
HOME: "/home/test",
|
|
550
|
+
ACP_TARGET: "gemini",
|
|
551
|
+
GEMINI_API_KEY: "example-test-key",
|
|
552
|
+
GOOGLE_GENAI_USE_VERTEXAI: "true",
|
|
553
|
+
GOOGLE_CLOUD_PROJECT: "example-project",
|
|
554
|
+
OPENAI_API_KEY: "not-for-gemini",
|
|
555
|
+
},
|
|
556
|
+
});
|
|
557
|
+
const target = resolveAcpTarget(config);
|
|
558
|
+
|
|
559
|
+
expect(target.target).toBe("gemini");
|
|
560
|
+
expect(target.command(config)).toEqual(["gemini", "--acp"]);
|
|
561
|
+
const env = target.env(config);
|
|
562
|
+
expect(env).toMatchObject({
|
|
563
|
+
GEMINI_API_KEY: "example-test-key",
|
|
564
|
+
GOOGLE_GENAI_USE_VERTEXAI: "true",
|
|
565
|
+
GOOGLE_CLOUD_PROJECT: "example-project",
|
|
566
|
+
GEMINI_CLI_TRUST_WORKSPACE: "true",
|
|
567
|
+
GEMINI_MODEL: "gemini-3-flash-preview",
|
|
568
|
+
});
|
|
569
|
+
expect(env.OPENAI_API_KEY).toBeUndefined();
|
|
570
|
+
// No artifact written for this config, so the built-in prompt stays.
|
|
571
|
+
expect(env.GEMINI_SYSTEM_MD).toBeUndefined();
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
test("Gemini preset writes the system prompt outside the cwd and points GEMINI_SYSTEM_MD at it", async () => {
|
|
575
|
+
const cwd = mkdtempSync(join(tmpdir(), "acp-gemini-cwd-"));
|
|
576
|
+
try {
|
|
577
|
+
const config = baseConfig({
|
|
578
|
+
cwd,
|
|
579
|
+
systemPrompt: "You are a swarm worker.",
|
|
580
|
+
env: { PATH: "/bin", ACP_TARGET: "gemini" },
|
|
581
|
+
});
|
|
582
|
+
const target = resolveAcpTarget(config);
|
|
583
|
+
await target.writeSystemPromptArtifact(config);
|
|
584
|
+
const path = target.env(config).GEMINI_SYSTEM_MD;
|
|
585
|
+
|
|
586
|
+
expect(path).toBeDefined();
|
|
587
|
+
expect(path!.startsWith(cwd)).toBe(false);
|
|
588
|
+
expect(await Bun.file(path!).text()).toBe("You are a swarm worker.");
|
|
589
|
+
// A second session gets its own file.
|
|
590
|
+
const other = baseConfig({ cwd, systemPrompt: "other", env: config.env });
|
|
591
|
+
await target.writeSystemPromptArtifact(other);
|
|
592
|
+
expect(target.env(other).GEMINI_SYSTEM_MD).not.toBe(path);
|
|
593
|
+
rmSync(join(path!, ".."), { recursive: true, force: true });
|
|
594
|
+
rmSync(join(target.env(other).GEMINI_SYSTEM_MD!, ".."), { recursive: true, force: true });
|
|
595
|
+
} finally {
|
|
596
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
test("Gemini preset keeps the built-in prompt when the swarm prompt is empty", async () => {
|
|
601
|
+
const config = baseConfig({ systemPrompt: " ", env: { PATH: "/bin", ACP_TARGET: "gemini" } });
|
|
602
|
+
const target = resolveAcpTarget(config);
|
|
603
|
+
await target.writeSystemPromptArtifact(config);
|
|
604
|
+
|
|
605
|
+
expect(target.env(config).GEMINI_SYSTEM_MD).toBeUndefined();
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
test("Copilot preset forwards its own credentials, pins the model, and never forwards GITHUB_TOKEN", () => {
|
|
609
|
+
const config = baseConfig({
|
|
610
|
+
model: "openai/gpt-5.2",
|
|
611
|
+
env: {
|
|
612
|
+
PATH: "/bin",
|
|
613
|
+
HOME: "/home/test",
|
|
614
|
+
ACP_TARGET: "copilot",
|
|
615
|
+
COPILOT_GITHUB_TOKEN: "example-copilot-token",
|
|
616
|
+
COPILOT_PROVIDER_BASE_URL: "https://llm.example/v1",
|
|
617
|
+
COPILOT_PROVIDER_API_KEY: "example-provider-key",
|
|
618
|
+
COPILOT_CUSTOM_INSTRUCTIONS_DIRS: "/opt/team-instructions",
|
|
619
|
+
GITHUB_TOKEN: "example-git-token",
|
|
620
|
+
GH_TOKEN: "example-gh-token",
|
|
621
|
+
},
|
|
622
|
+
});
|
|
623
|
+
const target = resolveAcpTarget(config);
|
|
624
|
+
|
|
625
|
+
expect(target.target).toBe("copilot");
|
|
626
|
+
expect(target.command(config)).toEqual(["copilot", "--acp"]);
|
|
627
|
+
const env = target.env(config);
|
|
628
|
+
expect(env).toMatchObject({
|
|
629
|
+
COPILOT_GITHUB_TOKEN: "example-copilot-token",
|
|
630
|
+
COPILOT_PROVIDER_BASE_URL: "https://llm.example/v1",
|
|
631
|
+
COPILOT_PROVIDER_API_KEY: "example-provider-key",
|
|
632
|
+
COPILOT_MODEL: "openai/gpt-5.2",
|
|
633
|
+
COPILOT_AUTO_UPDATE: "false",
|
|
634
|
+
COPILOT_CUSTOM_INSTRUCTIONS_DIRS: `${copilotInstructionsDir(config)},/opt/team-instructions`,
|
|
635
|
+
});
|
|
636
|
+
// The worker's git token lacks the Copilot Requests permission and would
|
|
637
|
+
// shadow a BYOK or COPILOT_GITHUB_TOKEN setup.
|
|
638
|
+
expect(env.GITHUB_TOKEN).toBeUndefined();
|
|
639
|
+
expect(env.GH_TOKEN).toBeUndefined();
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
test("Copilot preset delivers the system prompt outside the repo and removes it after the session", async () => {
|
|
643
|
+
const cwd = makeTempDir();
|
|
644
|
+
const binDir = makeTempDir();
|
|
645
|
+
const captureFile = join(binDir, "capture.json");
|
|
646
|
+
const agentPath = join(binDir, "fake-copilot-acp.ts");
|
|
647
|
+
const sdkPath = join(process.cwd(), "node_modules/@agentclientprotocol/sdk/dist/acp.js");
|
|
648
|
+
const repoAgentsMd = "# Repo rules\nKeep this file as is.\n";
|
|
649
|
+
await Bun.write(join(cwd, "AGENTS.md"), repoAgentsMd);
|
|
650
|
+
await Bun.write(
|
|
651
|
+
agentPath,
|
|
652
|
+
`
|
|
653
|
+
import { readFileSync } from "node:fs";
|
|
654
|
+
import { join } from "node:path";
|
|
655
|
+
import { Readable, Writable } from "node:stream";
|
|
656
|
+
import { AgentSideConnection, PROTOCOL_VERSION, ndJsonStream } from "${sdkPath}";
|
|
657
|
+
class FakeCopilot {
|
|
658
|
+
constructor(connection) { this.connection = connection; }
|
|
659
|
+
async initialize() {
|
|
660
|
+
return { protocolVersion: PROTOCOL_VERSION, agentCapabilities: { loadSession: false } };
|
|
661
|
+
}
|
|
662
|
+
async newSession() {
|
|
663
|
+
const dir = process.env.COPILOT_CUSTOM_INSTRUCTIONS_DIRS.split(",")[0];
|
|
664
|
+
await Bun.write(${JSON.stringify(captureFile)}, JSON.stringify({
|
|
665
|
+
argv: process.argv.slice(2),
|
|
666
|
+
dir,
|
|
667
|
+
instructions: readFileSync(join(dir, ${JSON.stringify(COPILOT_INSTRUCTIONS_FILE)}), "utf8"),
|
|
668
|
+
model: process.env.COPILOT_MODEL,
|
|
669
|
+
}));
|
|
670
|
+
return { sessionId: "copilot-session-1" };
|
|
671
|
+
}
|
|
672
|
+
async prompt(params) {
|
|
673
|
+
await this.connection.sessionUpdate({
|
|
674
|
+
sessionId: params.sessionId,
|
|
675
|
+
update: { sessionUpdate: "agent_message_chunk", content: { type: "text", text: "done" }, messageId: "m1" },
|
|
676
|
+
});
|
|
677
|
+
return { stopReason: "end_turn" };
|
|
678
|
+
}
|
|
679
|
+
async cancel() {}
|
|
680
|
+
}
|
|
681
|
+
const stream = ndJsonStream(Writable.toWeb(process.stdout), Readable.toWeb(process.stdin));
|
|
682
|
+
new AgentSideConnection((connection) => new FakeCopilot(connection), stream);
|
|
683
|
+
`,
|
|
684
|
+
);
|
|
685
|
+
const copilotStub = join(binDir, "copilot");
|
|
686
|
+
await Bun.write(copilotStub, `#!/bin/sh\nexec "${process.execPath}" "${agentPath}" "$@"\n`);
|
|
687
|
+
chmodSync(copilotStub, 0o755);
|
|
688
|
+
mkdirSync(join(binDir, "home"));
|
|
689
|
+
|
|
690
|
+
const { server, apiUrl } = await startTokenStubServer("copilot-token-id", "example-aseph_x");
|
|
691
|
+
try {
|
|
692
|
+
const config = baseConfig({
|
|
693
|
+
cwd,
|
|
694
|
+
apiUrl,
|
|
695
|
+
model: "openai/gpt-5.2",
|
|
696
|
+
systemPrompt: "You are a swarm worker.",
|
|
697
|
+
env: {
|
|
698
|
+
PATH: `${binDir}:${process.env.PATH ?? ""}`,
|
|
699
|
+
HOME: join(binDir, "home"),
|
|
700
|
+
ACP_TARGET: "copilot",
|
|
701
|
+
},
|
|
702
|
+
});
|
|
703
|
+
const session = await new ACPAdapter().createSession(config);
|
|
704
|
+
const result = await session.waitForCompletion();
|
|
705
|
+
expect(result.isError).toBe(false);
|
|
706
|
+
|
|
707
|
+
const captured = JSON.parse(await Bun.file(captureFile).text()) as {
|
|
708
|
+
argv: string[];
|
|
709
|
+
dir: string;
|
|
710
|
+
instructions: string;
|
|
711
|
+
model: string;
|
|
712
|
+
};
|
|
713
|
+
expect(captured.argv).toEqual(["--acp"]);
|
|
714
|
+
expect(captured.dir).toBe(copilotInstructionsDir(config));
|
|
715
|
+
expect(captured.dir.startsWith(cwd)).toBe(false);
|
|
716
|
+
expect(captured.instructions).toBe('---\napplyTo: "**"\n---\n\nYou are a swarm worker.\n');
|
|
717
|
+
expect(captured.model).toBe("openai/gpt-5.2");
|
|
718
|
+
expect(await Bun.file(join(cwd, "AGENTS.md")).text()).toBe(repoAgentsMd);
|
|
719
|
+
|
|
720
|
+
const deadline = Date.now() + 2_000;
|
|
721
|
+
while (existsSync(captured.dir) && Date.now() < deadline) await Bun.sleep(10);
|
|
722
|
+
expect(existsSync(captured.dir)).toBe(false);
|
|
723
|
+
} finally {
|
|
724
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
|
|
539
728
|
test("custom target passes through only explicitly named env and supports a model env fallback", () => {
|
|
540
729
|
const config = baseConfig({
|
|
541
730
|
model: "custom-model",
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
3
|
+
import {
|
|
4
|
+
BOOT_SCRUB_TARGETS,
|
|
5
|
+
bootScrubCursorKey,
|
|
6
|
+
bootScrubDoneKey,
|
|
7
|
+
runBootScrubSweep,
|
|
8
|
+
} from "../be/boot-scrub-sweep";
|
|
9
|
+
import { closeDb, getDb, getDbClient, initDb, isSqliteVecAvailable } from "../be/db";
|
|
10
|
+
import { getMemoryStore } from "../be/memory";
|
|
11
|
+
import { contentSha256 } from "../commands/profile-sync";
|
|
12
|
+
import {
|
|
13
|
+
refreshSecretScrubberCache,
|
|
14
|
+
registerVolatileSecret,
|
|
15
|
+
SCRUBBER_RULES_VERSION,
|
|
16
|
+
} from "../utils/secret-scrubber";
|
|
17
|
+
import { randomToken, type SyntheticSecret, syntheticSecret } from "./synthetic-secret-helpers";
|
|
18
|
+
|
|
19
|
+
const TEST_DB_PATH = "./test-boot-scrub-sweep.sqlite";
|
|
20
|
+
const AGENT_ID = "5c2b0000-0000-4000-8000-000000000001";
|
|
21
|
+
// Distinctive non-secret words: FTS must still find the row by them after redaction.
|
|
22
|
+
const MARKER_WORD = "quokkazebra";
|
|
23
|
+
|
|
24
|
+
let known: SyntheticSecret;
|
|
25
|
+
let envName: string;
|
|
26
|
+
let envValue: string;
|
|
27
|
+
let password: string;
|
|
28
|
+
|
|
29
|
+
/** Text carrying all three secret forms: known value, env value and a `*_PASSWORD=` assignment. */
|
|
30
|
+
function secretText(label: string): string {
|
|
31
|
+
return `${label} ${MARKER_WORD} token=${known.value} env=${envValue} X_DEMO_PASSWORD=${password} end`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function expectRedacted(value: string | null | undefined, where: string): void {
|
|
35
|
+
expect(value, where).toBeString();
|
|
36
|
+
for (const secret of [known.value, envValue, password]) {
|
|
37
|
+
expect(value!.includes(secret), `${where} still holds a secret`).toBe(false);
|
|
38
|
+
}
|
|
39
|
+
expect(value!, where).toContain("[REDACTED:");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function writeAudit(): Promise<{ tbl: string; id: string }[]> {
|
|
43
|
+
return getDbClient().query<{ tbl: string; id: string }>("SELECT tbl, id FROM scrub_write_audit");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function ftsHits(term: string): Promise<string[]> {
|
|
47
|
+
const rows = await getDbClient().query<{ memory_id: string }>(
|
|
48
|
+
"SELECT memory_id FROM memory_fts WHERE memory_fts MATCH ?",
|
|
49
|
+
[`"${term}"`],
|
|
50
|
+
);
|
|
51
|
+
return rows.map((r) => r.memory_id);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const ids = {
|
|
55
|
+
log: crypto.randomUUID(),
|
|
56
|
+
logControl: crypto.randomUUID(),
|
|
57
|
+
task: crypto.randomUUID(),
|
|
58
|
+
taskControl: crypto.randomUUID(),
|
|
59
|
+
event: crypto.randomUUID(),
|
|
60
|
+
run: crypto.randomUUID(),
|
|
61
|
+
step: crypto.randomUUID(),
|
|
62
|
+
memory: "",
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
describe("boot retro-sweep", () => {
|
|
66
|
+
beforeAll(async () => {
|
|
67
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
68
|
+
try {
|
|
69
|
+
await unlink(TEST_DB_PATH + suffix);
|
|
70
|
+
} catch {}
|
|
71
|
+
}
|
|
72
|
+
initDb(TEST_DB_PATH);
|
|
73
|
+
// workflow_run_steps.runId references workflow_runs; the sweep never
|
|
74
|
+
// touches FKs, so seed the step without building a whole workflow.
|
|
75
|
+
getDb().run("PRAGMA foreign_keys = OFF");
|
|
76
|
+
|
|
77
|
+
known = syntheticSecret("sweep");
|
|
78
|
+
envName = "X_DEMO_SWEEP_API_KEY";
|
|
79
|
+
envValue = randomToken(32);
|
|
80
|
+
process.env[envName] = envValue;
|
|
81
|
+
refreshSecretScrubberCache();
|
|
82
|
+
password = randomToken(24);
|
|
83
|
+
|
|
84
|
+
const db = getDbClient();
|
|
85
|
+
// Every UPDATE on a swept table lands here: proves untouched rows saw no write.
|
|
86
|
+
await db.run("CREATE TABLE scrub_write_audit (tbl TEXT NOT NULL, id TEXT NOT NULL)");
|
|
87
|
+
for (const { table } of BOOT_SCRUB_TARGETS) {
|
|
88
|
+
await db.run(
|
|
89
|
+
`CREATE TRIGGER audit_${table} AFTER UPDATE ON ${table}
|
|
90
|
+
BEGIN INSERT INTO scrub_write_audit (tbl, id) VALUES ('${table}', NEW.id); END`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const now = new Date().toISOString();
|
|
95
|
+
await db.run(
|
|
96
|
+
`INSERT INTO session_logs (id, sessionId, iteration, content, lineNumber, createdAt)
|
|
97
|
+
VALUES (?, 's1', 1, ?, 1, ?), (?, 's1', 1, ?, 2, ?)`,
|
|
98
|
+
[ids.log, secretText("log"), now, ids.logControl, "plain log line, nothing to hide", now],
|
|
99
|
+
);
|
|
100
|
+
await db.run(
|
|
101
|
+
`INSERT INTO agent_tasks (id, task, output, failureReason, progress, createdAt, lastUpdatedAt)
|
|
102
|
+
VALUES (?, ?, ?, ?, ?, ?, ?), (?, 'control task', 'control output', NULL, NULL, ?, ?)`,
|
|
103
|
+
[
|
|
104
|
+
ids.task,
|
|
105
|
+
secretText("task"),
|
|
106
|
+
secretText("output"),
|
|
107
|
+
secretText("failure"),
|
|
108
|
+
secretText("progress"),
|
|
109
|
+
now,
|
|
110
|
+
now,
|
|
111
|
+
ids.taskControl,
|
|
112
|
+
now,
|
|
113
|
+
now,
|
|
114
|
+
],
|
|
115
|
+
);
|
|
116
|
+
await db.run(
|
|
117
|
+
`INSERT INTO events (id, category, event, source, data, createdAt)
|
|
118
|
+
VALUES (?, 'test', 'test.event', 'test', ?, ?)`,
|
|
119
|
+
[ids.event, JSON.stringify({ note: secretText("event"), nested: { n: 1 } }), now],
|
|
120
|
+
);
|
|
121
|
+
await db.run(
|
|
122
|
+
`INSERT INTO workflow_run_steps (id, runId, nodeId, nodeType, input, output, error, diagnostics)
|
|
123
|
+
VALUES (?, ?, 'n1', 'agent-task', ?, ?, ?, ?)`,
|
|
124
|
+
[
|
|
125
|
+
ids.step,
|
|
126
|
+
ids.run,
|
|
127
|
+
JSON.stringify({ prompt: secretText("input") }),
|
|
128
|
+
JSON.stringify({ result: secretText("step-output") }),
|
|
129
|
+
secretText("error"),
|
|
130
|
+
JSON.stringify({ trace: secretText("diagnostics") }),
|
|
131
|
+
],
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const store = getMemoryStore();
|
|
135
|
+
const legacy = {
|
|
136
|
+
name: `note ${known.value}`,
|
|
137
|
+
content: secretText("memory"),
|
|
138
|
+
summary: secretText("summary"),
|
|
139
|
+
};
|
|
140
|
+
const memory = await store.store({
|
|
141
|
+
agentId: AGENT_ID,
|
|
142
|
+
scope: "agent",
|
|
143
|
+
source: "manual",
|
|
144
|
+
...legacy,
|
|
145
|
+
});
|
|
146
|
+
ids.memory = memory.id;
|
|
147
|
+
// store() now scrubs at write; put the raw text back so the row looks like
|
|
148
|
+
// one written before that, which is what the sweep exists for.
|
|
149
|
+
await db.run(
|
|
150
|
+
"UPDATE agent_memory SET name = ?, content = ?, summary = ?, contentHash = ? WHERE id = ?",
|
|
151
|
+
[legacy.name, legacy.content, legacy.summary, contentSha256(legacy.content), memory.id],
|
|
152
|
+
);
|
|
153
|
+
await db.run(
|
|
154
|
+
"UPDATE agent_memory_version SET content = ?, contentHash = ? WHERE memory_id = ?",
|
|
155
|
+
[legacy.content, contentSha256(legacy.content), memory.id],
|
|
156
|
+
);
|
|
157
|
+
await db.run("DELETE FROM memory_fts WHERE memory_id = ?", [memory.id]);
|
|
158
|
+
await db.run("INSERT INTO memory_fts(memory_id, name, content) VALUES (?, ?, ?)", [
|
|
159
|
+
memory.id,
|
|
160
|
+
legacy.name,
|
|
161
|
+
legacy.content,
|
|
162
|
+
]);
|
|
163
|
+
const embedding = new Float32Array(512);
|
|
164
|
+
embedding[0] = 1;
|
|
165
|
+
await store.updateEmbedding(memory.id, embedding, "test");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// The test preload clears volatile secrets after every test.
|
|
169
|
+
beforeEach(() => registerVolatileSecret(known.value, known.name));
|
|
170
|
+
|
|
171
|
+
afterAll(async () => {
|
|
172
|
+
delete process.env[envName];
|
|
173
|
+
known.cleanup();
|
|
174
|
+
refreshSecretScrubberCache();
|
|
175
|
+
closeDb();
|
|
176
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
177
|
+
try {
|
|
178
|
+
await unlink(TEST_DB_PATH + suffix);
|
|
179
|
+
} catch {}
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("redacts every target column, keeps context, JSON and FTS, drops the stale vector", async () => {
|
|
184
|
+
const db = getDbClient();
|
|
185
|
+
// Positive controls: the secret is really in the index and the vector exists.
|
|
186
|
+
expect(await ftsHits(password)).toContain(ids.memory);
|
|
187
|
+
const before = await db.get<{ embedding: Uint8Array | null }>(
|
|
188
|
+
"SELECT embedding FROM agent_memory WHERE id = ?",
|
|
189
|
+
[ids.memory],
|
|
190
|
+
);
|
|
191
|
+
expect(before?.embedding).not.toBeNull();
|
|
192
|
+
if (isSqliteVecAvailable()) {
|
|
193
|
+
expect(
|
|
194
|
+
await db.get("SELECT memory_id FROM memory_vec WHERE memory_id = ?", [ids.memory]),
|
|
195
|
+
).not.toBeNull();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const stats = await runBootScrubSweep({ version: SCRUBBER_RULES_VERSION, reembed: false });
|
|
199
|
+
expect(stats).not.toBeNull();
|
|
200
|
+
const byTable = Object.fromEntries(stats!.map((s) => [s.table, s]));
|
|
201
|
+
expect(byTable.session_logs!.changed).toBe(1);
|
|
202
|
+
expect(byTable.agent_tasks!.changed).toBe(1);
|
|
203
|
+
expect(byTable.agent_memory!.changed).toBe(1);
|
|
204
|
+
expect(byTable.agent_memory_version!.changed).toBe(1);
|
|
205
|
+
expect(byTable.events!.changed).toBe(1);
|
|
206
|
+
expect(byTable.workflow_run_steps!.changed).toBe(1);
|
|
207
|
+
|
|
208
|
+
const log = await db.get<{ content: string }>("SELECT content FROM session_logs WHERE id = ?", [
|
|
209
|
+
ids.log,
|
|
210
|
+
]);
|
|
211
|
+
expectRedacted(log?.content, "session_logs.content");
|
|
212
|
+
expect(log!.content).toContain(MARKER_WORD);
|
|
213
|
+
|
|
214
|
+
const task = await db.get<Record<string, string>>(
|
|
215
|
+
"SELECT task, output, failureReason, progress FROM agent_tasks WHERE id = ?",
|
|
216
|
+
[ids.task],
|
|
217
|
+
);
|
|
218
|
+
for (const col of ["task", "output", "failureReason", "progress"]) {
|
|
219
|
+
expectRedacted(task?.[col], `agent_tasks.${col}`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const mem = await db.get<{
|
|
223
|
+
name: string;
|
|
224
|
+
content: string;
|
|
225
|
+
summary: string;
|
|
226
|
+
contentHash: string;
|
|
227
|
+
embedding: Uint8Array | null;
|
|
228
|
+
}>("SELECT name, content, summary, contentHash, embedding FROM agent_memory WHERE id = ?", [
|
|
229
|
+
ids.memory,
|
|
230
|
+
]);
|
|
231
|
+
expectRedacted(mem?.name, "agent_memory.name");
|
|
232
|
+
expectRedacted(mem?.content, "agent_memory.content");
|
|
233
|
+
expectRedacted(mem?.summary, "agent_memory.summary");
|
|
234
|
+
expect(mem!.contentHash).toBe(contentSha256(mem!.content));
|
|
235
|
+
expect(mem!.embedding).toBeNull();
|
|
236
|
+
if (isSqliteVecAvailable()) {
|
|
237
|
+
expect(
|
|
238
|
+
await db.get("SELECT memory_id FROM memory_vec WHERE memory_id = ?", [ids.memory]),
|
|
239
|
+
).toBeNull();
|
|
240
|
+
}
|
|
241
|
+
// FTS was rewritten from the scrubbed text, not left stale.
|
|
242
|
+
expect(await ftsHits(MARKER_WORD)).toContain(ids.memory);
|
|
243
|
+
expect(await ftsHits(password)).not.toContain(ids.memory);
|
|
244
|
+
|
|
245
|
+
const version = await db.get<{ content: string; contentHash: string }>(
|
|
246
|
+
"SELECT content, contentHash FROM agent_memory_version WHERE memory_id = ?",
|
|
247
|
+
[ids.memory],
|
|
248
|
+
);
|
|
249
|
+
expectRedacted(version?.content, "agent_memory_version.content");
|
|
250
|
+
expect(version!.contentHash).toBe(contentSha256(version!.content));
|
|
251
|
+
|
|
252
|
+
const event = await db.get<{ data: string }>("SELECT data FROM events WHERE id = ?", [
|
|
253
|
+
ids.event,
|
|
254
|
+
]);
|
|
255
|
+
expectRedacted(event?.data, "events.data");
|
|
256
|
+
const parsed = JSON.parse(event!.data) as { note: string; nested: { n: number } };
|
|
257
|
+
expect(parsed.nested.n).toBe(1);
|
|
258
|
+
expect(parsed.note).toContain(MARKER_WORD);
|
|
259
|
+
|
|
260
|
+
const step = await db.get<Record<string, string>>(
|
|
261
|
+
"SELECT input, output, error, diagnostics FROM workflow_run_steps WHERE id = ?",
|
|
262
|
+
[ids.step],
|
|
263
|
+
);
|
|
264
|
+
for (const col of ["input", "output", "error", "diagnostics"]) {
|
|
265
|
+
expectRedacted(step?.[col], `workflow_run_steps.${col}`);
|
|
266
|
+
}
|
|
267
|
+
for (const col of ["input", "output", "diagnostics"]) {
|
|
268
|
+
expect(() => JSON.parse(step![col]!)).not.toThrow();
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("control rows without a secret see no write", async () => {
|
|
273
|
+
const audit = await writeAudit();
|
|
274
|
+
// Positive control: the trigger fired for the rows that held a secret.
|
|
275
|
+
expect(audit).toContainEqual({ tbl: "session_logs", id: ids.log });
|
|
276
|
+
expect(audit).toContainEqual({ tbl: "agent_tasks", id: ids.task });
|
|
277
|
+
// The controls were never written.
|
|
278
|
+
expect(audit.map((a) => a.id)).not.toContain(ids.logControl);
|
|
279
|
+
expect(audit.map((a) => a.id)).not.toContain(ids.taskControl);
|
|
280
|
+
const control = await getDbClient().get<{ content: string }>(
|
|
281
|
+
"SELECT content FROM session_logs WHERE id = ?",
|
|
282
|
+
[ids.logControl],
|
|
283
|
+
);
|
|
284
|
+
expect(control?.content).toBe("plain log line, nothing to hide");
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("marks the version done and is a no-op on rerun; a new version runs again", async () => {
|
|
288
|
+
const db = getDbClient();
|
|
289
|
+
expect(
|
|
290
|
+
await db.get("SELECT key FROM seed_state WHERE kind = 'maintenance' AND key = ?", [
|
|
291
|
+
bootScrubDoneKey(SCRUBBER_RULES_VERSION),
|
|
292
|
+
]),
|
|
293
|
+
).not.toBeNull();
|
|
294
|
+
const cursors = await db.query("SELECT key FROM seed_state WHERE key LIKE 'boot-scrub-v%:%'");
|
|
295
|
+
expect(cursors).toHaveLength(0);
|
|
296
|
+
|
|
297
|
+
const auditBefore = (await writeAudit()).length;
|
|
298
|
+
expect(await runBootScrubSweep({ version: SCRUBBER_RULES_VERSION, reembed: false })).toBeNull();
|
|
299
|
+
|
|
300
|
+
const bumped = SCRUBBER_RULES_VERSION + 100;
|
|
301
|
+
const stats = await runBootScrubSweep({ version: bumped, reembed: false });
|
|
302
|
+
expect(stats).not.toBeNull();
|
|
303
|
+
expect(stats!.every((s) => s.changed === 0)).toBe(true);
|
|
304
|
+
expect(stats!.find((s) => s.table === "session_logs")!.scanned).toBeGreaterThanOrEqual(2);
|
|
305
|
+
expect((await writeAudit()).length).toBe(auditBefore);
|
|
306
|
+
expect(
|
|
307
|
+
await db.get("SELECT key FROM seed_state WHERE kind = 'maintenance' AND key = ?", [
|
|
308
|
+
bootScrubDoneKey(bumped),
|
|
309
|
+
]),
|
|
310
|
+
).not.toBeNull();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("an interrupted run resumes from the cursor without rescanning batch 1", async () => {
|
|
314
|
+
const db = getDbClient();
|
|
315
|
+
const now = new Date().toISOString();
|
|
316
|
+
for (let i = 0; i < 5; i++) {
|
|
317
|
+
await db.run(
|
|
318
|
+
`INSERT INTO session_logs (id, sessionId, iteration, content, lineNumber, createdAt)
|
|
319
|
+
VALUES (?, 's2', 1, ?, ?, ?)`,
|
|
320
|
+
[crypto.randomUUID(), secretText(`resume-${i}`), 10 + i, now],
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
const version = SCRUBBER_RULES_VERSION + 200;
|
|
324
|
+
let firstBatch: string[] = [];
|
|
325
|
+
await expect(
|
|
326
|
+
runBootScrubSweep({
|
|
327
|
+
version,
|
|
328
|
+
batchSize: 2,
|
|
329
|
+
reembed: false,
|
|
330
|
+
afterBatch: ({ table, ids: batchIds }) => {
|
|
331
|
+
if (table !== "session_logs") return;
|
|
332
|
+
firstBatch = batchIds;
|
|
333
|
+
throw new Error("interrupted after batch 1");
|
|
334
|
+
},
|
|
335
|
+
}),
|
|
336
|
+
).rejects.toThrow("interrupted after batch 1");
|
|
337
|
+
expect(firstBatch).toHaveLength(2);
|
|
338
|
+
|
|
339
|
+
const cursor = await db.get<{ seededHash: string }>(
|
|
340
|
+
"SELECT seededHash FROM seed_state WHERE kind = 'maintenance' AND key = ?",
|
|
341
|
+
[bootScrubCursorKey(version, "session_logs")],
|
|
342
|
+
);
|
|
343
|
+
expect(cursor?.seededHash).toBe(firstBatch[1]);
|
|
344
|
+
expect(
|
|
345
|
+
await db.get("SELECT key FROM seed_state WHERE key = ?", [bootScrubDoneKey(version)]),
|
|
346
|
+
).toBeNull();
|
|
347
|
+
|
|
348
|
+
const resumed: string[] = [];
|
|
349
|
+
const stats = await runBootScrubSweep({
|
|
350
|
+
version,
|
|
351
|
+
batchSize: 2,
|
|
352
|
+
reembed: false,
|
|
353
|
+
afterBatch: ({ table, ids: batchIds }) => {
|
|
354
|
+
if (table === "session_logs") resumed.push(...batchIds);
|
|
355
|
+
},
|
|
356
|
+
});
|
|
357
|
+
expect(resumed.length).toBeGreaterThan(0);
|
|
358
|
+
for (const id of firstBatch) expect(resumed).not.toContain(id);
|
|
359
|
+
const total = await db.get<{ n: number }>("SELECT COUNT(*) AS n FROM session_logs");
|
|
360
|
+
expect(stats!.find((s) => s.table === "session_logs")!.scanned).toBe(total!.n - 2);
|
|
361
|
+
|
|
362
|
+
// Batch 1 was committed before the interrupt, the rest by the resumed run.
|
|
363
|
+
const remaining = await db.query<{ content: string }>(
|
|
364
|
+
"SELECT content FROM session_logs WHERE sessionId = 's2'",
|
|
365
|
+
);
|
|
366
|
+
for (const row of remaining) expectRedacted(row.content, "resumed session_logs.content");
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test("skips a row whose scrub would break valid JSON", async () => {
|
|
370
|
+
const db = getDbClient();
|
|
371
|
+
const id = crypto.randomUUID();
|
|
372
|
+
const data = JSON.stringify({ msg: "BREAKJSON here" });
|
|
373
|
+
await db.run(
|
|
374
|
+
`INSERT INTO events (id, category, event, source, data, createdAt)
|
|
375
|
+
VALUES (?, 'test', 'test.event', 'test', ?, ?)`,
|
|
376
|
+
[id, data, new Date().toISOString()],
|
|
377
|
+
);
|
|
378
|
+
// Force a scrub result that is not valid JSON (an unbalanced quote).
|
|
379
|
+
const stats = await runBootScrubSweep({
|
|
380
|
+
version: SCRUBBER_RULES_VERSION + 300,
|
|
381
|
+
reembed: false,
|
|
382
|
+
scrub: (text) => text.replace("BREAKJSON", '"'),
|
|
383
|
+
});
|
|
384
|
+
const events = stats!.find((s) => s.table === "events")!;
|
|
385
|
+
expect(events.skippedInvalidJson).toBe(1);
|
|
386
|
+
expect(events.changed).toBe(0);
|
|
387
|
+
const row = await db.get<{ data: string }>("SELECT data FROM events WHERE id = ?", [id]);
|
|
388
|
+
expect(row?.data).toBe(data);
|
|
389
|
+
});
|
|
390
|
+
});
|
|
@@ -181,23 +181,15 @@ async function callPoll(agentId: string): Promise<{
|
|
|
181
181
|
}> {
|
|
182
182
|
let status = 200;
|
|
183
183
|
let bodyStr = "";
|
|
184
|
-
const headers: Record<string, string> = {};
|
|
185
|
-
|
|
186
184
|
const req = {
|
|
187
185
|
method: "GET",
|
|
188
186
|
url: "/api/poll",
|
|
189
187
|
headers: { "x-agent-id": agentId },
|
|
190
188
|
} as unknown as Parameters<typeof handlePoll>[0];
|
|
191
|
-
|
|
192
189
|
const res = {
|
|
193
|
-
setHeader(
|
|
194
|
-
|
|
195
|
-
},
|
|
196
|
-
writeHead(code: number, h?: Record<string, string>) {
|
|
190
|
+
setHeader() {},
|
|
191
|
+
writeHead(code: number) {
|
|
197
192
|
status = code;
|
|
198
|
-
if (h) {
|
|
199
|
-
for (const [k, v] of Object.entries(h)) headers[k.toLowerCase()] = v;
|
|
200
|
-
}
|
|
201
193
|
},
|
|
202
194
|
end(body?: string) {
|
|
203
195
|
bodyStr = body ?? "";
|
|
@@ -288,17 +280,11 @@ describe("user budget scope", () => {
|
|
|
288
280
|
expect(result.allowed).toBe(true);
|
|
289
281
|
});
|
|
290
282
|
|
|
291
|
-
test("/mcp-user task is refused
|
|
283
|
+
test("/mcp-user task is refused when the Lead polls after the user budget is spent", async () => {
|
|
292
284
|
const server = createMcpUserTestServer();
|
|
293
285
|
const port = await listenOnFreePort(server, "127.0.0.1");
|
|
294
286
|
try {
|
|
295
287
|
const lead = await createAgent({ name: "lead", isLead: true, status: "idle", maxTasks: 1 });
|
|
296
|
-
const worker = await createAgent({
|
|
297
|
-
name: "worker",
|
|
298
|
-
isLead: false,
|
|
299
|
-
status: "idle",
|
|
300
|
-
maxTasks: 1,
|
|
301
|
-
});
|
|
302
288
|
const user = await createUser({ name: "MCP Budget User", dailyBudgetUsd: 0.5 });
|
|
303
289
|
await upsertBudget("user", user.id, 0.5);
|
|
304
290
|
const token = await mintToken(user.id, "qa", ACTOR);
|
|
@@ -329,21 +315,21 @@ describe("user budget scope", () => {
|
|
|
329
315
|
await createSessionCost({
|
|
330
316
|
sessionId: `sess-${crypto.randomUUID()}`,
|
|
331
317
|
taskId,
|
|
332
|
-
agentId:
|
|
318
|
+
agentId: lead.id,
|
|
333
319
|
totalCostUsd: 0.5,
|
|
334
320
|
durationMs: 1000,
|
|
335
321
|
numTurns: 1,
|
|
336
322
|
model: "test-model",
|
|
337
323
|
});
|
|
338
324
|
|
|
339
|
-
const firstPoll = await callPoll(
|
|
325
|
+
const firstPoll = await callPoll(lead.id);
|
|
340
326
|
expect(firstPoll.status).toBe(200);
|
|
341
327
|
if ("error" in firstPoll.body) throw new Error("unexpected poll error");
|
|
342
328
|
expect(firstPoll.body.trigger?.type).toBe("budget_refused");
|
|
343
329
|
expect((firstPoll.body.trigger as { cause: string }).cause).toBe("user");
|
|
344
330
|
expect((firstPoll.body.trigger as { userSpend: number }).userSpend).toBe(0.5);
|
|
345
331
|
expect((firstPoll.body.trigger as { userBudget: number }).userBudget).toBe(0.5);
|
|
346
|
-
expect((await getTaskById(taskId))?.status).toBe("
|
|
332
|
+
expect((await getTaskById(taskId))?.status).toBe("pending");
|
|
347
333
|
|
|
348
334
|
const firstDedup = await getDbClient().get<{
|
|
349
335
|
follow_up_task_id: string | null;
|
|
@@ -357,7 +343,7 @@ describe("user budget scope", () => {
|
|
|
357
343
|
const firstFollowUpId = firstDedup?.follow_up_task_id;
|
|
358
344
|
expect(firstFollowUpId ? (await getTaskById(firstFollowUpId))?.agentId : null).toBe(lead.id);
|
|
359
345
|
|
|
360
|
-
const secondPoll = await callPoll(
|
|
346
|
+
const secondPoll = await callPoll(lead.id);
|
|
361
347
|
expect(secondPoll.status).toBe(200);
|
|
362
348
|
if ("error" in secondPoll.body) throw new Error("unexpected poll error");
|
|
363
349
|
expect(secondPoll.body.trigger?.type).toBe("budget_refused");
|