diff --git a/.deva.example b/.deva.example index 9a95ead..ad2e787 100644 --- a/.deva.example +++ b/.deva.example @@ -61,10 +61,10 @@ EPHEMERAL=false # Hybrid Agent Setup: # # Hybrid is the DEFAULT. deva walks every populated subdir under -# ~/.config/deva/ (claude, codex, gemini) and mounts each agent's +# ~/.config/deva/ (claude, codex, gemini, grok) and mounts each agent's # canonical entries into the container. Populated = you either # hand-created the subdir or autolink symlinked it from legacy -# ~/.claude, ~/.codex, ~/.gemini on first run. +# ~/.claude, ~/.codex, ~/.gemini, ~/.grok on first run. # # No .deva entries required for the common case. To opt OUT of # hybrid for a single invocation, pass --config-home DIR to diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d8de931..8bcdaf1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,6 +79,7 @@ jobs: --build-arg CCTRACE_VERSION="${{ steps.pins.outputs.cctrace_version }}" \ --build-arg CODEX_VERSION="${{ steps.pins.outputs.codex_version }}" \ --build-arg GEMINI_CLI_VERSION="${{ steps.pins.outputs.gemini_cli_version }}" \ + --build-arg GROK_CLI_VERSION="${{ steps.pins.outputs.grok_cli_version }}" \ --build-arg CCX_VERSION="${{ steps.pins.outputs.ccx_version }}" \ --build-arg COPILOT_API_VERSION="${{ steps.pins.outputs.copilot_api_version }}" \ . @@ -100,6 +101,7 @@ jobs: CCTRACE_VERSION="${{ steps.pins.outputs.cctrace_version }}" \ CODEX_VERSION="${{ steps.pins.outputs.codex_version }}" \ GEMINI_CLI_VERSION="${{ steps.pins.outputs.gemini_cli_version }}" \ + GROK_CLI_VERSION="${{ steps.pins.outputs.grok_cli_version }}" \ CCX_VERSION="${{ steps.pins.outputs.ccx_version }}" \ COPILOT_API_VERSION="${{ steps.pins.outputs.copilot_api_version }}" \ PLAYWRIGHT_VERSION="${{ steps.pins.outputs.playwright_version }}" \ @@ -124,6 +126,7 @@ jobs: deva.sh claude -Q -- --version deva.sh codex -Q -- --version deva.sh gemini -Q -- --version + deva.sh grok -Q -- --version - name: Smoke Claude --chrome mount assembly shell: bash diff --git a/.github/workflows/nightly-images.yml b/.github/workflows/nightly-images.yml index 7d0a4ea..72343c6 100644 --- a/.github/workflows/nightly-images.yml +++ b/.github/workflows/nightly-images.yml @@ -54,6 +54,7 @@ jobs: claude_code_version: ${{ steps.versions.outputs.claude_code_version }} codex_version: ${{ steps.versions.outputs.codex_version }} gemini_cli_version: ${{ steps.versions.outputs.gemini_cli_version }} + grok_cli_version: ${{ steps.versions.outputs.grok_cli_version }} ccx_version: ${{ steps.versions.outputs.ccx_version }} copilot_api_version: ${{ steps.versions.outputs.copilot_api_version }} steps: @@ -80,6 +81,7 @@ jobs: - Claude Code: \`${{ steps.versions.outputs.claude_code_version }}\` - Codex: \`${{ steps.versions.outputs.codex_version }}\` - Gemini CLI: \`${{ steps.versions.outputs.gemini_cli_version }}\` + - Grok CLI: \`${{ steps.versions.outputs.grok_cli_version }}\` - ccx: \`${{ steps.versions.outputs.ccx_version }}\` - Copilot API: \`${{ steps.versions.outputs.copilot_api_version }}\` - Stamp: \`${{ steps.versions.outputs.stamp }}\` @@ -140,6 +142,7 @@ jobs: CCTRACE_VERSION=${{ needs.load-version-pins.outputs.cctrace_version }} CODEX_VERSION=${{ needs.resolve-versions.outputs.codex_version }} GEMINI_CLI_VERSION=${{ needs.resolve-versions.outputs.gemini_cli_version }} + GROK_CLI_VERSION=${{ needs.resolve-versions.outputs.grok_cli_version }} CCX_VERSION=${{ needs.resolve-versions.outputs.ccx_version }} COPILOT_API_VERSION=${{ needs.resolve-versions.outputs.copilot_api_version }} @@ -193,6 +196,7 @@ jobs: CCTRACE_VERSION=${{ needs.load-version-pins.outputs.cctrace_version }} CODEX_VERSION=${{ needs.resolve-versions.outputs.codex_version }} GEMINI_CLI_VERSION=${{ needs.resolve-versions.outputs.gemini_cli_version }} + GROK_CLI_VERSION=${{ needs.resolve-versions.outputs.grok_cli_version }} CCX_VERSION=${{ needs.resolve-versions.outputs.ccx_version }} PLAYWRIGHT_VERSION=${{ needs.load-version-pins.outputs.playwright_version }} RUST_TOOLCHAINS=${{ needs.load-version-pins.outputs.rust_toolchains }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8bcf5ef..2f989a3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -50,6 +50,7 @@ jobs: cctrace_version: ${{ steps.pins.outputs.cctrace_version }} codex_version: ${{ steps.pins.outputs.codex_version }} gemini_cli_version: ${{ steps.pins.outputs.gemini_cli_version }} + grok_cli_version: ${{ steps.pins.outputs.grok_cli_version }} ccx_version: ${{ steps.pins.outputs.ccx_version }} copilot_api_version: ${{ steps.pins.outputs.copilot_api_version }} playwright_version: ${{ steps.pins.outputs.playwright_version }} @@ -84,6 +85,7 @@ jobs: - cctrace: \`${{ steps.pins.outputs.cctrace_version }}\` - Codex: \`${{ steps.pins.outputs.codex_version }}\` - Gemini CLI: \`${{ steps.pins.outputs.gemini_cli_version }}\` + - Grok CLI: \`${{ steps.pins.outputs.grok_cli_version }}\` - ccx: \`${{ steps.pins.outputs.ccx_version }}\` - Copilot API: \`${{ steps.pins.outputs.copilot_api_version }}\` - Playwright: \`${{ steps.pins.outputs.playwright_version }}\` @@ -147,6 +149,7 @@ jobs: CCTRACE_VERSION=${{ needs.load-version-pins.outputs.cctrace_version }} CODEX_VERSION=${{ needs.load-version-pins.outputs.codex_version }} GEMINI_CLI_VERSION=${{ needs.load-version-pins.outputs.gemini_cli_version }} + GROK_CLI_VERSION=${{ needs.load-version-pins.outputs.grok_cli_version }} CCX_VERSION=${{ needs.load-version-pins.outputs.ccx_version }} COPILOT_API_VERSION=${{ needs.load-version-pins.outputs.copilot_api_version }} @@ -202,6 +205,7 @@ jobs: CCTRACE_VERSION=${{ needs.load-version-pins.outputs.cctrace_version }} CODEX_VERSION=${{ needs.load-version-pins.outputs.codex_version }} GEMINI_CLI_VERSION=${{ needs.load-version-pins.outputs.gemini_cli_version }} + GROK_CLI_VERSION=${{ needs.load-version-pins.outputs.grok_cli_version }} CCX_VERSION=${{ needs.load-version-pins.outputs.ccx_version }} PLAYWRIGHT_VERSION=${{ needs.load-version-pins.outputs.playwright_version }} RUST_TOOLCHAINS=${{ needs.load-version-pins.outputs.rust_toolchains }} diff --git a/CHANGELOG.md b/CHANGELOG.md index c857193..6d2f27a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 uncommitted work via live ls-remote + merge ancestry, and emits a tiered dry-run plan; SKILL.md layers the judgment calls and gates execution behind explicit approval (#401) +- `grok` agent: xAI's official Grok CLI (npm `@xai-official/grok`) as the + fourth first-party agent (#403). `deva.sh grok` runs it YOLO-style + (`--always-approve`) in the standard container shape. Auth: `oauth` + (default, mounts `~/.grok`; in-container first login via + `grok login --device-auth`) or `api-key` (`XAI_API_KEY`). api-key mode + mounts no `~/.grok` at all: grok's credential priority (`model.api_key` > + `model.env_key` > session token > `XAI_API_KEY`) means anything a mounted + config carries could outrank — and bill instead of — the exported key. + Grok's npm launcher resolves `~/.grok/bin/grok` (its self-update dir) + before the packaged binary, so the image moves the real binary to + `~/.local/bin` and drops `~/.grok/bin` — a host-mounted `~/.grok`, + possibly holding a macOS binary, can never shadow the container CLI. + The reverse is guarded with container-local tmpfs over `~/.grok/bin` and + `~/.grok/downloads` whenever a host dir is mounted there: in-container + `grok update` cannot write Linux binaries into the host install. Pin: + `GROK_CLI_VERSION` in `versions.env`, label + `org.opencontainers.image.grok_cli_version`, wired through make/CI/ + nightly/release version plumbing ## [0.13.0] - 2026-07-07 diff --git a/DEV-LOGS.md b/DEV-LOGS.md index 486fe01..ad03684 100644 --- a/DEV-LOGS.md +++ b/DEV-LOGS.md @@ -13,6 +13,17 @@ - Minimal markdown markers, no unnecessary formatting, minimal emojis. - Reference issue numbers in the format `#` for easy linking. +# [2026-07-09] Dev Log: add grok agent (official xAI Grok CLI) #403 +- Why: xAI shipped an official coding-agent CLI (x.ai/cli, npm @xai-official/grok); deva should launch it like claude/codex/gemini. Not to be confused with community superagent-ai/grok-cli (npm grok-dev) — different product, same bin name. +- What: + - agents/grok.sh: oauth default (mount ~/.grok) + api-key (XAI_API_KEY); appends --always-approve (grok's bypassPermissions alias) since the container is the sandbox + - deva.sh: canonical entry .grok, autolink ~/.grok, XAI_API_KEY env handling. api-key mode mounts NO ~/.grok (grok credential priority: model.api_key > model.env_key > session token > XAI_API_KEY — anything a mounted config carries can outrank the exported key; per #403 spec and Codex review). Blank auth.json overlay kept as defense for explicit -v mounts + - install-agent-tooling.sh pin_grok_platform_binary: grok's npm bin is a trampoline that resolves ~/.grok/bin/grok FIRST, and postinstall parks the ~125MB real binary there (self-update dir); a host mount would shadow it (macOS binary -> exec format error). Move the binary to ~/.local/bin, repoint the npm bin, rm ~/.grok/bin + - deva.sh append_grok_update_guard: the reverse hole, found in review and reproduced against the real 0.2.93 — a mounted config.toml without the postinstall's `[cli] installer = "npm"` marker flips the updater to installer=internal, and `grok update` re-creates ~/.grok/bin + ~/.grok/downloads ON THE HOST MOUNT (Linux binary shadows a macOS host CLI via the host trampoline). Fix: tmpfs over both dirs whenever a host dir lands at /home/deva/.grok; updater writes die with the container + - docker-entrypoint.sh: grok version header, verbose diagnostics, ensure_agent_binaries + - GROK_CLI_VERSION pin wired through versions.env, Makefile, Dockerfile(+rust), version-pins/upgrade/report/resolve scripts, ci/nightly/release workflows; tests extended (release-utils registry, version-upgrade mock, install-tooling, version-targets, mount-shape grok + update-guard + api-key no-mount assertions) +- Result: deva.sh grok works with both auth modes (dry-run + mount-shape verified); grok pinned at 0.2.93; make versions-up picks up new upstream releases; host ~/.grok cannot be poisoned by in-container updates (tmpfs guard verified live: uid/gid/mode/size + noexec) + # [2026-07-07] Dev Log: switch --trace default from claude-trace to cctrace - Why: claude-trace's node --require fetch hook only sees /v1/messages and dies on native Claude binaries; cctrace (thevibeworks/cctrace) MITM-captures everything (OAuth, usage/credits, MCP) and auto-detects npm vs native installs - What: diff --git a/Dockerfile b/Dockerfile index df39049..662fabd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -211,12 +211,14 @@ ARG CLAUDE_CODE_VERSION=2.1.143 ARG CCTRACE_VERSION=0.4.0 ARG CODEX_VERSION=0.131.0 ARG GEMINI_CLI_VERSION=0.42.0 +ARG GROK_CLI_VERSION=0.2.93 # Record key tool versions as labels for quick inspection LABEL org.opencontainers.image.claude_code_version=${CLAUDE_CODE_VERSION} LABEL org.opencontainers.image.cctrace_version=${CCTRACE_VERSION} LABEL org.opencontainers.image.codex_version=${CODEX_VERSION} LABEL org.opencontainers.image.gemini_cli_version=${GEMINI_CLI_VERSION} +LABEL org.opencontainers.image.grok_cli_version=${GROK_CLI_VERSION} ARG CCX_VERSION=v0.7.0 diff --git a/Dockerfile.rust b/Dockerfile.rust index 829d292..3d7bba6 100644 --- a/Dockerfile.rust +++ b/Dockerfile.rust @@ -12,6 +12,7 @@ ARG CLAUDE_CODE_VERSION=2.1.143 ARG CCTRACE_VERSION=0.4.0 ARG CODEX_VERSION=0.131.0 ARG GEMINI_CLI_VERSION=0.42.0 +ARG GROK_CLI_VERSION=0.2.93 ARG CCX_VERSION=v0.7.0 ARG PLAYWRIGHT_VERSION=1.60.0 ARG RUST_TOOLCHAINS="stable" @@ -22,6 +23,7 @@ LABEL org.opencontainers.image.claude_code_version=${CLAUDE_CODE_VERSION} LABEL org.opencontainers.image.cctrace_version=${CCTRACE_VERSION} LABEL org.opencontainers.image.codex_version=${CODEX_VERSION} LABEL org.opencontainers.image.gemini_cli_version=${GEMINI_CLI_VERSION} +LABEL org.opencontainers.image.grok_cli_version=${GROK_CLI_VERSION} LABEL org.opencontainers.image.ccx_version=${CCX_VERSION} LABEL org.opencontainers.image.playwright_version=${PLAYWRIGHT_VERSION} diff --git a/Makefile b/Makefile index f675596..44c1e7b 100644 --- a/Makefile +++ b/Makefile @@ -34,6 +34,7 @@ CLAUDE_CODE_VERSION ?= 2.1.143 CCTRACE_VERSION ?= 0.4.0 CODEX_VERSION ?= 0.131.0 GEMINI_CLI_VERSION ?= 0.42.0 +GROK_CLI_VERSION ?= 0.2.93 CCX_VERSION ?= v0.1.4 COPILOT_API_VERSION ?= 0ea08febdd7e3e055b03dd298bf57e669500b5c1 PLAYWRIGHT_VERSION ?= 1.60.0 @@ -59,6 +60,7 @@ AGENT_BUILD_ARGS := \ --build-arg CCTRACE_VERSION=$(CCTRACE_VERSION) \ --build-arg CODEX_VERSION=$(CODEX_VERSION) \ --build-arg GEMINI_CLI_VERSION=$(GEMINI_CLI_VERSION) \ + --build-arg GROK_CLI_VERSION=$(GROK_CLI_VERSION) \ --build-arg CCX_VERSION=$(CCX_VERSION) MAIN_BUILD_ARGS := $(TOOLCHAIN_BUILD_ARGS) $(AGENT_BUILD_ARGS) \ @@ -81,6 +83,7 @@ VERSION_QUERY_OVERRIDES := \ $(if $(filter command line environment environment\ override override,$(origin CCTRACE_VERSION)),CCTRACE_VERSION=$(CCTRACE_VERSION)) \ $(if $(filter command line environment environment\ override override,$(origin CODEX_VERSION)),CODEX_VERSION=$(CODEX_VERSION)) \ $(if $(filter command line environment environment\ override override,$(origin GEMINI_CLI_VERSION)),GEMINI_CLI_VERSION=$(GEMINI_CLI_VERSION)) \ + $(if $(filter command line environment environment\ override override,$(origin GROK_CLI_VERSION)),GROK_CLI_VERSION=$(GROK_CLI_VERSION)) \ $(if $(filter command line environment environment\ override override,$(origin CCX_VERSION)),CCX_VERSION=$(CCX_VERSION)) \ $(if $(filter command line environment environment\ override override,$(origin COPILOT_API_VERSION)),COPILOT_API_VERSION=$(COPILOT_API_VERSION)) \ $(if $(filter command line environment environment\ override override,$(origin PLAYWRIGHT_VERSION)),PLAYWRIGHT_VERSION=$(PLAYWRIGHT_VERSION)) \ @@ -123,13 +126,15 @@ build-main: build-network-check @prev_claude=$$(docker inspect --format='{{ index .Config.Labels "org.opencontainers.image.claude_code_version" }}' $(MAIN_IMAGE) 2>/dev/null || true); \ prev_codex=$$(docker inspect --format='{{ index .Config.Labels "org.opencontainers.image.codex_version" }}' $(MAIN_IMAGE) 2>/dev/null || true); \ prev_gemini=$$(docker inspect --format='{{ index .Config.Labels "org.opencontainers.image.gemini_cli_version" }}' $(MAIN_IMAGE) 2>/dev/null || true); \ + prev_grok=$$(docker inspect --format='{{ index .Config.Labels "org.opencontainers.image.grok_cli_version" }}' $(MAIN_IMAGE) 2>/dev/null || true); \ fmt() { v="$$1"; if [ -z "$$v" ] || [ "$$v" = "" ]; then echo "-"; else case "$$v" in v*) echo "$$v";; *) echo "v$$v";; esac; fi; }; \ - curC=$$(fmt "$$prev_claude"); curX=$$(fmt "$$prev_codex"); curG=$$(fmt "$$prev_gemini"); \ - tgtC=$$(fmt "$(CLAUDE_CODE_VERSION)"); tgtX=$$(fmt "$(CODEX_VERSION)"); tgtG=$$(fmt "$(GEMINI_CLI_VERSION)"); \ - if [ "$$curC" = "$$tgtC" ] && [ "$$curX" = "$$tgtX" ] && [ "$$curG" = "$$tgtG" ]; then \ + curC=$$(fmt "$$prev_claude"); curX=$$(fmt "$$prev_codex"); curG=$$(fmt "$$prev_gemini"); curK=$$(fmt "$$prev_grok"); \ + tgtC=$$(fmt "$(CLAUDE_CODE_VERSION)"); tgtX=$$(fmt "$(CODEX_VERSION)"); tgtG=$$(fmt "$(GEMINI_CLI_VERSION)"); tgtK=$$(fmt "$(GROK_CLI_VERSION)"); \ + if [ "$$curC" = "$$tgtC" ] && [ "$$curX" = "$$tgtX" ] && [ "$$curG" = "$$tgtG" ] && [ "$$curK" = "$$tgtK" ]; then \ echo "Claude: $$tgtC (no change)"; \ echo "Codex: $$tgtX (no change)"; \ echo "Gemini: $$tgtG (no change)"; \ + echo "Grok: $$tgtK (no change)"; \ echo "Already up-to-date"; \ else \ if [ "$$curC" = "$$tgtC" ]; then \ @@ -147,6 +152,11 @@ build-main: build-network-check else \ echo "Gemini: $$curG -> $$tgtG"; \ fi; \ + if [ "$$curK" = "$$tgtK" ]; then \ + echo "Grok: $$tgtK (no change)"; \ + else \ + echo "Grok: $$curK -> $$tgtK"; \ + fi; \ fi @echo "Hint: override via GO_VERSION=... CLAUDE_CODE_VERSION=... or run 'make versions-pin'" docker build $(DOCKER_BUILD_EXTRA_ARGS) -f $(DOCKERFILE) $(MAIN_BUILD_ARGS) -t $(MAIN_IMAGE) . @@ -403,6 +413,7 @@ help: @echo " CCTRACE_VERSION cctrace version (default: $(CCTRACE_VERSION))" @echo " CODEX_VERSION Codex CLI version (default: $(CODEX_VERSION))" @echo " GEMINI_CLI_VERSION Gemini CLI version (default: $(GEMINI_CLI_VERSION))" + @echo " GROK_CLI_VERSION Grok CLI version (default: $(GROK_CLI_VERSION))" @echo " CCX_VERSION Atlas CLI version (default: $(CCX_VERSION))" @echo " PLAYWRIGHT_VERSION Playwright version (default: $(PLAYWRIGHT_VERSION))" @echo " RUST_TOOLCHAINS Rust toolchains to install (default: $(RUST_TOOLCHAINS))" diff --git a/README.md b/README.md index 6b12f42..e2872d2 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@ [![Checks](https://img.shields.io/badge/checks-shellcheck%20%7C%20docs%20%7C%20smoke-222222)](#development) [![License](https://img.shields.io/github/license/thevibeworks/deva)](LICENSE) [![Container](https://img.shields.io/badge/ghcr.io-thevibeworks%2Fdeva-blue)](https://github.com/thevibeworks/deva/pkgs/container/deva) -[![Agents](https://img.shields.io/badge/agents-codex%20%7C%20claude%20%7C%20gemini-222222)](#what-this-is) +[![Agents](https://img.shields.io/badge/agents-codex%20%7C%20claude%20%7C%20gemini%20%7C%20grok-222222)](#what-this-is) -Run Codex, Claude Code, and Gemini inside Docker without pretending the agent's own sandbox is the thing keeping you safe. +Run Codex, Claude Code, Gemini, and Grok inside Docker without pretending the agent's own sandbox is the thing keeping you safe. The container is the sandbox. Explicit mounts are the contract. Persistent project containers keep the workflow fast instead of rebuilding the same state every run. @@ -16,7 +16,7 @@ This repo is the source of truth for `deva.sh`. ## What This Is -- a Docker-based launcher for Codex, Claude, and Gemini +- a Docker-based launcher for Codex, Claude, Gemini, and Grok - one warm default container shape per project by default - explicit mount and env wiring instead of mystery behavior - per-agent config homes under `~/.config/deva/` @@ -54,7 +54,7 @@ deva.sh ps deva.sh stop ``` -If you already use Codex, Claude, or Gemini locally, deva will auto-link those auth homes into `~/.config/deva/` by default. If not, first run will ask you to authenticate inside the container. +If you already use Codex, Claude, Gemini, or Grok locally, deva will auto-link those auth homes into `~/.config/deva/` by default. If not, first run will ask you to authenticate inside the container. ## Docs @@ -115,6 +115,7 @@ deva.sh # Same container, different agents deva.sh codex deva.sh gemini +deva.sh grok # Throwaway run deva.sh claude --rm diff --git a/agents/grok.sh b/agents/grok.sh new file mode 100644 index 0000000..2178c76 --- /dev/null +++ b/agents/grok.sh @@ -0,0 +1,65 @@ +# shellcheck shell=bash + +# shellcheck disable=SC1091 +if [ -f "$(dirname "${BASH_SOURCE[0]}")/shared_auth.sh" ]; then + source "$(dirname "${BASH_SOURCE[0]}")/shared_auth.sh" +fi + +agent_prepare() { + local -a args + if [ $# -gt 0 ]; then + args=("$@") + else + args=() + fi + AGENT_COMMAND=("grok") + + parse_auth_args "grok" "${args[@]+"${args[@]}"}" + AUTH_METHOD="$PARSED_AUTH_METHOD" + local -a remaining_args=("${PARSED_REMAINING_ARGS[@]+"${PARSED_REMAINING_ARGS[@]}"}") + + AGENT_COMMAND+=("--always-approve") + + AGENT_COMMAND+=("${remaining_args[@]+"${remaining_args[@]}"}") + + setup_grok_auth "$AUTH_METHOD" +} + +setup_grok_auth() { + local method="$1" + + case "$method" in + oauth) + AUTH_DETAILS="oauth (~/.grok)" + # First login inside a container has no browser: run + # `grok login --device-auth` in the session, or auth on the + # host once and let the mount carry ~/.grok/auth.json. + # Only mount host ~/.grok directly when no config-home mechanism is active. + # -Q bare mode: no mounts at all. Explicit/auto config-home: centralized mount handles it. + if [ "${QUICK_MODE:-false}" = false ] && [ "${CONFIG_HOME_FROM_CLI:-false}" = false ] && [ "${CONFIG_HOME_AUTO:-false}" = false ]; then + if [ -d "$HOME/.grok" ]; then + DOCKER_ARGS+=("-v" "$HOME/.grok:/home/deva/.grok") + else + echo "Warning: ~/.grok directory not found, creating it" >&2 + mkdir -p "$HOME/.grok" + DOCKER_ARGS+=("-v" "$HOME/.grok:/home/deva/.grok") + fi + fi + ;; + api-key) + if [ -z "${XAI_API_KEY:-}" ]; then + auth_error "XAI_API_KEY not set for --auth-with api-key" \ + "Set: export XAI_API_KEY=your_key (from https://console.x.ai)" + fi + + # No ~/.grok mount in this mode (grok_api_key_no_mount in + # deva.sh): grok resolves config credentials above XAI_API_KEY, + # so a mounted config.toml could silently bill another account. + AUTH_DETAILS="api-key (XAI_API_KEY)" + DOCKER_ARGS+=("-e" "XAI_API_KEY=$XAI_API_KEY") + ;; + *) + auth_error "auth method '$method' not implemented for grok" + ;; + esac +} diff --git a/agents/shared_auth.sh b/agents/shared_auth.sh index ba4a3e2..90b5db4 100644 --- a/agents/shared_auth.sh +++ b/agents/shared_auth.sh @@ -257,6 +257,9 @@ parse_auth_args() { gemini) supported_methods=(oauth api-key gemini-api-key vertex compute-adc gemini-app-oauth) ;; + grok) + supported_methods=(oauth api-key) + ;; *) auth_error "Unknown agent: $agent_name" ;; @@ -309,6 +312,7 @@ parse_auth_args() { claude) auth_method="claude" ;; codex) auth_method="chatgpt" ;; gemini) auth_method="oauth" ;; + grok) auth_method="oauth" ;; esac fi diff --git a/deva.sh b/deva.sh index e956f62..568d6b7 100755 --- a/deva.sh +++ b/deva.sh @@ -186,6 +186,7 @@ Examples: deva.sh claude # Same deva.sh codex # Launch codex in the same default container shape deva.sh gemini # Launch gemini in the same default container shape + deva.sh grok # Launch grok in the same default container shape deva.sh claude --rm # Ephemeral: deva-work-myapp-claude-12345 # Container management (current project) @@ -882,7 +883,7 @@ generate_auth_tag() { fi case "$agent:$auth_method" in - claude:claude|codex:chatgpt|gemini:oauth|gemini:gemini-app-oauth) + claude:claude|codex:chatgpt|gemini:oauth|gemini:gemini-app-oauth|grok:oauth) printf '%s' "auth-default" return ;; @@ -904,6 +905,7 @@ generate_auth_tag() { claude) key_val="${ANTHROPIC_API_KEY:-}" ;; codex) key_val="${OPENAI_API_KEY:-}" ;; gemini) key_val="${GEMINI_API_KEY:-${GOOGLE_API_KEY:-}}" ;; + grok) key_val="${XAI_API_KEY:-}" ;; esac if [ -n "$key_val" ] && [ ${#key_val} -ge 4 ]; then printf '%s' "api-key-${key_val: -4}" @@ -1251,7 +1253,8 @@ categorize_mount() { elif [ "$dest" = "/var/run/docker.sock" ]; then printf 'bridge' elif [[ "$dest" == /deva-host-chrome-bridge* ]]; then printf 'bridge' elif [[ "$dest" == /home/deva/.claude* ]] || [[ "$dest" == /home/deva/.codex* ]] || \ - [[ "$dest" == /home/deva/.gemini* ]] || [ "$dest" = "/home/deva/.agents" ]; then + [[ "$dest" == /home/deva/.gemini* ]] || [[ "$dest" == /home/deva/.grok* ]] || \ + [ "$dest" = "/home/deva/.agents" ]; then printf 'config' else printf 'user' fi @@ -1432,7 +1435,7 @@ cmd_status() { if [ -d "$config_root" ]; then echo "Agent Homes ($(shorten_path "$config_root")):" - for agent_name in claude codex gemini; do + for agent_name in claude codex gemini grok; do local agent_dir="$config_root/$agent_name" if [ -d "$agent_dir" ]; then local canonical="" other_count=0 entry is_canonical @@ -1443,6 +1446,7 @@ cmd_status() { claude:.claude|claude:.claude.json) is_canonical=true ;; codex:.codex) is_canonical=true ;; gemini:.gemini) is_canonical=true ;; + grok:.grok) is_canonical=true ;; esac if [ "$is_canonical" = true ]; then if [ -L "$agent_dir/$entry" ]; then @@ -1893,6 +1897,24 @@ should_skip_env_for_auth() { ;; esac ;; + grok) + case "${AUTH_METHOD:-oauth}" in + oauth) + case "$name" in + XAI_API_KEY | ANTHROPIC_API_KEY | ANTHROPIC_BASE_URL | OPENAI_API_KEY | OPENAI_BASE_URL) + return 0 + ;; + esac + ;; + api-key) + case "$name" in + XAI_API_KEY) + return 0 + ;; + esac + ;; + esac + ;; esac return 1 @@ -1949,10 +1971,44 @@ agent_canonical_basenames() { claude) printf '%s\n' '.claude' '.claude.json' ;; codex) printf '%s\n' '.codex' ;; gemini) printf '%s\n' '.gemini' ;; + grok) printf '%s\n' '.grok' ;; *) return 0 ;; esac } +# grok api-key contract (#403): pass XAI_API_KEY, mount no auth dir. +# grok's credential priority is model.api_key > model.env_key > session +# token > XAI_API_KEY, so anything a mounted ~/.grok carries (config.toml +# per-model keys, auth.json session) can outrank the exported key and +# silently bill another account. +grok_api_key_no_mount() { + [ "$ACTIVE_AGENT" = "grok" ] && [ "${AUTH_METHOD:-}" = "api-key" ] +} + +# grok's self-updater writes Linux binaries into ~/.grok/bin and +# ~/.grok/downloads — inside the auth dir we bind-mount. Verified against +# @xai-official/grok 0.2.93: a mounted config.toml without the npm installer +# marker flips the updater to installer=internal, and `grok update` then +# re-creates both dirs on the mount (worst case: Linux binary shadowing a +# macOS host CLI via the host npm trampoline). Whenever a host dir lands at +# /home/deva/.grok, overlay those two paths with container-local tmpfs so +# updater writes die with the container instead of poisoning the host. +append_grok_update_guard() { + local arg + for arg in "${DOCKER_ARGS[@]+"${DOCKER_ARGS[@]}"}"; do + case "$arg" in + *:/home/deva/.grok | *:/home/deva/.grok:*) + DOCKER_ARGS+=( + --tmpfs "/home/deva/.grok/bin:uid=$(id -u),gid=$(id -g),mode=0755,size=512m" + --tmpfs "/home/deva/.grok/downloads:uid=$(id -u),gid=$(id -g),mode=0755,size=512m" + ) + return 0 + ;; + esac + done + return 0 +} + # Mount the canonical entries for one agent from a source directory. # Host-side CLI -v or .deva VOLUME= at the same container target wins # (user_volumes_declares_target suppression; first-writer-wins holds). @@ -1960,6 +2016,9 @@ mount_agent_canonical() { local agent="$1" local src_dir="$2" [ -n "$agent" ] && [ -d "$src_dir" ] || return 0 + if [ "$agent" = "grok" ] && grok_api_key_no_mount; then + return 0 + fi local entry src while IFS= read -r entry; do @@ -1989,7 +2048,7 @@ mount_config_home() { mount_agent_canonical "$ACTIVE_AGENT" "$CONFIG_HOME" } -# Effective config base: where agent config dirs (.claude/, .codex/, .gemini/) live. +# Effective config base: where agent config dirs (.claude/, .codex/, .gemini/, .grok/) live. resolve_config_base() { if [ -n "$CONFIG_HOME" ]; then printf '%s' "$CONFIG_HOME" @@ -2016,7 +2075,7 @@ has_auth_override() { # Non-default --auth-with if [ -n "${AUTH_METHOD:-}" ]; then case "${ACTIVE_AGENT}:${AUTH_METHOD}" in - claude:claude|codex:chatgpt|gemini:oauth|gemini:gemini-app-oauth) ;; + claude:claude|codex:chatgpt|gemini:oauth|gemini:gemini-app-oauth|grok:oauth) ;; *) return 0 ;; esac fi @@ -2027,6 +2086,7 @@ has_auth_override() { claude) auth_vars="ANTHROPIC_API_KEY ANTHROPIC_AUTH_TOKEN CLAUDE_CODE_OAUTH_TOKEN" ;; codex) auth_vars="OPENAI_API_KEY" ;; gemini) auth_vars="GEMINI_API_KEY" ;; + grok) auth_vars="XAI_API_KEY" ;; esac local var @@ -2067,6 +2127,12 @@ default_credential_target_path() { gemini) printf '%s' "/home/deva/.gemini/mcp-oauth-tokens-v2.json" ;; + grok) + # api-key mode mounts no ~/.grok (grok_api_key_no_mount), but an + # explicit user -v/.deva VOLUME can still carry one in; grok prefers + # a session token over XAI_API_KEY, so blank-overlay auth.json anyway. + printf '%s' "/home/deva/.grok/auth.json" + ;; *) return 1 ;; @@ -2093,7 +2159,7 @@ append_auth_credential_overlay() { local state_dir="${XDG_STATE_HOME:-$HOME/.local/state}/deva/auth-overlays/$ACTIVE_AGENT" local overlay_key="${AUTH_METHOD:-default}" case "$ACTIVE_AGENT:$AUTH_METHOD" in - claude:claude | codex:chatgpt | gemini:oauth | gemini:gemini-app-oauth) + claude:claude | codex:chatgpt | gemini:oauth | gemini:gemini-app-oauth | grok:oauth) overlay_key="env" ;; esac @@ -3207,7 +3273,7 @@ if [ "$CONFIG_HOME_AUTO" = true ]; then fi if [ "$CONFIG_HOME_FROM_CLI" = true ] && [ -n "$CONFIG_HOME" ]; then - if [ -d "$CONFIG_HOME/claude" ] || [ -d "$CONFIG_HOME/codex" ] || [ -d "$CONFIG_HOME/gemini" ]; then + if [ -d "$CONFIG_HOME/claude" ] || [ -d "$CONFIG_HOME/codex" ] || [ -d "$CONFIG_HOME/gemini" ] || [ -d "$CONFIG_HOME/grok" ]; then CONFIG_ROOT="$CONFIG_HOME" CONFIG_HOME="" CONFIG_HOME_AUTO=false @@ -3258,6 +3324,17 @@ autolink_legacy_into_deva_root() { if [ -d "$CONFIG_ROOT" ]; then [ -d "$CONFIG_ROOT/gemini/.gemini" ] || [ -L "$CONFIG_ROOT/gemini/.gemini" ] || mkdir -p "$CONFIG_ROOT/gemini/.gemini" fi + + if [ -d "$HOME/.grok" ]; then + [ -d "$CONFIG_ROOT/grok" ] || mkdir -p "$CONFIG_ROOT/grok" + if [ ! -e "$CONFIG_ROOT/grok/.grok" ] && [ ! -L "$CONFIG_ROOT/grok/.grok" ]; then + ln -s "$HOME/.grok" "$CONFIG_ROOT/grok/.grok" + echo "autolink: ~/.grok -> $CONFIG_ROOT/grok/.grok" >&2 + fi + fi + if [ -d "$CONFIG_ROOT" ]; then + [ -d "$CONFIG_ROOT/grok/.grok" ] || [ -L "$CONFIG_ROOT/grok/.grok" ] || mkdir -p "$CONFIG_ROOT/grok/.grok" + fi } check_agent "$ACTIVE_AGENT" @@ -3281,6 +3358,9 @@ if [ -n "$CONFIG_HOME" ] && [ "$DRY_RUN" != true ]; then [ -d "$CONFIG_HOME/.gemini" ] || mkdir -p "$CONFIG_HOME/.gemini" [ -f "$CONFIG_HOME/.gemini/settings.json" ] || echo '{}' >"$CONFIG_HOME/.gemini/settings.json" ;; + grok) + [ -d "$CONFIG_HOME/.grok" ] || mkdir -p "$CONFIG_HOME/.grok" + ;; esac fi @@ -3311,6 +3391,11 @@ if [ "$CONFIG_HOME_FROM_CLI" = true ] && [ -n "$CONFIG_HOME" ] && [ "$_config_ho echo "warning: $CONFIG_HOME/.gemini is empty; authentication will need to be set up" >&2 fi ;; + grok) + if [ ! -d "$CONFIG_HOME/.grok" ] || [ -z "$(ls -A "$CONFIG_HOME/.grok" 2>/dev/null)" ]; then + echo "warning: $CONFIG_HOME/.grok is empty; authentication will need to be set up" >&2 + fi + ;; esac fi @@ -3345,7 +3430,7 @@ _step "agent_prepare" if [ -n "${AUTH_METHOD:-}" ]; then case "${ACTIVE_AGENT}:${AUTH_METHOD}" in - claude:claude|codex:chatgpt|gemini:oauth|gemini:gemini-app-oauth) ;; + claude:claude|codex:chatgpt|gemini:oauth|gemini:gemini-app-oauth|grok:oauth) ;; *) _needs_rewrite=true ;; esac @@ -3416,8 +3501,8 @@ fi # -Q bare mode: skip all config/auth mounts entirely. # Explicit --config-home DIR: isolate to that single home (no sibling agents). # Default: walk CONFIG_ROOT but only for KNOWN AGENT subdirs, and within each -# mount only the CANONICAL entries (.claude/.claude.json/.codex/.gemini). Non- -# agent subdirs (sessions/, cache/, adhoc state) are skipped. This keeps the +# mount only the CANONICAL entries (.claude/.claude.json/.codex/.gemini/.grok). +# Non-agent subdirs (sessions/, cache/, adhoc state) are skipped. This keeps the # mount count bounded at ~4 regardless of how much state agents accumulate # under their home dirs — the old unfiltered walk could emit 200+ mounts and # turn validate_bind_mount_shape's O(N²) into a several-minute stall. @@ -3442,6 +3527,9 @@ else [ -f "$HOME/.claude.json" ] && DOCKER_ARGS+=("-v" "$HOME/.claude.json:/home/deva/.claude.json") [ -d "$HOME/.codex" ] && DOCKER_ARGS+=("-v" "$HOME/.codex:/home/deva/.codex") [ -d "$HOME/.gemini" ] && DOCKER_ARGS+=("-v" "$HOME/.gemini:/home/deva/.gemini") + if [ -d "$HOME/.grok" ] && ! grok_api_key_no_mount; then + DOCKER_ARGS+=("-v" "$HOME/.grok:/home/deva/.grok") + fi fi fi _step "mount dispatch: done" @@ -3469,6 +3557,9 @@ if [ "$CONFIG_HOME_FROM_CLI" = false ] && [ "$QUICK_MODE" = false ]; then fi fi +append_grok_update_guard +_step "append_grok_update_guard" + append_user_envs _step "append_user_envs" validate_bind_mount_shape diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 35fb7dd..7738939 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -29,6 +29,17 @@ get_codex_version() { echo "$version" } +get_grok_version() { + local version="" + for path in "$DEVA_HOME/.local/bin/grok" "$(command -v grok 2>/dev/null)"; do + if [ -x "$path" ]; then + version=$($path --version 2>/dev/null | head -1) + break + fi + done + echo "$version" +} + show_environment_info() { local header="[deva]" case "$DEVA_AGENT" in @@ -46,6 +57,13 @@ show_environment_info() { header+=" Codex CLI (version detection failed)" fi ;; + grok) + if [ -n "$GROK_VERSION" ]; then + header+=" Starting Grok ($GROK_VERSION)" + else + header+=" Grok CLI (version detection failed)" + fi + ;; *) header+=" Starting agent: $DEVA_AGENT" ;; @@ -87,6 +105,28 @@ show_environment_info() { else echo "warning: Claude auth directory not found in $DEVA_HOME/.claude" fi + elif [ "$DEVA_AGENT" = "grok" ]; then + local grok_path="" + for path in "$DEVA_HOME/.local/bin/grok" "$(command -v grok 2>/dev/null)"; do + if [ -x "$path" ]; then + grok_path="$path" + break + fi + done + if [ -n "$grok_path" ]; then + echo "Grok CLI: $($grok_path --version 2>/dev/null || echo 'Found but version failed')" + echo "Grok location: $grok_path" + else + echo "Grok CLI not found in PATH" + fi + + if [ -d "$DEVA_HOME/.grok" ]; then + echo "Grok auth directory mounted" + # shellcheck disable=SC2012 + ls -la "$DEVA_HOME/.grok" | head -5 + else + echo "warning: Grok auth directory not found in $DEVA_HOME/.grok" + fi else local codex_path="" for path in "/usr/local/bin/codex" "/usr/bin/codex" "$(command -v codex 2>/dev/null)"; do @@ -306,6 +346,12 @@ ensure_agent_binaries() { exit 1 fi ;; + grok) + if ! command -v grok >/dev/null 2>&1; then + echo "error: Grok CLI not found in container" + exit 1 + fi + ;; esac } @@ -316,6 +362,7 @@ main() { CLAUDE_VERSION="$(get_claude_version)" CODEX_VERSION="$(get_codex_version)" + GROK_VERSION="$(get_grok_version)" if [ -n "$ANTHROPIC_BASE_URL" ]; then case "$ANTHROPIC_BASE_URL" in diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index ee758e0..759478d 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -47,7 +47,8 @@ Deva-root layout: ~/auth-roots/team-a/ ├── claude/ ├── codex/ -└── gemini/ +├── gemini/ +└── grok/ ``` ```bash diff --git a/docs/authentication.md b/docs/authentication.md index 0f0209c..594a9fd 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -20,6 +20,7 @@ This guide documents what `deva.sh` actually supports, what env vars it reads, a | Claude | `claude` | `api-key`, `oat`, `bedrock`, `vertex`, `copilot`, credentials file | `.claude`, `.claude.json`, `ANTHROPIC_*`, `CLAUDE_CODE_OAUTH_TOKEN`, `AWS_*`, gcloud, `GH_TOKEN` | | Codex | `chatgpt` | `api-key`, `copilot`, credentials file | `.codex/auth.json`, `OPENAI_API_KEY`, `GH_TOKEN` | | Gemini | `oauth` | `api-key`, `gemini-api-key`, `vertex`, `compute-adc`, `gemini-app-oauth`, credentials file | `.gemini`, `GEMINI_API_KEY`, gcloud, service-account JSON | +| Grok | `oauth` | `api-key` | `.grok/auth.json`, `XAI_API_KEY` | ## Claude @@ -267,6 +268,62 @@ Example: deva.sh gemini --auth-with ~/keys/gcp-service-account.json ``` +## Grok + +### Default: `--auth-with oauth` + +Uses: + +- `/home/deva/.grok` + +Grok stores its session token in `.grok/auth.json`. First login opens a +browser, which does not exist inside the container. Two ways in: + +- authenticate on the host once; deva auto-links `~/.grok` and the mount + carries `auth.json` into the container +- run `grok login --device-auth` inside the container: it prints a URL and + code you complete on any device + +### `--auth-with api-key` + +Requires: + +- `XAI_API_KEY` (from [console.x.ai](https://console.x.ai)) + +Grok's credential priority puts mounted config above the environment: +`model.api_key` > `model.env_key` > session token > `XAI_API_KEY`. So in +this mode deva mounts no `~/.grok` at all — the exported key is the only +credential in the container, and it is what gets billed. Session state +lives container-local and does not persist across runs in this mode. + +If you force a `~/.grok` mount with `-v` anyway, deva still blank-overlays +the default `auth.json`, but per-model keys in a mounted `config.toml` +outrank `XAI_API_KEY` — don't mix the two. + +Example: + +```bash +export XAI_API_KEY=... +deva.sh grok --auth-with api-key +``` + +### One more grok-specific wire + +Grok keeps its real binary in `~/.grok/bin/` (its self-update dir) and the +npm launcher resolves that path first. Inside the image, deva moves the +binary to `~/.local/bin/grok` and removes `~/.grok/bin`, so a host-mounted +`~/.grok` (which may contain a macOS binary) never shadows it. + +The reverse direction is guarded too: grok's self-updater writes into +`~/.grok/bin/` and `~/.grok/downloads/`. A mounted `config.toml` without +the npm installer marker makes the updater treat the install as +self-managed, so `grok update` would drop Linux binaries into the host +mount — breaking a macOS host CLI via its npm launcher. Whenever a host +dir is mounted at `/home/deva/.grok`, deva overlays those two paths with +container-local tmpfs: in-container `grok update` works, its writes die +with the container, and the host install stays intact. The image pin +(`GROK_CLI_VERSION`) is the only version that matters. + ## Config Homes And Auth Isolation Default homes live under: @@ -275,6 +332,7 @@ Default homes live under: ~/.config/deva/claude ~/.config/deva/codex ~/.config/deva/gemini +~/.config/deva/grok ``` Use `--config-home` when you want a separate identity: diff --git a/docs/how-it-works.md b/docs/how-it-works.md index c00a261..48cc927 100644 --- a/docs/how-it-works.md +++ b/docs/how-it-works.md @@ -61,13 +61,14 @@ Default per-agent homes live under: ~/.config/deva/ ├── claude/ ├── codex/ -└── gemini/ +├── gemini/ +└── grok/ ``` `--config-home` supports two layouts: -- leaf home: `DIR/.claude`, `DIR/.claude.json`, `DIR/.codex`, `DIR/.gemini` -- deva root: `DIR/claude`, `DIR/codex`, `DIR/gemini` +- leaf home: `DIR/.claude`, `DIR/.claude.json`, `DIR/.codex`, `DIR/.gemini`, `DIR/.grok` +- deva root: `DIR/claude`, `DIR/codex`, `DIR/gemini`, `DIR/grok` `-Q` disables config-home resolution, autolink, and host config mounts entirely. @@ -81,6 +82,7 @@ Examples: - Claude API-style auth: env vars such as `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`, or `CLAUDE_CODE_OAUTH_TOKEN` - Codex default: `.codex/auth.json` - Gemini default: `.gemini` +- Grok default: `.grok/auth.json` When non-default auth is active, deva mounts a blank overlay over the default credential file path so the agent cannot silently fall back to some unrelated OAuth state. That is the point of the overlay fix. diff --git a/docs/quick-start.md b/docs/quick-start.md index 7562bfc..fd84475 100644 --- a/docs/quick-start.md +++ b/docs/quick-start.md @@ -26,6 +26,7 @@ That installs: - `agents/claude.sh` - `agents/codex.sh` - `agents/gemini.sh` +- `agents/grok.sh` - `agents/shared_auth.sh` It also pulls `ghcr.io/thevibeworks/deva:latest`, with Docker Hub as fallback. @@ -73,6 +74,7 @@ Same project, same default container shape: ```bash deva.sh claude deva.sh gemini +deva.sh grok ``` That is one of the main reasons this wrapper exists. You do not need a separate pet workflow for every vendor. @@ -110,6 +112,13 @@ export GEMINI_API_KEY=... deva.sh gemini --auth-with api-key ``` +Grok with xAI API key: + +```bash +export XAI_API_KEY=xai-... +deva.sh grok --auth-with api-key +``` + More auth details live in [Authentication Guide](authentication.md). ## Useful Modes diff --git a/install.sh b/install.sh index 912f5fb..fe07b88 100644 --- a/install.sh +++ b/install.sh @@ -10,6 +10,7 @@ agent_files=( "claude.sh" "codex.sh" "gemini.sh" + "grok.sh" "shared_auth.sh" ) @@ -138,6 +139,7 @@ echo " - $INSTALL_DIR/claude-yolo" echo " - $INSTALL_DIR/agents/claude.sh" echo " - $INSTALL_DIR/agents/codex.sh" echo " - $INSTALL_DIR/agents/gemini.sh" +echo " - $INSTALL_DIR/agents/grok.sh" echo " - $INSTALL_DIR/agents/shared_auth.sh" echo "" echo "Quick start:" diff --git a/scripts/install-agent-tooling.sh b/scripts/install-agent-tooling.sh index ee64386..9be5e8d 100644 --- a/scripts/install-agent-tooling.sh +++ b/scripts/install-agent-tooling.sh @@ -7,6 +7,7 @@ set -euo pipefail : "${CLAUDE_CODE_VERSION:?CLAUDE_CODE_VERSION is required}" : "${CODEX_VERSION:?CODEX_VERSION is required}" : "${GEMINI_CLI_VERSION:?GEMINI_CLI_VERSION is required}" +: "${GROK_CLI_VERSION:?GROK_CLI_VERSION is required}" CCTRACE_VERSION="${CCTRACE_VERSION:-0.4.0}" CCX_VERSION="${CCX_VERSION:-v0.7.0}" @@ -119,7 +120,7 @@ download_to() { install_npm_agent_tooling() { log "Installing npm agent tooling" - log "Requested versions: claude=${CLAUDE_CODE_VERSION} codex=${CODEX_VERSION} gemini=${GEMINI_CLI_VERSION}" + log "Requested versions: claude=${CLAUDE_CODE_VERSION} codex=${CODEX_VERSION} gemini=${GEMINI_CLI_VERSION} grok=${GROK_CLI_VERSION}" mkdir -p "$DEVA_HOME/.npm-global" "$DEVA_HOME/.local/bin" npm config set prefix "$DEVA_HOME/.npm-global" @@ -130,6 +131,7 @@ install_npm_agent_tooling() { "@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}" \ "@openai/codex@${CODEX_VERSION}" \ "@google/gemini-cli@${GEMINI_CLI_VERSION}" \ + "@xai-official/grok@${GROK_CLI_VERSION}" \ || die "npm install failed" npm cache clean --force @@ -138,7 +140,33 @@ install_npm_agent_tooling() { "$DEVA_HOME/.npm-global/bin/claude" --version "$DEVA_HOME/.npm-global/bin/codex" --version "$DEVA_HOME/.npm-global/bin/gemini" --version - (npm list -g --depth=0 @anthropic-ai/claude-code @openai/codex @google/gemini-cli || true) + pin_grok_platform_binary + (npm list -g --depth=0 @anthropic-ai/claude-code @openai/codex @google/gemini-cli @xai-official/grok || true) +} + +# grok's npm postinstall puts the real binary in ~/.grok/bin (the CLI's +# self-update dir) and the npm bin is a node trampoline that resolves that +# path FIRST. At runtime a host ~/.grok mounted for auth would shadow the +# image binary (worst case: exec-format error on a host macOS binary). +# Move the binary to ~/.local/bin (first in PATH), repoint the npm bin, and +# drop ~/.grok/bin so the container always runs the image binary no matter +# what is mounted at ~/.grok. +pin_grok_platform_binary() { + local trampoline="$DEVA_HOME/.npm-global/bin/grok" + # Also bootstraps the canonical ~/.grok/bin layout from the per-platform + # package in the rare case npm skipped the postinstall. + "$trampoline" --version >/dev/null || die "grok install verification failed" + + local real_bin + real_bin="$(readlink -f "$DEVA_HOME/.grok/bin/grok" 2>/dev/null || true)" + [ -n "$real_bin" ] && [ -f "$real_bin" ] || die "grok binary not found under $DEVA_HOME/.grok/bin" + + mv "$real_bin" "$DEVA_HOME/.local/bin/grok" + chmod 755 "$DEVA_HOME/.local/bin/grok" + ln -sf "$DEVA_HOME/.local/bin/grok" "$trampoline" + rm -rf "$DEVA_HOME/.grok/bin" + + "$DEVA_HOME/.local/bin/grok" --version } ccx_platform() { diff --git a/scripts/release-utils.sh b/scripts/release-utils.sh index 3fca661..8d09660 100755 --- a/scripts/release-utils.sh +++ b/scripts/release-utils.sh @@ -29,6 +29,7 @@ TOOL_REGISTRY=( "claude-code|npm|@anthropic-ai/claude-code|org.opencontainers.image.claude_code_version|https://www.npmjs.com/package/@anthropic-ai/claude-code|https://raw.githubusercontent.com/anthropics/claude-code/main/CHANGELOG.md|agent|main" "codex|npm|@openai/codex|org.opencontainers.image.codex_version|https://www.npmjs.com/package/@openai/codex|github:openai/codex|agent|main" "gemini-cli|npm|@google/gemini-cli|org.opencontainers.image.gemini_cli_version|https://www.npmjs.com/package/@google/gemini-cli||agent|main" + "grok-cli|npm|@xai-official/grok|org.opencontainers.image.grok_cli_version|https://www.npmjs.com/package/@xai-official/grok||agent|main" "ccx|github-release|thevibeworks/ccx|org.opencontainers.image.ccx_version|https://github.com/thevibeworks/ccx|github:thevibeworks/ccx|agent|main" "copilot-api|github-commit|ericc-ch/copilot-api|org.opencontainers.image.copilot_api_version|https://github.com/ericc-ch/copilot-api||agent|main" "cctrace|npm|@thevibeworks/cctrace|org.opencontainers.image.cctrace_version|https://www.npmjs.com/package/@thevibeworks/cctrace|github:thevibeworks/cctrace|agent|main" @@ -134,6 +135,7 @@ get_display_name() { claude-code) echo "Claude Code" ;; codex) echo "Codex" ;; gemini-cli) echo "Gemini CLI" ;; + grok-cli) echo "Grok CLI" ;; ccx) echo "CCX" ;; copilot-api) echo "Copilot API" ;; playwright) echo "Playwright" ;; diff --git a/scripts/resolve-tool-versions.sh b/scripts/resolve-tool-versions.sh index 7251e0e..96d387c 100644 --- a/scripts/resolve-tool-versions.sh +++ b/scripts/resolve-tool-versions.sh @@ -30,6 +30,7 @@ main() { resolve_tool "claude_code_version" "claude-code" resolve_tool "codex_version" "codex" resolve_tool "gemini_cli_version" "gemini-cli" + resolve_tool "grok_cli_version" "grok-cli" resolve_tool "ccx_version" "ccx" resolve_tool "copilot_api_version" "copilot-api" } diff --git a/scripts/test-install-agent-tooling.sh b/scripts/test-install-agent-tooling.sh index 9bcf98c..ccee700 100755 --- a/scripts/test-install-agent-tooling.sh +++ b/scripts/test-install-agent-tooling.sh @@ -34,17 +34,28 @@ config) ;; install) mkdir -p "$DEVA_HOME/.npm-global/bin" - for bin in claude codex gemini; do + for bin in claude codex gemini grok; do cat >"$DEVA_HOME/.npm-global/bin/$bin" <<'BIN' #!/usr/bin/env bash case "$(basename "$0")" in claude) echo "__CLAUDE_CODE_VERSION__ (Claude Code)" ;; codex) echo "codex-cli __CODEX_VERSION__" ;; gemini) echo "__GEMINI_CLI_VERSION__" ;; + grok) echo "grok __GROK_CLI_VERSION__" ;; esac BIN chmod +x "$DEVA_HOME/.npm-global/bin/$bin" done + # grok postinstall layout: real binary lives in ~/.grok/bin with a + # versioned name + canonical symlink, consumed by pin_grok_platform_binary + grok_bin_dir="$DEVA_HOME/.grok/bin" + mkdir -p "$grok_bin_dir" + cat >"$grok_bin_dir/grok-__GROK_CLI_VERSION__" <<'BIN' +#!/usr/bin/env bash +echo "grok __GROK_CLI_VERSION__" +BIN + chmod +x "$grok_bin_dir/grok-__GROK_CLI_VERSION__" + ln -sf "grok-__GROK_CLI_VERSION__" "$grok_bin_dir/grok" ;; pack) spec="${1:?pack spec required}" @@ -72,6 +83,7 @@ sed -i \ -e "s#__CLAUDE_CODE_VERSION__#$CLAUDE_CODE_VERSION#g" \ -e "s#__CODEX_VERSION__#$CODEX_VERSION#g" \ -e "s#__GEMINI_CLI_VERSION__#$GEMINI_CLI_VERSION#g" \ + -e "s#__GROK_CLI_VERSION__#$GROK_CLI_VERSION#g" \ "$fake_bin/npm" cat >"$fake_bin/curl" <<'EOF' @@ -171,6 +183,7 @@ mkdir -p "$tmp_root/deny" CCTRACE_VERSION="$CCTRACE_VERSION" \ CODEX_VERSION="$CODEX_VERSION" \ GEMINI_CLI_VERSION="$GEMINI_CLI_VERSION" \ + GROK_CLI_VERSION="$GROK_CLI_VERSION" \ CCX_VERSION="$CCX_VERSION" \ DEVA_SKIP_NPM_REGISTRY_CHECK=1 \ bash "$REPO_ROOT/scripts/install-agent-tooling.sh" 2>&1 @@ -178,6 +191,12 @@ mkdir -p "$tmp_root/deny" grep -F "Current working directory is not accessible; switching to $fake_home" <<<"$output" >/dev/null grep -F "Installing npm agent tooling" <<<"$output" >/dev/null + # pin_grok_platform_binary: real binary moved to .local/bin, global bin + # repointed at it, and the ~/.grok/bin self-update dir removed. + test -x "$fake_home/.local/bin/grok" + test -L "$fake_home/.npm-global/bin/grok" + [ "$(readlink "$fake_home/.npm-global/bin/grok")" = "$fake_home/.local/bin/grok" ] + test ! -e "$fake_home/.grok/bin" grep -F "Installing ccx pinned to $CCX_VERSION" <<<"$output" >/dev/null grep -F "falling back to go install" <<<"$output" >/dev/null grep -F "ccx installed" <<<"$output" >/dev/null diff --git a/scripts/test-mount-shape.sh b/scripts/test-mount-shape.sh index 43e04d8..a638ce4 100755 --- a/scripts/test-mount-shape.sh +++ b/scripts/test-mount-shape.sh @@ -53,9 +53,11 @@ mkdir -p \ "$hybrid_root/xdg/deva/claude" \ "$hybrid_root/xdg/deva/codex" \ "$hybrid_root/xdg/deva/gemini" \ + "$hybrid_root/xdg/deva/grok" \ "$hybrid_root/host/.claude" \ "$hybrid_root/host/.codex" \ "$hybrid_root/host/.gemini" \ + "$hybrid_root/host/.grok" \ "$hybrid_root/cli/.codex" touch "$hybrid_root/host/.claude.json" @@ -63,10 +65,12 @@ ln -sf "$hybrid_root/host/.claude" "$hybrid_root/xdg/deva/claude/.claude" ln -sf "$hybrid_root/host/.claude.json" "$hybrid_root/xdg/deva/claude/.claude.json" ln -sf "$hybrid_root/host/.codex" "$hybrid_root/xdg/deva/codex/.codex" ln -sf "$hybrid_root/host/.gemini" "$hybrid_root/xdg/deva/gemini/.gemini" +ln -sf "$hybrid_root/host/.grok" "$hybrid_root/xdg/deva/grok/.grok" cat > "$hybrid_root/xdg/deva/.deva" </dev/null; then echo "hybrid recipe triggered duplicate-target error for $agent" >&2 echo "$out" >&2 exit 1 fi - for tgt in /home/deva/.claude /home/deva/.claude.json /home/deva/.codex /home/deva/.gemini; do + for tgt in /home/deva/.claude /home/deva/.claude.json /home/deva/.codex /home/deva/.gemini /home/deva/.grok; do c="$(count_target "$tgt" "$out")" if [[ "$c" -ne 1 ]]; then echo "hybrid $agent: target $tgt emitted $c times (want 1)" >&2 @@ -152,7 +156,8 @@ trap default_cleanup EXIT mkdir -p \ "$default_root/xdg/deva/claude/.claude" \ "$default_root/xdg/deva/codex/.codex" \ - "$default_root/xdg/deva/gemini/.gemini" + "$default_root/xdg/deva/gemini/.gemini" \ + "$default_root/xdg/deva/grok/.grok" echo '{}' > "$default_root/xdg/deva/claude/.claude.json" run_default() { @@ -167,9 +172,9 @@ run_default() { ) 2>&1 } -for agent in claude codex gemini; do +for agent in claude codex gemini grok; do out="$(run_default "$agent" --dry-run || true)" - for tgt in /home/deva/.claude /home/deva/.claude.json /home/deva/.codex /home/deva/.gemini; do + for tgt in /home/deva/.claude /home/deva/.claude.json /home/deva/.codex /home/deva/.gemini /home/deva/.grok; do c="$(count_target "$tgt" "$out")" if [[ "$c" -ne 1 ]]; then echo "hybrid-default $agent: target $tgt emitted $c times (want 1)" >&2 @@ -179,6 +184,33 @@ for agent in claude codex gemini; do done done +# ───── grok update-guard + api-key no-mount coverage ───── +# A mounted /home/deva/.grok must come with tmpfs overlays for the +# self-updater dirs (bin/, downloads/) so in-container `grok update` +# cannot write into the host mount. +grok_out="$(run_default grok --dry-run || true)" +for guard in "--tmpfs /home/deva/.grok/bin:" "--tmpfs /home/deva/.grok/downloads:"; do + if ! grep -F -- "$guard" <<<"$grok_out" >/dev/null; then + echo "grok oauth: missing update guard $guard" >&2 + echo "$grok_out" >&2 + exit 1 + fi +done + +# api-key mode (#403): XAI_API_KEY only, no ~/.grok mount, no guard needed. +apikey_out="$(XAI_API_KEY=test-key run_default grok --auth-with api-key --dry-run || true)" +apikey_grok="$(count_target /home/deva/.grok "$apikey_out")" +if [[ "$apikey_grok" -ne 0 ]]; then + echo "grok api-key: target /home/deva/.grok emitted $apikey_grok times (want 0)" >&2 + echo "$apikey_out" >&2 + exit 1 +fi +if grep -F -- "--tmpfs /home/deva/.grok/bin:" <<<"$apikey_out" >/dev/null; then + echo "grok api-key: update guard emitted without a .grok mount" >&2 + echo "$apikey_out" >&2 + exit 1 +fi + # Explicit --config-home DIR isolates to a single home (no sibling agents). iso_out="$(run_default claude --config-home "$default_root/xdg/deva/claude" --dry-run || true)" iso_claude="$(count_target /home/deva/.claude "$iso_out")" diff --git a/scripts/test-version-targets.sh b/scripts/test-version-targets.sh index 7a859fd..e0b8842 100755 --- a/scripts/test-version-targets.sh +++ b/scripts/test-version-targets.sh @@ -31,6 +31,7 @@ assert_absent "CLAUDE_CODE_VERSION=" "$default_versions_up" assert_absent "CCTRACE_VERSION=" "$default_versions_up" assert_absent "CODEX_VERSION=" "$default_versions_up" assert_absent "GEMINI_CLI_VERSION=" "$default_versions_up" +assert_absent "GROK_CLI_VERSION=" "$default_versions_up" assert_absent "CCX_VERSION=" "$default_versions_up" assert_absent "COPILOT_API_VERSION=" "$default_versions_up" assert_absent "PLAYWRIGHT_VERSION=" "$default_versions_up" @@ -41,6 +42,7 @@ assert_absent "CLAUDE_CODE_VERSION=" "$default_versions" assert_absent "CCTRACE_VERSION=" "$default_versions" assert_absent "CODEX_VERSION=" "$default_versions" assert_absent "GEMINI_CLI_VERSION=" "$default_versions" +assert_absent "GROK_CLI_VERSION=" "$default_versions" assert_absent "CCX_VERSION=" "$default_versions" assert_absent "COPILOT_API_VERSION=" "$default_versions" assert_present "./scripts/version-report.sh" "$default_versions" @@ -51,6 +53,7 @@ override_versions_up="$( CCTRACE_VERSION=1.2.3 \ CODEX_VERSION=8.8.8 \ GEMINI_CLI_VERSION=7.7.7 \ + GROK_CLI_VERSION=5.5.5 \ CCX_VERSION=v6.6.6 \ COPILOT_API_VERSION=deadbeef \ PLAYWRIGHT_VERSION=1.2.4 \ @@ -60,6 +63,7 @@ assert_present "CLAUDE_CODE_VERSION=9.9.9" "$override_versions_up" assert_present "CCTRACE_VERSION=1.2.3" "$override_versions_up" assert_present "CODEX_VERSION=8.8.8" "$override_versions_up" assert_present "GEMINI_CLI_VERSION=7.7.7" "$override_versions_up" +assert_present "GROK_CLI_VERSION=5.5.5" "$override_versions_up" assert_present "CCX_VERSION=v6.6.6" "$override_versions_up" assert_present "COPILOT_API_VERSION=deadbeef" "$override_versions_up" assert_present "PLAYWRIGHT_VERSION=1.2.4" "$override_versions_up" @@ -70,6 +74,7 @@ override_versions="$( CCTRACE_VERSION=1.2.3 \ CODEX_VERSION=8.8.8 \ GEMINI_CLI_VERSION=7.7.7 \ + GROK_CLI_VERSION=5.5.5 \ CCX_VERSION=v6.6.6 \ COPILOT_API_VERSION=deadbeef \ versions @@ -78,5 +83,6 @@ assert_present "CLAUDE_CODE_VERSION=9.9.9" "$override_versions" assert_present "CCTRACE_VERSION=1.2.3" "$override_versions" assert_present "CODEX_VERSION=8.8.8" "$override_versions" assert_present "GEMINI_CLI_VERSION=7.7.7" "$override_versions" +assert_present "GROK_CLI_VERSION=5.5.5" "$override_versions" assert_present "CCX_VERSION=v6.6.6" "$override_versions" assert_present "COPILOT_API_VERSION=deadbeef" "$override_versions" diff --git a/scripts/update-version-pins.sh b/scripts/update-version-pins.sh index ee35599..d87e08c 100644 --- a/scripts/update-version-pins.sh +++ b/scripts/update-version-pins.sh @@ -64,6 +64,7 @@ CLAUDE_CODE_VERSION=$CLAUDE_CODE_VERSION CCTRACE_VERSION=$CCTRACE_VERSION CODEX_VERSION=$CODEX_VERSION GEMINI_CLI_VERSION=$GEMINI_CLI_VERSION +GROK_CLI_VERSION=$GROK_CLI_VERSION CCX_VERSION=$CCX_VERSION COPILOT_API_VERSION=$COPILOT_API_VERSION PLAYWRIGHT_VERSION=$PLAYWRIGHT_VERSION @@ -101,6 +102,7 @@ main() { refresh_pin CCTRACE_VERSION "$(fetch_npm_version @thevibeworks/cctrace)" refresh_pin CODEX_VERSION "$(fetch_npm_version @openai/codex)" refresh_pin GEMINI_CLI_VERSION "$(fetch_npm_version @google/gemini-cli)" + refresh_pin GROK_CLI_VERSION "$(fetch_npm_version @xai-official/grok)" refresh_pin CCX_VERSION "$(fetch_latest_git_tag https://github.com/thevibeworks/ccx.git)" refresh_pin COPILOT_API_VERSION "$(fetch_latest_commit https://github.com/ericc-ch/copilot-api.git refs/heads/master)" refresh_pin PLAYWRIGHT_VERSION "$(fetch_npm_version playwright)" diff --git a/scripts/version-pins.sh b/scripts/version-pins.sh index 3908032..a95341b 100644 --- a/scripts/version-pins.sh +++ b/scripts/version-pins.sh @@ -17,6 +17,7 @@ VERSION_PIN_VARS=( CCTRACE_VERSION CODEX_VERSION GEMINI_CLI_VERSION + GROK_CLI_VERSION CCX_VERSION COPILOT_API_VERSION PLAYWRIGHT_VERSION diff --git a/scripts/version-upgrade.sh b/scripts/version-upgrade.sh index 4e17ce9..af416a5 100755 --- a/scripts/version-upgrade.sh +++ b/scripts/version-upgrade.sh @@ -16,6 +16,7 @@ _CLI_CLAUDE_CODE="${CLAUDE_CODE_VERSION:-}" _CLI_CCTRACE="${CCTRACE_VERSION:-}" _CLI_CODEX="${CODEX_VERSION:-}" _CLI_GEMINI="${GEMINI_CLI_VERSION:-}" +_CLI_GROK="${GROK_CLI_VERSION:-}" _CLI_CCX="${CCX_VERSION:-}" _CLI_COPILOT="${COPILOT_API_VERSION:-}" _CLI_PLAYWRIGHT="${PLAYWRIGHT_VERSION:-}" @@ -50,6 +51,7 @@ Environment: CCTRACE_VERSION Override cctrace version CODEX_VERSION Override codex version GEMINI_CLI_VERSION Override gemini-cli version + GROK_CLI_VERSION Override grok-cli version CCX_VERSION Override ccx version COPILOT_API_VERSION Override copilot-api version PLAYWRIGHT_VERSION Override playwright version (rust image only) @@ -87,11 +89,12 @@ main() { # Resolve build versions early so we can show the manifest before countdown. # CLI override wins; otherwise use whatever load_versions fetched. - local claude_ver cctrace_ver codex_ver gemini_ver ccx_ver copilot_ver playwright_ver + local claude_ver cctrace_ver codex_ver gemini_ver grok_ver ccx_ver copilot_ver playwright_ver claude_ver="${_CLI_CLAUDE_CODE:-$(get_latest "claude-code")}" cctrace_ver="${_CLI_CCTRACE:-$(get_latest "cctrace")}" codex_ver="${_CLI_CODEX:-$(get_latest "codex")}" gemini_ver="${_CLI_GEMINI:-$(get_latest "gemini-cli")}" + grok_ver="${_CLI_GROK:-$(get_latest "grok-cli")}" ccx_ver="${_CLI_CCX:-$(get_latest "ccx")}" copilot_ver="${_CLI_COPILOT:-$(get_latest "copilot-api")}" playwright_ver="${_CLI_PLAYWRIGHT:-${PLAYWRIGHT_VERSION}}" @@ -100,6 +103,7 @@ main() { [[ -z $claude_ver ]] && missing+=("CLAUDE_CODE_VERSION") [[ -z $codex_ver ]] && missing+=("CODEX_VERSION") [[ -z $gemini_ver ]] && missing+=("GEMINI_CLI_VERSION") + [[ -z $grok_ver ]] && missing+=("GROK_CLI_VERSION") [[ -z $ccx_ver ]] && missing+=("CCX_VERSION") [[ -z $copilot_ver ]] && missing+=("COPILOT_API_VERSION") [[ -z $playwright_ver ]] && missing+=("PLAYWRIGHT_VERSION") @@ -116,6 +120,7 @@ main() { "Claude Code|claude_ver|_CLI_CLAUDE_CODE|claude-code" "cctrace|cctrace_ver|_CLI_CCTRACE|cctrace" "Codex|codex_ver|_CLI_CODEX|codex" + "Grok CLI|grok_ver|_CLI_GROK|grok-cli" "CCX|ccx_ver|_CLI_CCX|ccx" "Copilot API|copilot_ver|_CLI_COPILOT|copilot-api" "Playwright|playwright_ver|_CLI_PLAYWRIGHT|playwright" @@ -220,6 +225,7 @@ main() { --build-arg CCTRACE_VERSION="$cctrace_ver" \ --build-arg CODEX_VERSION="$codex_ver" \ --build-arg GEMINI_CLI_VERSION="$gemini_ver" \ + --build-arg GROK_CLI_VERSION="$grok_ver" \ --build-arg CCX_VERSION="$ccx_ver" \ --build-arg COPILOT_API_VERSION="$copilot_ver" \ -t "$BUILD_IMAGE" . @@ -232,6 +238,7 @@ main() { --build-arg CCTRACE_VERSION="$cctrace_ver" \ --build-arg CODEX_VERSION="$codex_ver" \ --build-arg GEMINI_CLI_VERSION="$gemini_ver" \ + --build-arg GROK_CLI_VERSION="$grok_ver" \ --build-arg CCX_VERSION="$ccx_ver" \ --build-arg PLAYWRIGHT_VERSION="$playwright_ver" \ --build-arg RUST_TOOLCHAINS="$RUST_TOOLCHAINS" \ diff --git a/tests/test_release_utils.sh b/tests/test_release_utils.sh index 30a0657..bcde3f1 100644 --- a/tests/test_release_utils.sh +++ b/tests/test_release_utils.sh @@ -129,7 +129,7 @@ assert_eq "caller 'image' not clobbered" "caller-image" "${leak_after##*|}" # ───── get_tools_by_group ───── section "get_tools_by_group" agent_tools="$(get_tools_by_group agent | sort | tr '\n' ' ' | sed 's/ $//')" -expected_agent="cctrace ccx claude-code codex copilot-api gemini-cli" +expected_agent="cctrace ccx claude-code codex copilot-api gemini-cli grok-cli" assert_eq "group=agent" "$expected_agent" "$agent_tools" browser_tools="$(get_tools_by_group browser | sort | tr '\n' ' ' | sed 's/ $//')" @@ -144,7 +144,7 @@ assert_eq "group=does-not-exist" "" "$nonexistent_group" # ───── get_tools_by_image ───── section "get_tools_by_image" main_tools="$(get_tools_by_image main | sort | tr '\n' ' ' | sed 's/ $//')" -expected_main="cctrace ccx claude-code codex copilot-api gemini-cli" +expected_main="cctrace ccx claude-code codex copilot-api gemini-cli grok-cli" assert_eq "image=main" "$expected_main" "$main_tools" rust_tools="$(get_tools_by_image rust | sort | tr '\n' ' ' | sed 's/ $//')" @@ -156,8 +156,8 @@ assert_eq "image=base (empty until Step 3)" "" "$base_tools" # ───── filter_tools: scope resolution ───── section "filter_tools: default (no scope)" default_all="$(unset GROUP TOOL IMAGE; filter_tools | sort | tr '\n' ' ' | sed 's/ $//')" -expected_all="cctrace ccx claude-code codex copilot-api gemini-cli playwright" -assert_eq "default returns all 7" "$expected_all" "$default_all" +expected_all="cctrace ccx claude-code codex copilot-api gemini-cli grok-cli playwright" +assert_eq "default returns all 8" "$expected_all" "$default_all" section "filter_tools: TOOL= scope" single="$(TOOL=claude-code GROUP= IMAGE= filter_tools)" diff --git a/tests/version-upgrade.sh b/tests/version-upgrade.sh index bdd1209..9371505 100644 --- a/tests/version-upgrade.sh +++ b/tests/version-upgrade.sh @@ -26,6 +26,7 @@ inspect) "org.opencontainers.image.cctrace_version":"0.3.0", "org.opencontainers.image.codex_version":"0.116.0", "org.opencontainers.image.gemini_cli_version":"0.35.0", + "org.opencontainers.image.grok_cli_version":"0.2.90", "org.opencontainers.image.ccx_version":"v0.7.0", "org.opencontainers.image.copilot_api_version":"0ea08febdd7e3e055b03dd298bf57e669500b5c1", "org.opencontainers.image.playwright_version":"1.59.0" @@ -76,6 +77,12 @@ case "${2:-}" in @google/gemini-cli@0.35.3) echo '{"0.35.3":"2026-03-28T03:17:00Z"}' ;; +@xai-official/grok) + echo "0.2.93" + ;; +@xai-official/grok@0.2.93) + echo '{"0.2.93":"2026-07-01T00:00:00Z"}' + ;; playwright) echo "1.60.0" ;; @@ -172,6 +179,7 @@ for expected in \ "--build-arg CCTRACE_VERSION=0.4.0" \ "--build-arg CODEX_VERSION=0.117.0" \ "--build-arg GEMINI_CLI_VERSION=0.35.3" \ + "--build-arg GROK_CLI_VERSION=0.2.93" \ "--build-arg CCX_VERSION=v0.7.0" \ "--build-arg COPILOT_API_VERSION=0ea08febdd7e3e055b03dd298bf57e669500b5c1" \ "--build-arg GO_VERSION=1.26.2" @@ -188,6 +196,7 @@ for expected in \ "--build-arg CCTRACE_VERSION=0.4.0" \ "--build-arg CODEX_VERSION=0.117.0" \ "--build-arg GEMINI_CLI_VERSION=0.35.3" \ + "--build-arg GROK_CLI_VERSION=0.2.93" \ "--build-arg CCX_VERSION=v0.7.0" \ "--build-arg PLAYWRIGHT_VERSION=1.60.0" do diff --git a/versions.env b/versions.env index 948f70d..c1d7545 100644 --- a/versions.env +++ b/versions.env @@ -12,6 +12,7 @@ CLAUDE_CODE_VERSION=2.1.143 CCTRACE_VERSION=0.4.0 CODEX_VERSION=0.131.0 GEMINI_CLI_VERSION=0.42.0 +GROK_CLI_VERSION=0.2.93 CCX_VERSION=v0.7.0 COPILOT_API_VERSION=0ea08febdd7e3e055b03dd298bf57e669500b5c1 PLAYWRIGHT_VERSION=1.60.0