Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .deva.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}" \
.
Expand All @@ -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 }}" \
Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/nightly-images.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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 }}\`
Expand Down Expand Up @@ -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 }}

Expand Down Expand Up @@ -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 }}
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down Expand Up @@ -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 }}\`
Expand Down Expand Up @@ -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 }}

Expand Down Expand Up @@ -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 }}
Expand Down
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
11 changes: 11 additions & 0 deletions DEV-LOGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@
- Minimal markdown markers, no unnecessary formatting, minimal emojis.
- Reference issue numbers in the format `#<issue-number>` 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:
Expand Down
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions Dockerfile.rust
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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}

Expand Down
17 changes: 14 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) \
Expand All @@ -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)) \
Expand Down Expand Up @@ -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" = "<no value>" ]; 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 \
Expand All @@ -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) .
Expand Down Expand Up @@ -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))"
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@
[![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.

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/`
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
Loading
Loading