@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
|
@@ -887,6 +887,96 @@ describe("isCredCheckDisabled", () => {
|
|
|
887
887
|
});
|
|
888
888
|
|
|
889
889
|
describe("buildCredStatusReport", () => {
|
|
890
|
+
const HOME = "/home/worker";
|
|
891
|
+
|
|
892
|
+
for (const [provider, authPath] of [
|
|
893
|
+
["pi", ".pi/agent/auth.json"],
|
|
894
|
+
["opencode", ".local/share/opencode/auth.json"],
|
|
895
|
+
] as const) {
|
|
896
|
+
test(`${provider}: auth.json gives a successful presence-only live test`, async () => {
|
|
897
|
+
const realFetch = globalThis.fetch;
|
|
898
|
+
const fetchMock = mock(async () => {
|
|
899
|
+
throw new Error("file credentials must not be sent to a provider endpoint");
|
|
900
|
+
});
|
|
901
|
+
globalThis.fetch = fetchMock as typeof fetch;
|
|
902
|
+
try {
|
|
903
|
+
for (const env of [{ HOME }, { HOME, OPENROUTER_API_KEY: "unused-key" }]) {
|
|
904
|
+
const snap = await buildCredStatusReport(
|
|
905
|
+
provider,
|
|
906
|
+
env,
|
|
907
|
+
{ homeDir: HOME, fs: fsWith(new Set([`${HOME}/${authPath}`])) },
|
|
908
|
+
"boot",
|
|
909
|
+
);
|
|
910
|
+
expect(snap.ready).toBe(true);
|
|
911
|
+
expect(snap.satisfiedBy).toBe("file");
|
|
912
|
+
expect(snap.liveTest).toMatchObject({ ok: true, error: null, latency_ms: 0 });
|
|
913
|
+
}
|
|
914
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
915
|
+
} finally {
|
|
916
|
+
globalThis.fetch = realFetch;
|
|
917
|
+
}
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
test(`${provider}: missing credentials do not become a successful live test`, async () => {
|
|
921
|
+
const snap = await buildCredStatusReport(
|
|
922
|
+
provider,
|
|
923
|
+
{},
|
|
924
|
+
{ homeDir: HOME, fs: noFiles },
|
|
925
|
+
"boot",
|
|
926
|
+
);
|
|
927
|
+
expect(snap.ready).toBe(false);
|
|
928
|
+
expect(snap.liveTest).toBeNull();
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
for (const key of ["OPENROUTER_API_KEY", "ANTHROPIC_API_KEY", "OPENAI_API_KEY"] as const) {
|
|
932
|
+
test(`${provider}: ${key} still receives a live probe and reports its result`, async () => {
|
|
933
|
+
const realFetch = globalThis.fetch;
|
|
934
|
+
let responseStatus = 200;
|
|
935
|
+
const fetchMock = mock(async (_url: string | URL | Request, _init?: RequestInit) => {
|
|
936
|
+
return new Response("{}", { status: responseStatus });
|
|
937
|
+
});
|
|
938
|
+
globalThis.fetch = fetchMock as typeof fetch;
|
|
939
|
+
try {
|
|
940
|
+
const env = { [key]: "provider-key" };
|
|
941
|
+
const opts = { homeDir: HOME, fs: noFiles };
|
|
942
|
+
const success = await buildCredStatusReport(provider, env, opts, "boot");
|
|
943
|
+
expect(success.liveTest?.ok).toBe(true);
|
|
944
|
+
responseStatus = 401;
|
|
945
|
+
const failure = await buildCredStatusReport(provider, env, opts, "post_task");
|
|
946
|
+
expect(failure.ready).toBe(true);
|
|
947
|
+
expect(failure.liveTest?.ok).toBe(false);
|
|
948
|
+
expect(failure.liveTest?.error).toContain("HTTP 401");
|
|
949
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
950
|
+
expect(String(fetchMock.mock.calls[0]?.[0])).toMatch(/\/models$/);
|
|
951
|
+
} finally {
|
|
952
|
+
globalThis.fetch = realFetch;
|
|
953
|
+
}
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
test("pi: a Google model with only GEMINI_API_KEY gives a successful presence-only live test", async () => {
|
|
959
|
+
const realFetch = globalThis.fetch;
|
|
960
|
+
const fetchMock = mock(async () => {
|
|
961
|
+
throw new Error("a presence-only credential must not issue a live probe");
|
|
962
|
+
});
|
|
963
|
+
globalThis.fetch = fetchMock as typeof fetch;
|
|
964
|
+
try {
|
|
965
|
+
const snap = await buildCredStatusReport(
|
|
966
|
+
"pi",
|
|
967
|
+
{ MODEL_OVERRIDE: "google/gemini-3-flash-preview", GEMINI_API_KEY: "google-key" },
|
|
968
|
+
{ homeDir: HOME, fs: noFiles },
|
|
969
|
+
"boot",
|
|
970
|
+
);
|
|
971
|
+
expect(snap.ready).toBe(true);
|
|
972
|
+
expect(snap.satisfiedBy).toBe("env");
|
|
973
|
+
expect(snap.liveTest).toMatchObject({ ok: true, error: null, latency_ms: 0 });
|
|
974
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
975
|
+
} finally {
|
|
976
|
+
globalThis.fetch = realFetch;
|
|
977
|
+
}
|
|
978
|
+
});
|
|
979
|
+
|
|
890
980
|
test("not ready → no live test, snapshot mirrors presence check", async () => {
|
|
891
981
|
const snap = await buildCredStatusReport("claude", {}, {}, "boot");
|
|
892
982
|
expect(snap.ready).toBe(false);
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { createServer as createHttpServer, type Server } from "node:http";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { closeDb, getDb, initDb, upsertKv } from "../be/db";
|
|
6
|
+
import { upsertCodexDeviceFlow } from "../be/db/codex-oauth-device-flows";
|
|
7
|
+
import {
|
|
8
|
+
DbQueryDeniedTableError,
|
|
9
|
+
executeReadOnlyQueryGated,
|
|
10
|
+
handleDbQuery,
|
|
11
|
+
} from "../http/db-query";
|
|
12
|
+
import { executeReadOnlyQuery } from "../http/db-query-shared";
|
|
13
|
+
import { getPathSegments, parseQueryParams } from "../http/utils";
|
|
14
|
+
import { listenOnFreePort } from "./test-net";
|
|
15
|
+
|
|
16
|
+
// A real on-disk database: the bounded path spawns a child process that opens
|
|
17
|
+
// its own connection, so an in-memory test template would be invisible to it.
|
|
18
|
+
const DB_PATH = join(tmpdir(), `db-query-denylist-${process.pid}-${Date.now()}.sqlite`);
|
|
19
|
+
|
|
20
|
+
const testGlobals = globalThis as typeof globalThis & {
|
|
21
|
+
__testMigrationTemplate?: Uint8Array;
|
|
22
|
+
};
|
|
23
|
+
let savedTemplate: Uint8Array | undefined;
|
|
24
|
+
|
|
25
|
+
// Built at runtime so no secret-shaped literal lands in the repo.
|
|
26
|
+
const syntheticToken = () => `gh${"p"}_${"K".repeat(36)}`;
|
|
27
|
+
|
|
28
|
+
async function removeDb(): Promise<void> {
|
|
29
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
30
|
+
try {
|
|
31
|
+
await Bun.file(`${DB_PATH}${suffix}`).delete();
|
|
32
|
+
} catch {}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
beforeAll(async () => {
|
|
37
|
+
savedTemplate = testGlobals.__testMigrationTemplate;
|
|
38
|
+
testGlobals.__testMigrationTemplate = undefined;
|
|
39
|
+
closeDb();
|
|
40
|
+
await removeDb();
|
|
41
|
+
initDb(DB_PATH);
|
|
42
|
+
const db = getDb();
|
|
43
|
+
db.run("CREATE VIEW denylist_test_view AS SELECT * FROM oauth_authorizations");
|
|
44
|
+
db.run("CREATE TABLE denylist_test_notes (id INTEGER PRIMARY KEY, body TEXT)");
|
|
45
|
+
db.run("INSERT INTO denylist_test_notes (body) VALUES (?)", [
|
|
46
|
+
`deploy used ${syntheticToken()} from the env dump`,
|
|
47
|
+
]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
afterAll(async () => {
|
|
51
|
+
closeDb();
|
|
52
|
+
testGlobals.__testMigrationTemplate = savedTemplate;
|
|
53
|
+
await removeDb();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const DENIED_QUERIES: Array<[string, string, string]> = [
|
|
57
|
+
["direct read", "SELECT * FROM oauth_authorizations", "oauth_authorizations"],
|
|
58
|
+
["alias", "SELECT a.accessToken FROM oauth_authorizations AS a", "oauth_authorizations"],
|
|
59
|
+
["CTE", "WITH t AS (SELECT * FROM oauth_authorizations) SELECT * FROM t", "oauth_authorizations"],
|
|
60
|
+
[
|
|
61
|
+
"join",
|
|
62
|
+
"SELECT n.body, p.codeVerifier FROM denylist_test_notes n JOIN oauth_pending p ON 1 = 1",
|
|
63
|
+
"oauth_pending",
|
|
64
|
+
],
|
|
65
|
+
["subquery", "SELECT * FROM (SELECT * FROM user_tokens)", "user_tokens"],
|
|
66
|
+
["view", "SELECT * FROM denylist_test_view", "oauth_authorizations"],
|
|
67
|
+
["scalar subquery", "SELECT (SELECT clientSecret FROM oauth_apps LIMIT 1) AS s", "oauth_apps"],
|
|
68
|
+
["count only", "SELECT COUNT(*) FROM session_tokens", "session_tokens"],
|
|
69
|
+
["mixed case", "select * from Script_Apis", "script_apis"],
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
describe("db-query credential-table denylist", () => {
|
|
73
|
+
for (const [label, sql, table] of DENIED_QUERIES) {
|
|
74
|
+
test(`in-process path rejects a ${label}`, () => {
|
|
75
|
+
expect(() => executeReadOnlyQuery(sql)).toThrow(DbQueryDeniedTableError);
|
|
76
|
+
expect(() => executeReadOnlyQuery(sql)).toThrow(table);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
test("bounded path rejects before spawning a child", async () => {
|
|
81
|
+
for (const [, sql, table] of DENIED_QUERIES) {
|
|
82
|
+
await expect(executeReadOnlyQueryGated(sql, [], 5_000, 10)).rejects.toThrow(table);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("allows swarm_config, EXPLAIN and a table name in a string literal", async () => {
|
|
87
|
+
const config = await executeReadOnlyQueryGated("SELECT key FROM swarm_config", [], 5_000, 10);
|
|
88
|
+
expect(config.columns).toEqual(["key"]);
|
|
89
|
+
|
|
90
|
+
const explained = executeReadOnlyQuery("EXPLAIN QUERY PLAN SELECT * FROM oauth_authorizations");
|
|
91
|
+
expect(explained.columns.length).toBeGreaterThan(0);
|
|
92
|
+
|
|
93
|
+
const literal = executeReadOnlyQuery("SELECT 'oauth_authorizations' AS name");
|
|
94
|
+
expect(literal.rows).toEqual([["oauth_authorizations"]]);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("POST /api/db-query", () => {
|
|
99
|
+
let server: Server;
|
|
100
|
+
let baseUrl = "";
|
|
101
|
+
|
|
102
|
+
beforeAll(async () => {
|
|
103
|
+
server = createHttpServer(async (req, res) => {
|
|
104
|
+
const handled = await handleDbQuery(
|
|
105
|
+
req,
|
|
106
|
+
res,
|
|
107
|
+
getPathSegments(req.url || ""),
|
|
108
|
+
parseQueryParams(req.url || ""),
|
|
109
|
+
);
|
|
110
|
+
if (!handled) {
|
|
111
|
+
res.writeHead(404);
|
|
112
|
+
res.end();
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
baseUrl = `http://localhost:${await listenOnFreePort(server)}`;
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
afterAll(async () => {
|
|
119
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
async function post(sql: string) {
|
|
123
|
+
const res = await fetch(`${baseUrl}/api/db-query`, {
|
|
124
|
+
method: "POST",
|
|
125
|
+
headers: { "Content-Type": "application/json" },
|
|
126
|
+
body: JSON.stringify({ sql }),
|
|
127
|
+
});
|
|
128
|
+
return { status: res.status, body: (await res.json()) as Record<string, unknown> };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
test("rejects a credential table with a clear 400", async () => {
|
|
132
|
+
const { status, body } = await post("SELECT * FROM oauth_authorizations");
|
|
133
|
+
expect(status).toBe(400);
|
|
134
|
+
expect(String(body.error)).toContain("oauth_authorizations");
|
|
135
|
+
expect(String(body.error)).toContain("credentials");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("redacts a secret stored in an allowed table and keeps the context", async () => {
|
|
139
|
+
const { status, body } = await post("SELECT body FROM denylist_test_notes");
|
|
140
|
+
expect(status).toBe(200);
|
|
141
|
+
const serialized = JSON.stringify(body.rows);
|
|
142
|
+
expect(serialized).not.toContain(syntheticToken());
|
|
143
|
+
expect(serialized).toContain("[REDACTED:");
|
|
144
|
+
expect(serialized).toContain("from the env dump");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("denies Codex device-login state and keeps general KV readable", async () => {
|
|
148
|
+
const marker = `device-state-${crypto.randomUUID()}`;
|
|
149
|
+
await upsertCodexDeviceFlow({
|
|
150
|
+
flowId: crypto.randomUUID(),
|
|
151
|
+
state: marker,
|
|
152
|
+
expiresAt: Date.now() + 60_000,
|
|
153
|
+
});
|
|
154
|
+
await upsertKv({
|
|
155
|
+
namespace: "denylist-test",
|
|
156
|
+
key: "note",
|
|
157
|
+
value: "plain kv value",
|
|
158
|
+
valueType: "string",
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
for (const sql of [
|
|
162
|
+
"SELECT * FROM codex_oauth_device_flows",
|
|
163
|
+
"SELECT f.state FROM codex_oauth_device_flows f JOIN kv_entries k ON 1",
|
|
164
|
+
]) {
|
|
165
|
+
const { status, body } = await post(sql);
|
|
166
|
+
expect(status).toBe(400);
|
|
167
|
+
expect(String(body.error)).toContain("codex_oauth_device_flows");
|
|
168
|
+
expect(JSON.stringify(body)).not.toContain(marker);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const kv = await post("SELECT namespace, value FROM kv_entries");
|
|
172
|
+
expect(kv.status).toBe(200);
|
|
173
|
+
const rows = JSON.stringify(kv.body.rows);
|
|
174
|
+
expect(rows).toContain("plain kv value");
|
|
175
|
+
expect(rows).not.toContain("codex-oauth-device");
|
|
176
|
+
expect(rows).not.toContain(marker);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("migration 202 moves legacy kv device flows out of kv_entries", async () => {
|
|
180
|
+
const flowId = crypto.randomUUID();
|
|
181
|
+
const marker = `legacy-state-${crypto.randomUUID()}`;
|
|
182
|
+
await upsertKv({
|
|
183
|
+
namespace: "codex-oauth-device",
|
|
184
|
+
key: flowId,
|
|
185
|
+
value: marker,
|
|
186
|
+
valueType: "string",
|
|
187
|
+
expiresAt: Date.now() + 60_000,
|
|
188
|
+
});
|
|
189
|
+
const migration = await Bun.file(
|
|
190
|
+
join(import.meta.dir, "../be/migrations/202_codex_oauth_device_flows.sql"),
|
|
191
|
+
).text();
|
|
192
|
+
getDb().exec(migration);
|
|
193
|
+
|
|
194
|
+
const kv = await post("SELECT key FROM kv_entries WHERE namespace = 'codex-oauth-device'");
|
|
195
|
+
expect(kv.status).toBe(200);
|
|
196
|
+
expect(kv.body.rows).toEqual([]);
|
|
197
|
+
const moved = getDb()
|
|
198
|
+
.query("SELECT state FROM codex_oauth_device_flows WHERE flow_id = ?")
|
|
199
|
+
.get(flowId) as { state: string } | null;
|
|
200
|
+
expect(moved?.state).toBe(marker);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
@@ -78,7 +78,7 @@ describe("docs API index footprint", () => {
|
|
|
78
78
|
expect(tasks).toContain('"path":"/api/task-templates","method":"get"');
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
-
test("OpenAPI and generated pages keep all
|
|
81
|
+
test("OpenAPI and generated pages keep all 399 operations", async () => {
|
|
82
82
|
const spec = await Bun.file(resolve(repoRoot, "openapi.json")).json();
|
|
83
83
|
const operationKeys = [];
|
|
84
84
|
const tags = new Set();
|
|
@@ -103,8 +103,8 @@ describe("docs API index footprint", () => {
|
|
|
103
103
|
expect(spec.paths["/api/agents/{id}/runtime"]?.get).toBeDefined();
|
|
104
104
|
|
|
105
105
|
const generatedKeys = await generatedOperationKeys();
|
|
106
|
-
expect(operationKeys).toHaveLength(
|
|
107
|
-
expect(generatedKeys).toHaveLength(
|
|
106
|
+
expect(operationKeys).toHaveLength(399);
|
|
107
|
+
expect(generatedKeys).toHaveLength(399);
|
|
108
108
|
expect(new Set(generatedKeys)).toEqual(new Set(operationKeys));
|
|
109
109
|
});
|
|
110
110
|
|
|
@@ -43,6 +43,8 @@ const STUB_BINARIES = [
|
|
|
43
43
|
"claude",
|
|
44
44
|
"codex",
|
|
45
45
|
"opencode",
|
|
46
|
+
"gemini",
|
|
47
|
+
"copilot",
|
|
46
48
|
"dsh",
|
|
47
49
|
"acp-target-stub",
|
|
48
50
|
"acp target stub",
|
|
@@ -129,6 +131,47 @@ describe("docker-entrypoint.sh: acp target binary verification", () => {
|
|
|
129
131
|
CHILD_PROCESS_TEST_BUDGET_MS,
|
|
130
132
|
);
|
|
131
133
|
|
|
134
|
+
test(
|
|
135
|
+
"resolves the gemini catalog target to the gemini binary",
|
|
136
|
+
async () => {
|
|
137
|
+
const result = await runChain({ HARNESS_PROVIDER: "acp", ACP_TARGET: "gemini" });
|
|
138
|
+
|
|
139
|
+
expect(result.exitCode).toBe(0);
|
|
140
|
+
expect(result.stdout).toContain("ACP target: ");
|
|
141
|
+
expect(result.stdout).toContain("ACP_TARGET='gemini'");
|
|
142
|
+
},
|
|
143
|
+
CHILD_PROCESS_TEST_BUDGET_MS,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
test(
|
|
147
|
+
"resolves the copilot catalog target to the copilot binary",
|
|
148
|
+
async () => {
|
|
149
|
+
const result = await runChain({ HARNESS_PROVIDER: "acp", ACP_TARGET: "copilot" });
|
|
150
|
+
|
|
151
|
+
expect(result.exitCode).toBe(0);
|
|
152
|
+
expect(result.stdout).toContain("ACP target: ");
|
|
153
|
+
expect(result.stdout).toContain("ACP_TARGET='copilot'");
|
|
154
|
+
},
|
|
155
|
+
CHILD_PROCESS_TEST_BUDGET_MS,
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
test(
|
|
159
|
+
"fails boot with an install hint when the copilot binary is missing",
|
|
160
|
+
async () => {
|
|
161
|
+
// No stub dir on PATH, so `copilot` cannot resolve.
|
|
162
|
+
const result = await runChain({
|
|
163
|
+
HARNESS_PROVIDER: "acp",
|
|
164
|
+
ACP_TARGET: "copilot",
|
|
165
|
+
PATH: "/usr/bin:/bin",
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
expect(result.exitCode).not.toBe(0);
|
|
169
|
+
expect(result.stdout).toContain("FATAL: ACP target binary not found: 'copilot'");
|
|
170
|
+
expect(result.stdout).toContain("npm install -g @github/copilot");
|
|
171
|
+
},
|
|
172
|
+
CHILD_PROCESS_TEST_BUDGET_MS,
|
|
173
|
+
);
|
|
174
|
+
|
|
132
175
|
test(
|
|
133
176
|
"resolves a custom target supplied as a command string with argv",
|
|
134
177
|
async () => {
|
|
@@ -178,7 +221,7 @@ describe("docker-entrypoint.sh: acp target binary verification", () => {
|
|
|
178
221
|
|
|
179
222
|
expect(result.exitCode).not.toBe(0);
|
|
180
223
|
expect(result.stdout).toContain(
|
|
181
|
-
"FATAL: unsupported ACP target 'gemini-cli'. Supported targets: opencode, custom.",
|
|
224
|
+
"FATAL: unsupported ACP target 'gemini-cli'. Supported targets: opencode, gemini, copilot, custom.",
|
|
182
225
|
);
|
|
183
226
|
},
|
|
184
227
|
CHILD_PROCESS_TEST_BUDGET_MS,
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
3
|
+
import { closeDb, getDbClient, initDb } from "../be/db";
|
|
4
|
+
import { createEvent, createEventsBatch } from "../be/events";
|
|
5
|
+
import { randomToken, syntheticSecret } from "./synthetic-secret-helpers";
|
|
6
|
+
|
|
7
|
+
const TEST_DB_PATH = "./test-events-scrub.sqlite";
|
|
8
|
+
|
|
9
|
+
beforeAll(async () => {
|
|
10
|
+
try {
|
|
11
|
+
await unlink(TEST_DB_PATH);
|
|
12
|
+
} catch {}
|
|
13
|
+
initDb(TEST_DB_PATH);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterAll(async () => {
|
|
17
|
+
closeDb();
|
|
18
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
19
|
+
try {
|
|
20
|
+
await unlink(`${TEST_DB_PATH}${suffix}`);
|
|
21
|
+
} catch {}
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
async function readData(sessionId: string): Promise<string[]> {
|
|
26
|
+
const rows = await getDbClient().query<{ data: string | null }>(
|
|
27
|
+
"SELECT data FROM events WHERE sessionId = ? ORDER BY rowid",
|
|
28
|
+
[sessionId],
|
|
29
|
+
);
|
|
30
|
+
return rows.map((r) => r.data ?? "");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("events.data scrub at write", () => {
|
|
34
|
+
test("createEvent redacts a secret in the payload and keeps valid JSON", async () => {
|
|
35
|
+
const pat = ["ghp", randomToken(36)].join("_");
|
|
36
|
+
const sessionId = crypto.randomUUID();
|
|
37
|
+
await createEvent({
|
|
38
|
+
category: "session",
|
|
39
|
+
event: "session.start",
|
|
40
|
+
source: "api",
|
|
41
|
+
sessionId,
|
|
42
|
+
data: { note: "cloned repo with token", output: `remote: ${pat} ok`, count: 3 },
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const [raw] = await readData(sessionId);
|
|
46
|
+
expect(raw).toBeDefined();
|
|
47
|
+
expect(raw).not.toContain(pat);
|
|
48
|
+
expect(raw).toContain("[REDACTED:");
|
|
49
|
+
const parsed = JSON.parse(raw as string);
|
|
50
|
+
expect(parsed.note).toBe("cloned repo with token");
|
|
51
|
+
expect(parsed.output).toStartWith("remote: ");
|
|
52
|
+
expect(parsed.output).toEndWith(" ok");
|
|
53
|
+
expect(parsed.count).toBe(3);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("createEventsBatch redacts every row and keeps valid JSON", async () => {
|
|
57
|
+
// Registered per test: the preload clears volatile secrets after each one.
|
|
58
|
+
const known = syntheticSecret("evscrub");
|
|
59
|
+
const sessionId = crypto.randomUUID();
|
|
60
|
+
const count = await createEventsBatch([
|
|
61
|
+
{
|
|
62
|
+
category: "tool",
|
|
63
|
+
event: "tool.start",
|
|
64
|
+
source: "worker",
|
|
65
|
+
sessionId,
|
|
66
|
+
data: { tool: "Bash", input: `echo ${known.value}` },
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
category: "tool",
|
|
70
|
+
event: "tool.end",
|
|
71
|
+
source: "worker",
|
|
72
|
+
sessionId,
|
|
73
|
+
data: { tool: "Bash", nested: { lines: ["first", `leaked ${known.value} here`] } },
|
|
74
|
+
},
|
|
75
|
+
]);
|
|
76
|
+
expect(count).toBe(2);
|
|
77
|
+
|
|
78
|
+
const rows = await readData(sessionId);
|
|
79
|
+
expect(rows).toHaveLength(2);
|
|
80
|
+
for (const raw of rows) {
|
|
81
|
+
expect(raw).not.toContain(known.value);
|
|
82
|
+
expect(raw).toContain(`[REDACTED:${known.name}]`);
|
|
83
|
+
expect(JSON.parse(raw).tool).toBe("Bash");
|
|
84
|
+
}
|
|
85
|
+
expect(JSON.parse(rows[0] as string).input).toStartWith("echo ");
|
|
86
|
+
expect(JSON.parse(rows[1] as string).nested.lines[0]).toBe("first");
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -47,6 +47,7 @@ let leadId = "";
|
|
|
47
47
|
let workerId = "";
|
|
48
48
|
let acpId = "";
|
|
49
49
|
let codexId = "";
|
|
50
|
+
let grokId = "";
|
|
50
51
|
|
|
51
52
|
beforeAll(async () => {
|
|
52
53
|
await removeDbFiles(TEST_DB_PATH);
|
|
@@ -78,6 +79,14 @@ beforeAll(async () => {
|
|
|
78
79
|
harnessProvider: "codex",
|
|
79
80
|
})
|
|
80
81
|
).id;
|
|
82
|
+
grokId = (
|
|
83
|
+
await createAgent({
|
|
84
|
+
name: "validation-grok",
|
|
85
|
+
isLead: false,
|
|
86
|
+
status: "idle",
|
|
87
|
+
harnessProvider: "grok",
|
|
88
|
+
})
|
|
89
|
+
).id;
|
|
81
90
|
|
|
82
91
|
server = createServer((req, res) => {
|
|
83
92
|
setRequestAuth(req, { kind: "operator", fingerprint: "explicit-model-validation-test" });
|
|
@@ -250,6 +259,78 @@ describe("explicitModelError", () => {
|
|
|
250
259
|
});
|
|
251
260
|
});
|
|
252
261
|
|
|
262
|
+
describe("grok model namespace", () => {
|
|
263
|
+
test("another vendor's id, namespace or alias is refused, even with allowCustomModel", async () => {
|
|
264
|
+
for (const model of [
|
|
265
|
+
"claude-opus-5-5",
|
|
266
|
+
"anthropic/claude-opus-5-5",
|
|
267
|
+
"gpt-5.6-sol",
|
|
268
|
+
"openai/gpt-5.6-sol",
|
|
269
|
+
"deepseek/deepseek-v4.1-flash",
|
|
270
|
+
"latest:anthropic/opus",
|
|
271
|
+
"latest:openai/gpt-5*",
|
|
272
|
+
]) {
|
|
273
|
+
for (const allowCustomModel of [false, true]) {
|
|
274
|
+
const error = await explicitModelError({
|
|
275
|
+
model,
|
|
276
|
+
harnessProvider: "grok",
|
|
277
|
+
allowCustomModel,
|
|
278
|
+
});
|
|
279
|
+
expect(error).toContain(`Model "${model}" does not run on the grok harness`);
|
|
280
|
+
expect(error).toContain("openrouter/<vendor>/<id>");
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("bare xAI ids and both OpenRouter spellings pass", async () => {
|
|
286
|
+
for (const model of [
|
|
287
|
+
"grok-4.6",
|
|
288
|
+
"openrouter/deepseek/deepseek-v4.1-flash",
|
|
289
|
+
"latest:openrouter/deepseek/deepseek-v4.1-*",
|
|
290
|
+
]) {
|
|
291
|
+
expect(await explicitModelError({ model, harnessProvider: "grok" })).toBeNull();
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test("xai/<id> runs on grok; the API catalog has no xai section, so it needs allowCustomModel", async () => {
|
|
296
|
+
expect(
|
|
297
|
+
await explicitModelError({
|
|
298
|
+
model: "xai/grok-4.6",
|
|
299
|
+
harnessProvider: "grok",
|
|
300
|
+
allowCustomModel: true,
|
|
301
|
+
}),
|
|
302
|
+
).toBeNull();
|
|
303
|
+
expect(await explicitModelError({ model: "xai/grok-4.6", harnessProvider: "grok" })).toContain(
|
|
304
|
+
'Unknown model "xai/grok-4.6"',
|
|
305
|
+
);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("POST /api/tasks to a grok agent: vendor ids are a 400, xAI and OpenRouter ids a 201", async () => {
|
|
309
|
+
const base = { task: "grok route", agentId: grokId, routingReason: "human_pinned" };
|
|
310
|
+
for (const model of ["claude-opus-5-5", "latest:anthropic/opus"]) {
|
|
311
|
+
const refused = await api("POST", "/api/tasks", { ...base, model });
|
|
312
|
+
expect(refused.status).toBe(400);
|
|
313
|
+
expect(String(refused.body.error)).toContain(
|
|
314
|
+
`does not run on the grok harness of agent "validation-grok" (${grokId})`,
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
for (const model of ["grok-4.6", "openrouter/deepseek/deepseek-v4.1-flash"]) {
|
|
318
|
+
const accepted = await api("POST", "/api/tasks", { ...base, model });
|
|
319
|
+
expect(accepted.status).toBe(201);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test("MODEL_TIER_GROK_<TIER> refuses another vendor's model", () => {
|
|
324
|
+
expect(validateTierConfigValue("MODEL_TIER_GROK_SMART", "claude-opus-5-5")).toBe(
|
|
325
|
+
'Invalid MODEL_TIER_GROK_SMART: model "claude-opus-5-5" does not run on the grok harness.',
|
|
326
|
+
);
|
|
327
|
+
expect(validateTierConfigValue("MODEL_TIER_GROK_SMART", "grok-4.6")).toBeNull();
|
|
328
|
+
expect(
|
|
329
|
+
validateTierConfigValue("MODEL_TIER_GROK_SMOL", "openrouter/deepseek/deepseek-v4.1-flash"),
|
|
330
|
+
).toBeNull();
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
253
334
|
describe("MODEL_TIER_<PROVIDER>_<TIER> values", () => {
|
|
254
335
|
test("a model from another harness family is rejected on write", () => {
|
|
255
336
|
expect(validateTierConfigValue("MODEL_TIER_CODEX_SMART", "claude-opus-5-5")).toBe(
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
createAgent,
|
|
9
9
|
createScheduledTask,
|
|
10
10
|
createTaskExtended,
|
|
11
|
+
createUser,
|
|
11
12
|
createWorkflow,
|
|
12
13
|
createWorkflowRun,
|
|
13
14
|
createWorkflowRunStep,
|
|
@@ -26,8 +27,9 @@ import { handleTasks } from "../http/tasks";
|
|
|
26
27
|
import { dispatchScheduleTarget } from "../scheduler/scheduler";
|
|
27
28
|
import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
|
|
28
29
|
import { createWorkerTaskFollowUp } from "../tasks/worker-follow-up";
|
|
29
|
-
import { registerSendTaskTool } from "../tools/send-task";
|
|
30
|
+
import { registerSendTaskTool, sendTaskHandler } from "../tools/send-task";
|
|
30
31
|
import { registerTaskActionTool } from "../tools/task-action";
|
|
32
|
+
import { userCtx } from "../tools/task-tool-ctx";
|
|
31
33
|
import { markExtensionRequestOrigin } from "../tools/utils";
|
|
32
34
|
import { workflowEventBus } from "../workflows/event-bus";
|
|
33
35
|
import { AgentTaskExecutor } from "../workflows/executors/agent-task";
|
|
@@ -139,6 +141,7 @@ describe("extension task boundaries", () => {
|
|
|
139
141
|
const client = getDbClient();
|
|
140
142
|
await client.run("DELETE FROM agent_tasks");
|
|
141
143
|
await client.run("DELETE FROM extensions");
|
|
144
|
+
await client.run("DELETE FROM users");
|
|
142
145
|
await client.run("DELETE FROM agents");
|
|
143
146
|
});
|
|
144
147
|
|
|
@@ -501,6 +504,35 @@ export default extension;
|
|
|
501
504
|
expect(compatible.body.model).toBe("claude-opus-5-5");
|
|
502
505
|
});
|
|
503
506
|
|
|
507
|
+
test("user send-task rejects an extension that retargets the Lead assignment", async () => {
|
|
508
|
+
await createAgent({ name: "user-task-lead", isLead: true, status: "idle" });
|
|
509
|
+
const worker = await createAgent({ name: "user-task-worker", isLead: false, status: "idle" });
|
|
510
|
+
const user = await createUser({ name: "Extension Guard User" });
|
|
511
|
+
await installSource(
|
|
512
|
+
"retarget-user-task",
|
|
513
|
+
`import type { SwarmExtension } from "swarm-extension";
|
|
514
|
+
const extension: SwarmExtension = (api) => {
|
|
515
|
+
api.on("pre.task.create", () => ({ action: "modify", data: { agentId: "${worker.id}" } }));
|
|
516
|
+
};
|
|
517
|
+
export default extension;
|
|
518
|
+
`,
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
const result = await sendTaskHandler(
|
|
522
|
+
userCtx(user),
|
|
523
|
+
sendTaskArgs("extension must not retarget this user task"),
|
|
524
|
+
);
|
|
525
|
+
|
|
526
|
+
expect(result.ok).toBe(false);
|
|
527
|
+
expect(result.message).toBe(
|
|
528
|
+
"A pre.task.create extension cannot change the user task's Lead assignment.",
|
|
529
|
+
);
|
|
530
|
+
expect(
|
|
531
|
+
(await getDbClient().get<{ count: number }>("SELECT COUNT(*) AS count FROM agent_tasks"))
|
|
532
|
+
?.count,
|
|
533
|
+
).toBe(0);
|
|
534
|
+
});
|
|
535
|
+
|
|
504
536
|
test("extension tool calls derive their origin and skip the creating extension", async () => {
|
|
505
537
|
const creator = await installAndEnable("rewrite-task-priority");
|
|
506
538
|
await installAndEnable("block-tasks-from-source", {
|