@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
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
chmodSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
mkdtempSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import { createServer, type Server } from "node:http";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { normalizeSessionLogs } from "../../apps/ui/src/logs-parser";
|
|
15
|
+
import { createProviderAdapter } from "../providers";
|
|
16
|
+
import { ACPAdapter } from "../providers/acp-adapter";
|
|
17
|
+
import {
|
|
18
|
+
GROK_ISOLATION_ENV,
|
|
19
|
+
grokPromptCost,
|
|
20
|
+
grokReportedModel,
|
|
21
|
+
grokTargetProfile,
|
|
22
|
+
resolveAcpTarget,
|
|
23
|
+
} from "../providers/acp-targets";
|
|
24
|
+
import {
|
|
25
|
+
buildGrokConfigToml,
|
|
26
|
+
checkGrokCredentials,
|
|
27
|
+
GrokAdapter,
|
|
28
|
+
installedClaudePluginNames,
|
|
29
|
+
} from "../providers/grok-adapter";
|
|
30
|
+
import type { ProviderEvent, ProviderSessionConfig } from "../providers/types";
|
|
31
|
+
import { getModelAwareCredentialVars } from "../utils/credentials";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Recorded from a live `grok agent stdio` session (Grok CLI 1.0.46,
|
|
35
|
+
* grok-build-0.1) running a swarm task that calls get-swarm and
|
|
36
|
+
* store-progress. Long strings are trimmed; `available_commands_update` is
|
|
37
|
+
* dropped. The prompt response carries Grok's usage in `_meta`.
|
|
38
|
+
*/
|
|
39
|
+
const FIXTURE = join(import.meta.dir, "fixtures", "grok", "acp-updates-recorded.jsonl");
|
|
40
|
+
const PROMPT_META = join(import.meta.dir, "fixtures", "grok", "prompt-response-meta.json");
|
|
41
|
+
const SDK_PATH = join(process.cwd(), "node_modules/@agentclientprotocol/sdk/dist/acp.js");
|
|
42
|
+
|
|
43
|
+
const tmpDirs: string[] = [];
|
|
44
|
+
|
|
45
|
+
function makeTempDir(): string {
|
|
46
|
+
const dir = mkdtempSync(join(tmpdir(), "agent-swarm-grok-"));
|
|
47
|
+
tmpDirs.push(dir);
|
|
48
|
+
return dir;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
for (const dir of tmpDirs.splice(0)) rmSync(dir, { recursive: true, force: true });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
function baseConfig(overrides: Partial<ProviderSessionConfig> = {}): ProviderSessionConfig {
|
|
56
|
+
return {
|
|
57
|
+
prompt: "how many agents?",
|
|
58
|
+
systemPrompt: "You are a swarm worker.",
|
|
59
|
+
model: "grok-4.6",
|
|
60
|
+
role: "worker",
|
|
61
|
+
agentId: "agent-1",
|
|
62
|
+
taskId: "task-1",
|
|
63
|
+
apiUrl: "http://swarm.example",
|
|
64
|
+
apiKey: "api-key",
|
|
65
|
+
cwd: makeTempDir(),
|
|
66
|
+
logFile: "/tmp/grok.log",
|
|
67
|
+
env: { PATH: process.env.PATH ?? "", HOME: makeTempDir() },
|
|
68
|
+
...overrides,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function startTokenStub(): Promise<{ server: Server; apiUrl: string }> {
|
|
73
|
+
const server = createServer((req, res) => {
|
|
74
|
+
if (req.method === "POST" && req.url === "/api/sessions/tokens") {
|
|
75
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
76
|
+
res.end(JSON.stringify({ tokenId: "tok-1", plaintext: "aseph_grokstubtoken1234567890" }));
|
|
77
|
+
} else if (req.method === "DELETE") {
|
|
78
|
+
res.writeHead(204);
|
|
79
|
+
res.end();
|
|
80
|
+
} else {
|
|
81
|
+
res.writeHead(404);
|
|
82
|
+
res.end();
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
86
|
+
const { port } = server.address() as import("node:net").AddressInfo;
|
|
87
|
+
return { server, apiUrl: `http://127.0.0.1:${port}` };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* A fake `grok` executable: a shell wrapper around a bun ACP agent. It records
|
|
92
|
+
* its argv, the isolation env, the files in GROK_HOME and the `session/new`
|
|
93
|
+
* params into `capture.json` in its cwd, then replays the fixture.
|
|
94
|
+
*/
|
|
95
|
+
/**
|
|
96
|
+
* The recorded prompt `_meta` plus a side-model call: a second `modelUsage`
|
|
97
|
+
* entry (grok-4.3) folded into the cumulative totals, the shape Grok sends
|
|
98
|
+
* when a fallback or side model runs inside one prompt.
|
|
99
|
+
*/
|
|
100
|
+
function promptMetaWithSideModel(dir: string): string {
|
|
101
|
+
const meta = JSON.parse(readFileSync(PROMPT_META, "utf8"));
|
|
102
|
+
const side = {
|
|
103
|
+
inputTokens: 2000,
|
|
104
|
+
outputTokens: 40,
|
|
105
|
+
totalTokens: 2050,
|
|
106
|
+
cachedReadTokens: 500,
|
|
107
|
+
cacheCreationTokens: 0,
|
|
108
|
+
reasoningTokens: 10,
|
|
109
|
+
modelCalls: 1,
|
|
110
|
+
costUsdTicks: 20000000,
|
|
111
|
+
};
|
|
112
|
+
for (const key of Object.keys(side) as (keyof typeof side)[]) meta.usage[key] += side[key];
|
|
113
|
+
meta.usage.modelUsage["grok-4.3"] = side;
|
|
114
|
+
const path = join(dir, "prompt-meta-two-models.json");
|
|
115
|
+
writeFileSync(path, JSON.stringify(meta));
|
|
116
|
+
return path;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function writeFakeGrok(
|
|
120
|
+
dir: string,
|
|
121
|
+
mode: "ok" | "auth-required" | "wait-for-cancel",
|
|
122
|
+
promptMeta = PROMPT_META,
|
|
123
|
+
): string {
|
|
124
|
+
const agentPath = join(dir, "fake-grok-agent.ts");
|
|
125
|
+
writeFileSync(
|
|
126
|
+
agentPath,
|
|
127
|
+
`
|
|
128
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
129
|
+
import { join } from "node:path";
|
|
130
|
+
import { Readable, Writable } from "node:stream";
|
|
131
|
+
import { AgentSideConnection, PROTOCOL_VERSION, RequestError, ndJsonStream } from "${SDK_PATH}";
|
|
132
|
+
const home = process.env.GROK_HOME ?? "";
|
|
133
|
+
const read = (name) => (existsSync(join(home, name)) ? readFileSync(join(home, name), "utf8") : null);
|
|
134
|
+
const capture = {
|
|
135
|
+
argv: process.argv.slice(2),
|
|
136
|
+
env: Object.fromEntries(Object.entries(process.env).filter(([k]) => /^(GROK_|XAI_|OPENROUTER_|ANTHROPIC_|OPENAI_|CLAUDE_)/.test(k))),
|
|
137
|
+
configToml: read("config.toml"),
|
|
138
|
+
requirementsToml: read("requirements.toml"),
|
|
139
|
+
};
|
|
140
|
+
class FakeGrok {
|
|
141
|
+
constructor(connection) { this.connection = connection; }
|
|
142
|
+
async initialize() {
|
|
143
|
+
return { protocolVersion: PROTOCOL_VERSION, agentCapabilities: { loadSession: true }, authMethods: [{ id: "grok.com", name: "grok.com" }] };
|
|
144
|
+
}
|
|
145
|
+
async newSession(params) {
|
|
146
|
+
capture.newSession = { meta: params._meta ?? null, mcpServers: params.mcpServers.map((s) => s.name) };
|
|
147
|
+
writeFileSync("capture.json", JSON.stringify(capture));
|
|
148
|
+
if (${JSON.stringify(mode)} === "auth-required") throw RequestError.authRequired();
|
|
149
|
+
return { sessionId: "grok-session-1" };
|
|
150
|
+
}
|
|
151
|
+
async prompt(params) {
|
|
152
|
+
for (const line of readFileSync(${JSON.stringify(FIXTURE)}, "utf8").trim().split("\\n")) {
|
|
153
|
+
await this.connection.sessionUpdate({ sessionId: params.sessionId, update: JSON.parse(line) });
|
|
154
|
+
}
|
|
155
|
+
if (${JSON.stringify(mode)} === "wait-for-cancel") await new Promise((resolve) => { this.cancelled = resolve; });
|
|
156
|
+
// Grok sends no ACP \`usage\`; its usage rides in \`_meta\`.
|
|
157
|
+
const stopReason = ${JSON.stringify(mode)} === "wait-for-cancel" ? "cancelled" : "end_turn";
|
|
158
|
+
return { stopReason, _meta: JSON.parse(readFileSync(${JSON.stringify(promptMeta)}, "utf8")) };
|
|
159
|
+
}
|
|
160
|
+
async cancel() { this.cancelled?.(); }
|
|
161
|
+
}
|
|
162
|
+
new AgentSideConnection((c) => new FakeGrok(c), ndJsonStream(Writable.toWeb(process.stdout), Readable.toWeb(process.stdin)));
|
|
163
|
+
`,
|
|
164
|
+
);
|
|
165
|
+
const binPath = join(dir, "grok");
|
|
166
|
+
writeFileSync(binPath, `#!/bin/sh\nexec bun ${JSON.stringify(agentPath)} "$@"\n`);
|
|
167
|
+
chmodSync(binPath, 0o755);
|
|
168
|
+
return binPath;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
describe("GrokAdapter", () => {
|
|
172
|
+
test("is the grok provider on the ACP client, with no steering", async () => {
|
|
173
|
+
const adapter = await createProviderAdapter("grok");
|
|
174
|
+
expect(adapter).toBeInstanceOf(GrokAdapter);
|
|
175
|
+
expect(adapter.name).toBe("grok");
|
|
176
|
+
expect(adapter.traits.hasMcp).toBe(true);
|
|
177
|
+
expect(adapter.traits.steerModes).toEqual([]);
|
|
178
|
+
expect(await adapter.canResume("any")).toBe(false);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("credential readiness needs XAI_API_KEY or OPENROUTER_API_KEY", () => {
|
|
182
|
+
expect(checkGrokCredentials({}).ready).toBe(false);
|
|
183
|
+
expect(checkGrokCredentials({ XAI_API_KEY: " " }).missing).toEqual([
|
|
184
|
+
"XAI_API_KEY",
|
|
185
|
+
"OPENROUTER_API_KEY",
|
|
186
|
+
]);
|
|
187
|
+
expect(checkGrokCredentials({ XAI_API_KEY: "xai-k" })).toMatchObject({
|
|
188
|
+
ready: true,
|
|
189
|
+
satisfiedBy: "env",
|
|
190
|
+
});
|
|
191
|
+
expect(checkGrokCredentials({ OPENROUTER_API_KEY: "or-k" }).ready).toBe(true);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("prompt cost reads Grok's _meta.usage; a BYOK model reports no USD", () => {
|
|
195
|
+
expect(grokPromptCost(undefined, "grok-4.6")).toBeUndefined();
|
|
196
|
+
expect(grokPromptCost({ totalTokens: 10 }, "grok-4.6")).toBeUndefined();
|
|
197
|
+
const byok = grokPromptCost(
|
|
198
|
+
{ usage: { inputTokens: 1000, cachedReadTokens: 0, outputTokens: 5, modelCalls: 2 } },
|
|
199
|
+
"openrouter/acme/thing",
|
|
200
|
+
);
|
|
201
|
+
expect(byok).toEqual({
|
|
202
|
+
model: "openrouter/acme/thing",
|
|
203
|
+
inputTokens: 1000,
|
|
204
|
+
cacheReadTokens: 0,
|
|
205
|
+
cacheWriteTokens: 0,
|
|
206
|
+
outputTokens: 5,
|
|
207
|
+
reasoningOutputTokens: 0,
|
|
208
|
+
numTurns: 2,
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("reported models keep the session's route namespace", () => {
|
|
213
|
+
expect(grokReportedModel("grok-build-0.1", "grok-4.6")).toBe("grok-build-0.1");
|
|
214
|
+
expect(grokReportedModel("xai/grok-4.3", "grok-4.6")).toBe("grok-4.3");
|
|
215
|
+
expect(grokReportedModel("deepseek/deepseek-v4.1-flash", "openrouter/acme/thing")).toBe(
|
|
216
|
+
"openrouter/deepseek/deepseek-v4.1-flash",
|
|
217
|
+
);
|
|
218
|
+
expect(
|
|
219
|
+
grokReportedModel("openrouter/deepseek/deepseek-v4.1-flash", "openrouter/acme/thing"),
|
|
220
|
+
).toBe("openrouter/deepseek/deepseek-v4.1-flash");
|
|
221
|
+
// Per-model rows carry no reasoning field; the final model is the one that ran.
|
|
222
|
+
expect(
|
|
223
|
+
grokPromptCost(
|
|
224
|
+
{
|
|
225
|
+
modelId: "deepseek/deepseek-v4.1-flash",
|
|
226
|
+
usage: {
|
|
227
|
+
inputTokens: 100,
|
|
228
|
+
cachedReadTokens: 40,
|
|
229
|
+
outputTokens: 5,
|
|
230
|
+
reasoningTokens: 3,
|
|
231
|
+
modelUsage: {
|
|
232
|
+
"deepseek/deepseek-v4.1-flash": {
|
|
233
|
+
inputTokens: 100,
|
|
234
|
+
cachedReadTokens: 40,
|
|
235
|
+
outputTokens: 5,
|
|
236
|
+
reasoningTokens: 3,
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
"openrouter/acme/thing",
|
|
242
|
+
),
|
|
243
|
+
).toMatchObject({
|
|
244
|
+
model: "openrouter/deepseek/deepseek-v4.1-flash",
|
|
245
|
+
models: [
|
|
246
|
+
{
|
|
247
|
+
model: "openrouter/deepseek/deepseek-v4.1-flash",
|
|
248
|
+
inputTokens: 60,
|
|
249
|
+
cacheReadTokens: 40,
|
|
250
|
+
cacheWriteTokens: 0,
|
|
251
|
+
outputTokens: 8,
|
|
252
|
+
},
|
|
253
|
+
],
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("the credential pool picks the key the model's route bills", () => {
|
|
258
|
+
expect(getModelAwareCredentialVars("grok", "grok-4.3")).toEqual(["XAI_API_KEY"]);
|
|
259
|
+
expect(getModelAwareCredentialVars("grok", "openrouter/acme/thing")).toEqual([
|
|
260
|
+
"OPENROUTER_API_KEY",
|
|
261
|
+
]);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("refuses to start without XAI_API_KEY", async () => {
|
|
265
|
+
const saved = process.env.XAI_API_KEY;
|
|
266
|
+
delete process.env.XAI_API_KEY;
|
|
267
|
+
try {
|
|
268
|
+
await expect(new GrokAdapter().createSession(baseConfig())).rejects.toThrow(
|
|
269
|
+
"grok requires XAI_API_KEY",
|
|
270
|
+
);
|
|
271
|
+
} finally {
|
|
272
|
+
if (saved !== undefined) process.env.XAI_API_KEY = saved;
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("runs a session as provider grok in an isolated GROK_HOME", async () => {
|
|
277
|
+
const dir = makeTempDir();
|
|
278
|
+
const grok = writeFakeGrok(dir, "ok", promptMetaWithSideModel(dir));
|
|
279
|
+
const { server, apiUrl } = await startTokenStub();
|
|
280
|
+
try {
|
|
281
|
+
const config = baseConfig({
|
|
282
|
+
apiUrl,
|
|
283
|
+
reasoningEffort: "high",
|
|
284
|
+
env: {
|
|
285
|
+
PATH: process.env.PATH ?? "",
|
|
286
|
+
HOME: makeTempDir(),
|
|
287
|
+
GROK_BINARY: grok,
|
|
288
|
+
XAI_API_KEY: "xai-test-key",
|
|
289
|
+
ANTHROPIC_API_KEY: "must-not-leak",
|
|
290
|
+
CLAUDE_CODE_OAUTH_TOKEN: "must-not-leak",
|
|
291
|
+
OPENAI_API_KEY: "must-not-leak",
|
|
292
|
+
OPENROUTER_API_KEY: "must-not-leak",
|
|
293
|
+
GROK_MODELS_BASE_URL: "https://gateway.example/v1",
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
const session = await new GrokAdapter().createSession(config);
|
|
297
|
+
const events: ProviderEvent[] = [];
|
|
298
|
+
session.onEvent((event) => events.push(event));
|
|
299
|
+
const result = await session.waitForCompletion();
|
|
300
|
+
|
|
301
|
+
expect(result.isError).toBe(false);
|
|
302
|
+
expect(result.output).toBe("1: grok-e2e-worker");
|
|
303
|
+
// `_meta.usage`: the recorded grok-build-0.1 usage (134378 input incl.
|
|
304
|
+
// 109504 cached, 144 output + 1827 reasoning, 507168000 ticks of 1e-10
|
|
305
|
+
// USD over 6 calls) plus one grok-4.3 side call. The task asked for
|
|
306
|
+
// grok-4.6; `_meta.modelId` says grok-build-0.1 ran, and the cost row
|
|
307
|
+
// says so too.
|
|
308
|
+
expect(result.cost).toMatchObject({
|
|
309
|
+
provider: "grok",
|
|
310
|
+
model: "grok-build-0.1",
|
|
311
|
+
inputTokens: 26374,
|
|
312
|
+
cacheReadTokens: 110004,
|
|
313
|
+
cacheWriteTokens: 0,
|
|
314
|
+
outputTokens: 2021,
|
|
315
|
+
reasoningOutputTokens: 1837,
|
|
316
|
+
totalCostUsd: 0.0527168,
|
|
317
|
+
numTurns: 6,
|
|
318
|
+
});
|
|
319
|
+
expect(result.cost?.models).toEqual([
|
|
320
|
+
{
|
|
321
|
+
model: "grok-build-0.1",
|
|
322
|
+
inputTokens: 24874,
|
|
323
|
+
cacheReadTokens: 109504,
|
|
324
|
+
cacheWriteTokens: 0,
|
|
325
|
+
outputTokens: 1971,
|
|
326
|
+
harnessCostUsd: 0.0507168,
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
model: "grok-4.3",
|
|
330
|
+
inputTokens: 1500,
|
|
331
|
+
cacheReadTokens: 500,
|
|
332
|
+
cacheWriteTokens: 0,
|
|
333
|
+
outputTokens: 50,
|
|
334
|
+
harnessCostUsd: 0.002,
|
|
335
|
+
},
|
|
336
|
+
]);
|
|
337
|
+
|
|
338
|
+
const init = events.find((e) => e.type === "session_init");
|
|
339
|
+
expect(init).toMatchObject({ provider: "grok", sessionId: "grok-session-1" });
|
|
340
|
+
const toolStarts = events.filter((e) => e.type === "tool_start");
|
|
341
|
+
// Swarm MCP calls arrive through Grok's `use_tool` proxy and are unwrapped.
|
|
342
|
+
expect(toolStarts.map((e) => e.type === "tool_start" && e.toolName)).toEqual([
|
|
343
|
+
"search_tool",
|
|
344
|
+
"search_tool",
|
|
345
|
+
"mcp__swarm__get-swarm",
|
|
346
|
+
"search_tool",
|
|
347
|
+
"mcp__swarm__store-progress",
|
|
348
|
+
]);
|
|
349
|
+
expect(toolStarts[4]).toMatchObject({
|
|
350
|
+
args: { taskId: "task-1", status: "completed", output: "1: grok-worker" },
|
|
351
|
+
});
|
|
352
|
+
expect(events.filter((e) => e.type === "tool_end")).toHaveLength(5);
|
|
353
|
+
// No `usage_update`: the last model call's `_meta.totalTokens` is the snapshot.
|
|
354
|
+
// The window is grok-build-0.1's (256k), not the requested grok-4.6's (500k).
|
|
355
|
+
expect(events.find((e) => e.type === "context_usage")).toMatchObject({
|
|
356
|
+
contextUsedTokens: 25028,
|
|
357
|
+
contextTotalTokens: 256000,
|
|
358
|
+
contextFormula: "harness-reported",
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// The dashboard reads grok session logs with the ACP normalizer.
|
|
362
|
+
const rows = events
|
|
363
|
+
.filter((e): e is Extract<ProviderEvent, { type: "raw_log" }> => e.type === "raw_log")
|
|
364
|
+
.map((e, i) => ({
|
|
365
|
+
id: `00000000-0000-4000-8000-${String(i).padStart(12, "0")}`,
|
|
366
|
+
taskId: "task-1",
|
|
367
|
+
sessionId: "grok-session-1",
|
|
368
|
+
iteration: 1,
|
|
369
|
+
cli: "grok",
|
|
370
|
+
content: e.content,
|
|
371
|
+
lineNumber: i,
|
|
372
|
+
createdAt: new Date(1_800_000_000_000 + i).toISOString(),
|
|
373
|
+
}));
|
|
374
|
+
const transcript = normalizeSessionLogs(rows);
|
|
375
|
+
expect(transcript.gate.passed).toBe(true);
|
|
376
|
+
expect(transcript.pairing.paired).toBe(5);
|
|
377
|
+
// Grok's `variant`-tagged update input does not leak into the call input,
|
|
378
|
+
// and its status-less updates do not render as progress rows.
|
|
379
|
+
const getSwarm = transcript.items.find(
|
|
380
|
+
(item) => item.kind === "tool_call" && item.tool?.name === "mcp__swarm__get-swarm",
|
|
381
|
+
);
|
|
382
|
+
expect(getSwarm?.tool?.input).toEqual({});
|
|
383
|
+
expect(
|
|
384
|
+
transcript.items.filter(
|
|
385
|
+
(item) =>
|
|
386
|
+
item.kind === "lifecycle" && (item.meta as { type?: string })?.type === "progress",
|
|
387
|
+
),
|
|
388
|
+
).toEqual([]);
|
|
389
|
+
expect(transcript.items.some((item) => item.kind === "unknown")).toBe(false);
|
|
390
|
+
expect(
|
|
391
|
+
transcript.items.filter((item) => item.kind === "text").map((item) => item.text),
|
|
392
|
+
).toEqual(["1: grok-e2e-worker"]);
|
|
393
|
+
|
|
394
|
+
const capture = JSON.parse(await Bun.file(join(config.cwd, "capture.json")).text());
|
|
395
|
+
expect(capture.argv).toEqual([
|
|
396
|
+
"agent",
|
|
397
|
+
"--no-leader",
|
|
398
|
+
"--always-approve",
|
|
399
|
+
"--model",
|
|
400
|
+
"grok-4.6",
|
|
401
|
+
"--reasoning-effort",
|
|
402
|
+
"high",
|
|
403
|
+
"stdio",
|
|
404
|
+
]);
|
|
405
|
+
expect(capture.newSession).toEqual({
|
|
406
|
+
meta: { rules: "You are a swarm worker.", yoloMode: true },
|
|
407
|
+
mcpServers: ["swarm"],
|
|
408
|
+
});
|
|
409
|
+
// Only the key and the isolation switches cross the env allowlist.
|
|
410
|
+
expect(capture.env).toEqual({
|
|
411
|
+
...GROK_ISOLATION_ENV,
|
|
412
|
+
XAI_API_KEY: "xai-test-key",
|
|
413
|
+
GROK_HOME: expect.stringContaining("swarm-grok-"),
|
|
414
|
+
GROK_MODELS_BASE_URL: "https://gateway.example/v1",
|
|
415
|
+
});
|
|
416
|
+
expect(capture.configToml).toContain("[compat.codex]");
|
|
417
|
+
expect(capture.configToml).not.toContain("[model.");
|
|
418
|
+
expect(capture.requirementsToml).toBe("allow_managed_hooks_only = true\n");
|
|
419
|
+
// The per-session GROK_HOME is removed once the session settles.
|
|
420
|
+
await Bun.sleep(50);
|
|
421
|
+
expect(existsSync(capture.env.GROK_HOME)).toBe(false);
|
|
422
|
+
} finally {
|
|
423
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test("runs an openrouter/ model as an OpenAI-compatible model on OPENROUTER_API_KEY", async () => {
|
|
428
|
+
const dir = makeTempDir();
|
|
429
|
+
// A BYOK route reports the endpoint's id and no ticks.
|
|
430
|
+
const metaPath = join(dir, "prompt-meta-openrouter.json");
|
|
431
|
+
const usage = { inputTokens: 900, cachedReadTokens: 100, outputTokens: 20, reasoningTokens: 0 };
|
|
432
|
+
writeFileSync(
|
|
433
|
+
metaPath,
|
|
434
|
+
JSON.stringify({
|
|
435
|
+
totalTokens: 920,
|
|
436
|
+
modelId: "google/gemini-3-flash-preview",
|
|
437
|
+
usage: { ...usage, numTurns: 1, modelUsage: { "google/gemini-3-flash-preview": usage } },
|
|
438
|
+
}),
|
|
439
|
+
);
|
|
440
|
+
const grok = writeFakeGrok(dir, "ok", metaPath);
|
|
441
|
+
const { server, apiUrl } = await startTokenStub();
|
|
442
|
+
const saved = process.env.XAI_API_KEY;
|
|
443
|
+
delete process.env.XAI_API_KEY;
|
|
444
|
+
try {
|
|
445
|
+
const config = baseConfig({
|
|
446
|
+
apiUrl,
|
|
447
|
+
model: "openrouter/google/gemini-3-flash-preview",
|
|
448
|
+
env: {
|
|
449
|
+
PATH: process.env.PATH ?? "",
|
|
450
|
+
HOME: makeTempDir(),
|
|
451
|
+
GROK_BINARY: grok,
|
|
452
|
+
OPENROUTER_API_KEY: "or-test-key",
|
|
453
|
+
},
|
|
454
|
+
});
|
|
455
|
+
const session = await new GrokAdapter().createSession(config);
|
|
456
|
+
const result = await session.waitForCompletion();
|
|
457
|
+
expect(result.isError).toBe(false);
|
|
458
|
+
expect(result.cost).toMatchObject({
|
|
459
|
+
provider: "grok",
|
|
460
|
+
model: "openrouter/google/gemini-3-flash-preview",
|
|
461
|
+
inputTokens: 800,
|
|
462
|
+
totalCostUsd: 0,
|
|
463
|
+
});
|
|
464
|
+
// The reported endpoint id keeps the openrouter/ namespace pricing reads.
|
|
465
|
+
expect(result.cost?.models).toEqual([
|
|
466
|
+
{
|
|
467
|
+
model: "openrouter/google/gemini-3-flash-preview",
|
|
468
|
+
inputTokens: 800,
|
|
469
|
+
cacheReadTokens: 100,
|
|
470
|
+
cacheWriteTokens: 0,
|
|
471
|
+
outputTokens: 20,
|
|
472
|
+
},
|
|
473
|
+
]);
|
|
474
|
+
|
|
475
|
+
const capture = JSON.parse(await Bun.file(join(config.cwd, "capture.json")).text());
|
|
476
|
+
expect(capture.argv).toContain("openrouter/google/gemini-3-flash-preview");
|
|
477
|
+
// The OpenRouter route carries its own key and never XAI_API_KEY.
|
|
478
|
+
expect(capture.env).toEqual({
|
|
479
|
+
...GROK_ISOLATION_ENV,
|
|
480
|
+
OPENROUTER_API_KEY: "or-test-key",
|
|
481
|
+
GROK_HOME: expect.stringContaining("swarm-grok-"),
|
|
482
|
+
});
|
|
483
|
+
expect(capture.configToml).toContain(
|
|
484
|
+
[
|
|
485
|
+
'[model."openrouter/google/gemini-3-flash-preview"]',
|
|
486
|
+
'model = "google/gemini-3-flash-preview"',
|
|
487
|
+
'base_url = "https://openrouter.ai/api/v1"',
|
|
488
|
+
'env_key = "OPENROUTER_API_KEY"',
|
|
489
|
+
'api_backend = "chat_completions"',
|
|
490
|
+
].join("\n"),
|
|
491
|
+
);
|
|
492
|
+
} finally {
|
|
493
|
+
if (saved !== undefined) process.env.XAI_API_KEY = saved;
|
|
494
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
test("an xai/-qualified model runs as the bare xAI id", async () => {
|
|
499
|
+
const dir = makeTempDir();
|
|
500
|
+
const grok = writeFakeGrok(dir, "ok");
|
|
501
|
+
const { server, apiUrl } = await startTokenStub();
|
|
502
|
+
try {
|
|
503
|
+
const config = baseConfig({
|
|
504
|
+
apiUrl,
|
|
505
|
+
model: "xai/grok-4.6",
|
|
506
|
+
env: {
|
|
507
|
+
PATH: process.env.PATH ?? "",
|
|
508
|
+
HOME: makeTempDir(),
|
|
509
|
+
GROK_BINARY: grok,
|
|
510
|
+
XAI_API_KEY: "k",
|
|
511
|
+
},
|
|
512
|
+
});
|
|
513
|
+
const result = await (await new GrokAdapter().createSession(config)).waitForCompletion();
|
|
514
|
+
expect(result.isError).toBe(false);
|
|
515
|
+
const capture = JSON.parse(await Bun.file(join(config.cwd, "capture.json")).text());
|
|
516
|
+
expect(capture.argv).toEqual([
|
|
517
|
+
"agent",
|
|
518
|
+
"--no-leader",
|
|
519
|
+
"--always-approve",
|
|
520
|
+
"--model",
|
|
521
|
+
"grok-4.6",
|
|
522
|
+
"stdio",
|
|
523
|
+
]);
|
|
524
|
+
} finally {
|
|
525
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
test("an abort keeps the usage of the cancelled prompt", async () => {
|
|
530
|
+
const dir = makeTempDir();
|
|
531
|
+
const grok = writeFakeGrok(dir, "wait-for-cancel");
|
|
532
|
+
const { server, apiUrl } = await startTokenStub();
|
|
533
|
+
try {
|
|
534
|
+
const config = baseConfig({
|
|
535
|
+
apiUrl,
|
|
536
|
+
env: {
|
|
537
|
+
PATH: process.env.PATH ?? "",
|
|
538
|
+
HOME: makeTempDir(),
|
|
539
|
+
GROK_BINARY: grok,
|
|
540
|
+
XAI_API_KEY: "k",
|
|
541
|
+
},
|
|
542
|
+
});
|
|
543
|
+
const session = await new GrokAdapter().createSession(config);
|
|
544
|
+
const events: ProviderEvent[] = [];
|
|
545
|
+
session.onEvent((event) => events.push(event));
|
|
546
|
+
// The server marks the task done (store-progress) while the turn is open.
|
|
547
|
+
while (events.filter((e) => e.type === "tool_end").length < 5) await Bun.sleep(10);
|
|
548
|
+
await session.abort();
|
|
549
|
+
const result = await session.waitForCompletion();
|
|
550
|
+
expect(result.isError).toBe(true);
|
|
551
|
+
expect(result.cost).toMatchObject({ totalCostUsd: 0.0507168, outputTokens: 1971 });
|
|
552
|
+
} finally {
|
|
553
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
test("refuses an openrouter/ model without OPENROUTER_API_KEY", async () => {
|
|
558
|
+
const saved = process.env.OPENROUTER_API_KEY;
|
|
559
|
+
delete process.env.OPENROUTER_API_KEY;
|
|
560
|
+
try {
|
|
561
|
+
await expect(
|
|
562
|
+
new GrokAdapter().createSession(
|
|
563
|
+
baseConfig({
|
|
564
|
+
model: "openrouter/acme/thing",
|
|
565
|
+
env: { PATH: process.env.PATH ?? "", XAI_API_KEY: "xai-k" },
|
|
566
|
+
}),
|
|
567
|
+
),
|
|
568
|
+
).rejects.toThrow("grok requires OPENROUTER_API_KEY for openrouter/acme/thing");
|
|
569
|
+
} finally {
|
|
570
|
+
if (saved !== undefined) process.env.OPENROUTER_API_KEY = saved;
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
test("maps Grok's auth error to a credential message", async () => {
|
|
575
|
+
const dir = makeTempDir();
|
|
576
|
+
const grok = writeFakeGrok(dir, "auth-required");
|
|
577
|
+
const { server, apiUrl } = await startTokenStub();
|
|
578
|
+
try {
|
|
579
|
+
await expect(
|
|
580
|
+
new GrokAdapter().createSession(
|
|
581
|
+
baseConfig({
|
|
582
|
+
apiUrl,
|
|
583
|
+
env: {
|
|
584
|
+
PATH: process.env.PATH ?? "",
|
|
585
|
+
HOME: makeTempDir(),
|
|
586
|
+
GROK_BINARY: grok,
|
|
587
|
+
XAI_API_KEY: "xai-bad-key",
|
|
588
|
+
},
|
|
589
|
+
}),
|
|
590
|
+
),
|
|
591
|
+
).rejects.toThrow(
|
|
592
|
+
"Grok rejected the credentials (XAI_API_KEY or OPENROUTER_API_KEY invalid or missing): Authentication required",
|
|
593
|
+
);
|
|
594
|
+
} finally {
|
|
595
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
describe("grok target profile", () => {
|
|
601
|
+
test("is not an operator ACP target", () => {
|
|
602
|
+
expect(() => resolveAcpTarget(baseConfig({ env: { ACP_TARGET: "grok" } }))).toThrow(
|
|
603
|
+
'Unsupported ACP target "grok"',
|
|
604
|
+
);
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
test("omits flags it has no value for", () => {
|
|
608
|
+
const config = baseConfig({ model: "", systemPrompt: "" });
|
|
609
|
+
expect(grokTargetProfile.command(config)).toEqual([
|
|
610
|
+
"grok",
|
|
611
|
+
"agent",
|
|
612
|
+
"--no-leader",
|
|
613
|
+
"--always-approve",
|
|
614
|
+
"stdio",
|
|
615
|
+
]);
|
|
616
|
+
expect(grokTargetProfile.sessionMeta?.(config)).toEqual({ yoloMode: true });
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
test("leaves other errors alone", () => {
|
|
620
|
+
expect(grokTargetProfile.describeError?.("rate limited")).toBeUndefined();
|
|
621
|
+
expect(grokTargetProfile.describeError?.("Not signed in. Run grok login")).toContain(
|
|
622
|
+
"OPENROUTER_API_KEY invalid or missing",
|
|
623
|
+
);
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
test("existing ACP targets still send no session _meta", () => {
|
|
627
|
+
const opencode = resolveAcpTarget(baseConfig({ env: { ACP_TARGET: "opencode" } }));
|
|
628
|
+
expect(opencode.sessionMeta).toBeUndefined();
|
|
629
|
+
expect(new ACPAdapter().name).toBe("acp");
|
|
630
|
+
});
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
describe("grok config", () => {
|
|
634
|
+
test("disables the worker's Claude plugins by name", async () => {
|
|
635
|
+
const home = makeTempDir();
|
|
636
|
+
mkdirSync(join(home, ".claude", "plugins"), { recursive: true });
|
|
637
|
+
writeFileSync(
|
|
638
|
+
join(home, ".claude", "plugins", "installed_plugins.json"),
|
|
639
|
+
JSON.stringify({ version: 2, plugins: { "context-mode@context-mode": [], "x@y": [] } }),
|
|
640
|
+
);
|
|
641
|
+
const names = await installedClaudePluginNames(home);
|
|
642
|
+
expect(names).toEqual(["context-mode", "x"]);
|
|
643
|
+
expect(buildGrokConfigToml(names)).toContain('disabled = ["context-mode", "x"]');
|
|
644
|
+
expect(await installedClaudePluginNames(makeTempDir())).toEqual([]);
|
|
645
|
+
expect(buildGrokConfigToml([])).toContain("disabled = []");
|
|
646
|
+
});
|
|
647
|
+
});
|