diff --git a/.deva.example b/.deva.example index ad2e787..ff8a961 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, grok) and mounts each agent's +# ~/.config/deva/ (claude, codex, gemini, grok, kimi) 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, ~/.grok on first run. +# ~/.claude, ~/.codex, ~/.gemini, ~/.grok, ~/.kimi-code 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 8bcdaf1..665d9e2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,6 +80,7 @@ jobs: --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 KIMI_CODE_VERSION="${{ steps.pins.outputs.kimi_code_version }}" \ --build-arg CCX_VERSION="${{ steps.pins.outputs.ccx_version }}" \ --build-arg COPILOT_API_VERSION="${{ steps.pins.outputs.copilot_api_version }}" \ . @@ -102,6 +103,7 @@ jobs: 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 }}" \ + KIMI_CODE_VERSION="${{ steps.pins.outputs.kimi_code_version }}" \ CCX_VERSION="${{ steps.pins.outputs.ccx_version }}" \ COPILOT_API_VERSION="${{ steps.pins.outputs.copilot_api_version }}" \ PLAYWRIGHT_VERSION="${{ steps.pins.outputs.playwright_version }}" \ @@ -127,6 +129,7 @@ jobs: deva.sh codex -Q -- --version deva.sh gemini -Q -- --version deva.sh grok -Q -- --version + deva.sh kimi -Q -- --version - name: Smoke Claude --chrome mount assembly shell: bash @@ -166,6 +169,14 @@ jobs: set -euo pipefail ./scripts/test-install-agent-tooling.sh + - name: Smoke kimi auth wiring + shell: bash + run: | + set -euo pipefail + DEVA_DOCKER_IMAGE=deva-smoke \ + DEVA_DOCKER_TAG=ci \ + ./scripts/test-kimi-auth.sh + - name: Smoke version targets shell: bash run: | diff --git a/.github/workflows/nightly-images.yml b/.github/workflows/nightly-images.yml index 6b4eb06..ee91900 100644 --- a/.github/workflows/nightly-images.yml +++ b/.github/workflows/nightly-images.yml @@ -55,6 +55,7 @@ jobs: 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 }} + kimi_code_version: ${{ steps.versions.outputs.kimi_code_version }} ccx_version: ${{ steps.versions.outputs.ccx_version }} copilot_api_version: ${{ steps.versions.outputs.copilot_api_version }} steps: @@ -82,6 +83,7 @@ jobs: - Codex: \`${{ steps.versions.outputs.codex_version }}\` - Gemini CLI: \`${{ steps.versions.outputs.gemini_cli_version }}\` - Grok CLI: \`${{ steps.versions.outputs.grok_cli_version }}\` + - Kimi Code: \`${{ steps.versions.outputs.kimi_code_version }}\` - ccx: \`${{ steps.versions.outputs.ccx_version }}\` - Copilot API: \`${{ steps.versions.outputs.copilot_api_version }}\` - Stamp: \`${{ steps.versions.outputs.stamp }}\` @@ -143,6 +145,7 @@ jobs: 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 }} + KIMI_CODE_VERSION=${{ needs.resolve-versions.outputs.kimi_code_version }} CCX_VERSION=${{ needs.resolve-versions.outputs.ccx_version }} COPILOT_API_VERSION=${{ needs.resolve-versions.outputs.copilot_api_version }} @@ -197,6 +200,7 @@ jobs: 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 }} + KIMI_CODE_VERSION=${{ needs.resolve-versions.outputs.kimi_code_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 2f989a3..78191b8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -51,6 +51,7 @@ jobs: 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 }} + kimi_code_version: ${{ steps.pins.outputs.kimi_code_version }} ccx_version: ${{ steps.pins.outputs.ccx_version }} copilot_api_version: ${{ steps.pins.outputs.copilot_api_version }} playwright_version: ${{ steps.pins.outputs.playwright_version }} @@ -86,6 +87,7 @@ jobs: - Codex: \`${{ steps.pins.outputs.codex_version }}\` - Gemini CLI: \`${{ steps.pins.outputs.gemini_cli_version }}\` - Grok CLI: \`${{ steps.pins.outputs.grok_cli_version }}\` + - Kimi Code: \`${{ steps.pins.outputs.kimi_code_version }}\` - ccx: \`${{ steps.pins.outputs.ccx_version }}\` - Copilot API: \`${{ steps.pins.outputs.copilot_api_version }}\` - Playwright: \`${{ steps.pins.outputs.playwright_version }}\` @@ -150,6 +152,7 @@ jobs: 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 }} + KIMI_CODE_VERSION=${{ needs.load-version-pins.outputs.kimi_code_version }} CCX_VERSION=${{ needs.load-version-pins.outputs.ccx_version }} COPILOT_API_VERSION=${{ needs.load-version-pins.outputs.copilot_api_version }} @@ -206,6 +209,7 @@ jobs: 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 }} + KIMI_CODE_VERSION=${{ needs.load-version-pins.outputs.kimi_code_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 ba1ff57..df70396 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- kimi agent (Moonshot Kimi Code CLI, #455): `deva.sh kimi` launches the + official `@moonshot-ai/kimi-code` CLI like claude/codex/gemini/grok. + `oauth` default (device-code `/login`, mounts `~/.kimi-code`) and + `api-key`. kimi reads no API key from the shell — its only shell channel + is the `KIMI_MODEL_*` family — so api-key mode maps `KIMI_CODE_API_KEY` + onto `KIMI_MODEL_{NAME,API_KEY,PROVIDER_TYPE,BASE_URL}` (default model + `k3`, coding endpoint `https://api.kimi.com/coding/v1`; override with + `DEVA_KIMI_MODEL` / `DEVA_KIMI_BASE_URL`). The key is synthesized + in-memory, never written to `config.toml`, so api-key mode mounts no + `~/.kimi-code` and nothing lands on disk. Unlike grok, kimi's npm bin is + a plain symlink (no self-update trampoline), so no binary-pinning guard + is needed. `KIMI_CODE_VERSION` pinned at 0.28.0 through versions.env, + Makefile, Dockerfile(+rust), version-pins/upgrade/update scripts, + ci/nightly/release workflows; tests added (release-utils registry, + version-upgrade mock, install-tooling, and `test-kimi-auth.sh`). + +### Fixed +- `--trace` launch killed by `cp: cannot create regular file + '/usr/local/share/ca-certificates/cctrace-mitm.crt': File exists` (#414): + the persistent-container flow runs the entrypoint's `setup_trace_ca` + concurrently — once in PID 1 (`docker run -d ... tail -f /dev/null` boot) + and once in the `docker exec` entrypoint that starts the agent — and GNU + cp creates the destination `O_EXCL`, so the loser of the race died under + `set -e`. The CA install is now serialized with `flock` and skips the + copy when the installed cert is already current. The first cut of the + flock subshell ended on `[ "$VERBOSE" = "true" ] && echo`, whose failed + test became the subshell's exit status and `set -e` killed every + non-verbose traced launch right after the banner; the subshell now + exits 0 explicitly. + ## [0.16.0] - 2026-07-14 ### Added diff --git a/DEV-LOGS.md b/DEV-LOGS.md index 0e253eb..422fa8b 100644 --- a/DEV-LOGS.md +++ b/DEV-LOGS.md @@ -13,6 +13,21 @@ - Minimal markdown markers, no unnecessary formatting, minimal emojis. - Reference issue numbers in the format `#` for easy linking. +# [2026-07-21] Dev Log: traced launches die silently in freshly built images #414 +- Why: after rebuilding images, every `--trace` launch died with "failed to launch ephemeral container" and no error. +- What: + - The #414 flock rework ended the CA-install subshell on `[ "$VERBOSE" = "true" ] && echo ...`; with VERBOSE unset the failed test became the subshell's exit status, and under `set -e` the entrypoint died right after the agent banner — silently, only in freshly built images (the entrypoint is baked). Fix: explicit `exit 0` before the subshell closes. Verified A/B: baked image dies, overlay-fixed image runs `deva.sh claude --trace --rm -Q -- --version` end-to-end (cctrace MITM up, trace file written). Rebuild images to pick this up. +- Result: traced launch verified live against the host daemon. + +# [2026-07-20] Dev Log: add kimi agent (Moonshot Kimi Code CLI) #455 +- Why: Moonshot shipped an official coding-agent CLI (@moonshot-ai/kimi-code, bin `kimi`); deva should launch it like claude/codex/gemini/grok. Traced the grok integration (commit 93a59e0) via ccx as the template. +- What: + - agents/kimi.sh: oauth default (mount ~/.kimi-code; device-code `kimi login`, no browser needed) + api-key; appends `--yolo` (auto-approve tool calls, still allows clarifying questions — not `--auto`) since the container is the sandbox. + - The auth divergence that broke the premise: kimi reads NO API key from the shell (docs: `export KIMI_API_KEY` does nothing; creds only from ~/.kimi-code/config.toml). The one shell channel is the `KIMI_MODEL_*` family, which synthesizes an in-memory provider. So api-key mode maps `KIMI_CODE_API_KEY` -> `KIMI_MODEL_{NAME=k3,API_KEY,PROVIDER_TYPE=kimi,BASE_URL=https://api.kimi.com/coding/v1}` (override via DEVA_KIMI_MODEL/DEVA_KIMI_BASE_URL). Verified end-to-end with a real sk-kim… key: the key auths only against api.kimi.com/coding/v1 (moonshot.ai rejects it), models k3/kimi-for-coding/highspeed, and the key is never persisted to config.toml -> no ~/.kimi-code mount in api-key mode, nothing on disk. + - deva.sh: canonical entry .kimi-code, autolink ~/.kimi-code, KIMI_* env filtering, config-home mkdir/warn/dispatch. Simpler than grok — no default_credential_target_path (env-only key), so no auth.json overlay; kimi's npm bin is a plain symlink to dist/main.mjs (no self-update trampoline, native/ only darwin+win32), so no pin_platform_binary and no update-guard tmpfs. + - KIMI_CODE_VERSION=0.28.0 pinned through versions.env, Makefile, Dockerfile(+rust), version-pins/upgrade/update scripts, release-utils TOOL_REGISTRY, ci/nightly/release workflows, install.sh + install-agent-tooling.sh. Tests: release-utils registry counts, version-upgrade mock (curl fixtures + outage label), install-tooling stub, and new scripts/test-kimi-auth.sh (12 assertions on the KIMI_MODEL_* wiring, redaction, naming, no-mount, overrides, missing-key error). +- Result: deva.sh kimi works in both auth modes (DEVA_NO_DOCKER dry-run + local `kimi -p` through KIMI_MODEL_* verified); full test suite green. Docker image build not verifiable in this container (no daemon); the fan-out is validated by the version tooling + assertions. NOTE: docs/authentication.md documents the unique no-shell-env auth model so users don't hit the same KIMI_CODE_API_KEY confusion. + # [2026-07-10] Dev Log: deva-tmux — tmux as a built-in module, both transports #412 - Why: tmux interaction was four loose scripts with zero deva.sh integration; #406 built the right transport (ssh) but deleted the socat fallback and stayed a loose script. Direction split matters: host->container (deva.sh shell) and agent<->agent (tmux-bridge) keep the sandbox; container->host breaks it and must be opt-in. - What: diff --git a/Dockerfile b/Dockerfile index 65a6925..96962b8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -212,12 +212,14 @@ ARG CLAUDE_CODE_VERSION=2.1.143 ARG CODEX_VERSION=0.131.0 ARG GEMINI_CLI_VERSION=0.42.0 ARG GROK_CLI_VERSION=0.2.93 +ARG KIMI_CODE_VERSION=0.28.0 # Record key tool versions as labels for quick inspection LABEL org.opencontainers.image.claude_code_version=${CLAUDE_CODE_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.kimi_code_version=${KIMI_CODE_VERSION} ARG CCX_VERSION=v0.7.0 diff --git a/Dockerfile.rust b/Dockerfile.rust index 8a34988..d10d48c 100644 --- a/Dockerfile.rust +++ b/Dockerfile.rust @@ -12,6 +12,7 @@ ARG CLAUDE_CODE_VERSION=2.1.143 ARG CODEX_VERSION=0.131.0 ARG GEMINI_CLI_VERSION=0.42.0 ARG GROK_CLI_VERSION=0.2.93 +ARG KIMI_CODE_VERSION=0.28.0 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.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.kimi_code_version=${KIMI_CODE_VERSION} LABEL org.opencontainers.image.ccx_version=${CCX_VERSION} LABEL org.opencontainers.image.playwright_version=${PLAYWRIGHT_VERSION} diff --git a/Makefile b/Makefile index 88a24bb..d3b8b9a 100644 --- a/Makefile +++ b/Makefile @@ -35,6 +35,7 @@ CCTRACE_VERSION ?= 0.16.0 CODEX_VERSION ?= 0.131.0 GEMINI_CLI_VERSION ?= 0.42.0 GROK_CLI_VERSION ?= 0.2.93 +KIMI_CODE_VERSION ?= 0.28.0 CCX_VERSION ?= v0.1.4 COPILOT_API_VERSION ?= 0ea08febdd7e3e055b03dd298bf57e669500b5c1 PLAYWRIGHT_VERSION ?= 1.60.0 @@ -84,6 +85,7 @@ AGENT_BUILD_ARGS := \ --build-arg CODEX_VERSION=$(CODEX_VERSION) \ --build-arg GEMINI_CLI_VERSION=$(GEMINI_CLI_VERSION) \ --build-arg GROK_CLI_VERSION=$(GROK_CLI_VERSION) \ + --build-arg KIMI_CODE_VERSION=$(KIMI_CODE_VERSION) \ --build-arg CCX_VERSION=$(CCX_VERSION) MAIN_BUILD_ARGS := $(TOOLCHAIN_BUILD_ARGS) $(AGENT_BUILD_ARGS) \ @@ -107,6 +109,7 @@ VERSION_QUERY_OVERRIDES := \ $(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 KIMI_CODE_VERSION)),KIMI_CODE_VERSION=$(KIMI_CODE_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)) \ @@ -150,14 +153,16 @@ build-main: build-network-check 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); \ + prev_kimi=$$(docker inspect --format='{{ index .Config.Labels "org.opencontainers.image.kimi_code_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"); 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 \ + curC=$$(fmt "$$prev_claude"); curX=$$(fmt "$$prev_codex"); curG=$$(fmt "$$prev_gemini"); curK=$$(fmt "$$prev_grok"); curKi=$$(fmt "$$prev_kimi"); \ + tgtC=$$(fmt "$(CLAUDE_CODE_VERSION)"); tgtX=$$(fmt "$(CODEX_VERSION)"); tgtG=$$(fmt "$(GEMINI_CLI_VERSION)"); tgtK=$$(fmt "$(GROK_CLI_VERSION)"); tgtKi=$$(fmt "$(KIMI_CODE_VERSION)"); \ + if [ "$$curC" = "$$tgtC" ] && [ "$$curX" = "$$tgtX" ] && [ "$$curG" = "$$tgtG" ] && [ "$$curK" = "$$tgtK" ] && [ "$$curKi" = "$$tgtKi" ]; then \ echo "Claude: $$tgtC (no change)"; \ echo "Codex: $$tgtX (no change)"; \ echo "Gemini: $$tgtG (no change)"; \ echo "Grok: $$tgtK (no change)"; \ + echo "Kimi: $$tgtKi (no change)"; \ echo "Already up-to-date"; \ else \ if [ "$$curC" = "$$tgtC" ]; then \ @@ -180,6 +185,11 @@ build-main: build-network-check else \ echo "Grok: $$curK -> $$tgtK"; \ fi; \ + if [ "$$curKi" = "$$tgtKi" ]; then \ + echo "Kimi: $$tgtKi (no change)"; \ + else \ + echo "Kimi: $$curKi -> $$tgtKi"; \ + fi; \ fi @echo "Hint: override via GO_VERSION=... CLAUDE_CODE_VERSION=... or run 'make versions-pin'" docker build $(DOCKER_BUILD_FLAGS) -f $(DOCKERFILE) $(MAIN_BUILD_ARGS) -t $(MAIN_IMAGE) . @@ -439,6 +449,7 @@ help: @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 " KIMI_CODE_VERSION Kimi Code CLI version (default: $(KIMI_CODE_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 d59c447..041f90b 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,11 @@ [![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%20%7C%20grok-222222)](#what-this-is) +[![Agents](https://img.shields.io/badge/agents-codex%20%7C%20claude%20%7C%20gemini%20%7C%20grok%20%7C%20kimi-222222)](#what-this-is) AI agents / LLMs: read [llms.txt](llms.txt) — the whole product in one fetch. -Run Codex, Claude Code, Gemini, and Grok inside Docker without pretending the agent's own sandbox is the thing keeping you safe. +Run Codex, Claude Code, Gemini, Grok, and Kimi 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. @@ -18,7 +18,7 @@ This repo is the source of truth for `deva.sh`. ## What This Is -- a Docker-based launcher for Codex, Claude, Gemini, and Grok +- a Docker-based launcher for Codex, Claude, Gemini, Grok, and Kimi - 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/` @@ -56,7 +56,7 @@ deva.sh ps deva.sh stop ``` -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. +If you already use Codex, Claude, Gemini, Grok, or Kimi 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 @@ -118,6 +118,7 @@ deva.sh deva.sh codex deva.sh gemini deva.sh grok +deva.sh kimi # Throwaway run deva.sh claude --rm diff --git a/agents/codex.sh b/agents/codex.sh index 2c64548..fab2322 100644 --- a/agents/codex.sh +++ b/agents/codex.sh @@ -33,24 +33,9 @@ agent_prepare() { AUTH_METHOD="$PARSED_AUTH_METHOD" local -a remaining_args=("${PARSED_REMAINING_ARGS[@]+"${PARSED_REMAINING_ARGS[@]}"}") - # Detect --trace flag — only before the -- sentinel; after it, args - # belong to the agent CLI verbatim (#427). First -- is stripped. - local use_trace=false - local seen_sep=false - if [ ${#remaining_args[@]} -gt 0 ]; then - local -a filtered_args=() - local arg - for arg in "${remaining_args[@]}"; do - if [ "$arg" = "--" ] && [ "$seen_sep" = false ]; then - seen_sep=true - elif [ "$arg" = "--trace" ] && [ "$seen_sep" = false ]; then - use_trace=true - else - filtered_args+=("$arg") - fi - done - remaining_args=("${filtered_args[@]+"${filtered_args[@]}"}") - fi + filter_trace_flag "${remaining_args[@]+"${remaining_args[@]}"}" + local use_trace="$TRACE_FLAG_PRESENT" + remaining_args=("${TRACE_FILTERED_ARGS[@]+"${TRACE_FILTERED_ARGS[@]}"}") local has_dangerous=false local has_model=false diff --git a/agents/grok.sh b/agents/grok.sh index 42ec762..db2e736 100644 --- a/agents/grok.sh +++ b/agents/grok.sh @@ -18,24 +18,9 @@ agent_prepare() { AUTH_METHOD="$PARSED_AUTH_METHOD" local -a remaining_args=("${PARSED_REMAINING_ARGS[@]+"${PARSED_REMAINING_ARGS[@]}"}") - # Detect --trace flag — only before the -- sentinel; after it, args - # belong to the agent CLI verbatim (#427). First -- is stripped. - local use_trace=false - local seen_sep=false - if [ ${#remaining_args[@]} -gt 0 ]; then - local -a filtered_args=() - local arg - for arg in "${remaining_args[@]}"; do - if [ "$arg" = "--" ] && [ "$seen_sep" = false ]; then - seen_sep=true - elif [ "$arg" = "--trace" ] && [ "$seen_sep" = false ]; then - use_trace=true - else - filtered_args+=("$arg") - fi - done - remaining_args=("${filtered_args[@]+"${filtered_args[@]}"}") - fi + filter_trace_flag "${remaining_args[@]+"${remaining_args[@]}"}" + local use_trace="$TRACE_FLAG_PRESENT" + remaining_args=("${TRACE_FILTERED_ARGS[@]+"${TRACE_FILTERED_ARGS[@]}"}") AGENT_COMMAND+=("--always-approve") diff --git a/agents/kimi.sh b/agents/kimi.sh new file mode 100644 index 0000000..fd45f9f --- /dev/null +++ b/agents/kimi.sh @@ -0,0 +1,93 @@ +# 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=("kimi") + + parse_auth_args "kimi" "${args[@]+"${args[@]}"}" + AUTH_METHOD="$PARSED_AUTH_METHOD" + local -a remaining_args=("${PARSED_REMAINING_ARGS[@]+"${PARSED_REMAINING_ARGS[@]}"}") + + filter_trace_flag "${remaining_args[@]+"${remaining_args[@]}"}" + local use_trace="$TRACE_FLAG_PRESENT" + remaining_args=("${TRACE_FILTERED_ARGS[@]+"${TRACE_FILTERED_ARGS[@]}"}") + + # Container is the sandbox: auto-approve tool calls. --yolo still lets the + # agent ask clarifying questions (unlike --auto), so interactive use works. + AGENT_COMMAND+=("--yolo") + + AGENT_COMMAND+=("${remaining_args[@]+"${remaining_args[@]}"}") + + if [ "$use_trace" = true ]; then + # cctrace kimi profile: kimi args go after "--"; always mitm. + # DEVA_TRACE=1 installs the MITM CA into the container store (#414). + DOCKER_ARGS+=("-e" "DEVA_TRACE=1") + DEVA_TRACE_ACTIVE=true + setup_trace_ui_port + AGENT_COMMAND=("cctrace" "kimi" "--no-open" "--" "${AGENT_COMMAND[@]:1}") + fi + + setup_kimi_auth "$AUTH_METHOD" +} + +setup_kimi_auth() { + local method="$1" + + case "$method" in + oauth) + AUTH_DETAILS="oauth (~/.kimi-code)" + # First login inside a container has no browser: run + # `kimi login` in the session (device-code flow, enter the code + # on any device), or log in on the host once and let the mount + # carry ~/.kimi-code. + # Only mount host ~/.kimi-code 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/.kimi-code" ]; then + DOCKER_ARGS+=("-v" "$HOME/.kimi-code:/home/deva/.kimi-code") + else + echo "Warning: ~/.kimi-code directory not found, creating it" >&2 + mkdir -p "$HOME/.kimi-code" + DOCKER_ARGS+=("-v" "$HOME/.kimi-code:/home/deva/.kimi-code") + fi + fi + ;; + api-key) + # kimi reads NO credential from the shell env (its docs are explicit: + # `export KIMI_API_KEY` does nothing). The one shell channel is the + # KIMI_MODEL_* family, which synthesizes an in-memory provider. The + # key is never written to config.toml, so no ~/.kimi-code mount is + # needed and the key never lands on disk. + local key="${KIMI_CODE_API_KEY:-${KIMI_API_KEY:-}}" + if [ -z "$key" ]; then + auth_error "KIMI_CODE_API_KEY not set for --auth-with api-key" \ + "Set: export KIMI_CODE_API_KEY=your_key (from https://platform.kimi.com)" + fi + + # sk-kim… coding keys authenticate against the Kimi Code coding + # endpoint, not moonshot.ai. Override both via DEVA_KIMI_* for a + # direct Moonshot key (base_url https://api.moonshot.ai/v1). + local model="${DEVA_KIMI_MODEL:-k3}" + local base_url="${DEVA_KIMI_BASE_URL:-https://api.kimi.com/coding/v1}" + + AUTH_DETAILS="api-key (KIMI_CODE_API_KEY -> KIMI_MODEL_*, model=$model)" + DOCKER_ARGS+=("-e" "KIMI_MODEL_NAME=$model") + DOCKER_ARGS+=("-e" "KIMI_MODEL_API_KEY=$key") + DOCKER_ARGS+=("-e" "KIMI_MODEL_PROVIDER_TYPE=kimi") + DOCKER_ARGS+=("-e" "KIMI_MODEL_BASE_URL=$base_url") + ;; + *) + auth_error "auth method '$method' not implemented for kimi" + ;; + esac +} diff --git a/agents/shared_auth.sh b/agents/shared_auth.sh index a115c4d..5b0106f 100644 --- a/agents/shared_auth.sh +++ b/agents/shared_auth.sh @@ -226,6 +226,29 @@ is_file_path() { [[ "$arg" == /* ]] || [[ "$arg" == ~* ]] || [[ "$arg" == ./* ]] || [[ "$arg" == ../* ]] || [[ "$arg" == *.json ]] } +# Detect and strip a --trace flag — only before the -- sentinel; after it, +# args belong to the agent CLI verbatim (#427). First -- is stripped. +# Sets TRACE_FLAG_PRESENT (true/false) and TRACE_FILTERED_ARGS. +# Shared by the agents whose only deva-side flag is --trace (codex, grok, +# kimi); claude.sh scans more flags and keeps its own loop. +TRACE_FLAG_PRESENT=false +TRACE_FILTERED_ARGS=() +filter_trace_flag() { + TRACE_FLAG_PRESENT=false + TRACE_FILTERED_ARGS=() + local seen_sep=false + local arg + for arg in "$@"; do + if [ "$arg" = "--" ] && [ "$seen_sep" = false ]; then + seen_sep=true + elif [ "$arg" = "--trace" ] && [ "$seen_sep" = false ]; then + TRACE_FLAG_PRESENT=true + else + TRACE_FILTERED_ARGS+=("$arg") + fi + done +} + # Publish the cctrace live UI (container port 9317, binds 0.0.0.0) to the # host loopback so the browser can reach it. Probe host ports from 9317 so # concurrent traced containers land on predictable neighbors (#425). @@ -439,6 +462,9 @@ parse_auth_args() { grok) supported_methods=(oauth api-key) ;; + kimi) + supported_methods=(oauth api-key) + ;; *) auth_error "Unknown agent: $agent_name" ;; @@ -505,6 +531,7 @@ parse_auth_args() { codex) auth_method="chatgpt" ;; gemini) auth_method="oauth" ;; grok) auth_method="oauth" ;; + kimi) auth_method="oauth" ;; esac fi diff --git a/deva.sh b/deva.sh index f23f062..fe8050d 100755 --- a/deva.sh +++ b/deva.sh @@ -101,7 +101,7 @@ _step() { usage() { cat <<'USAGE' -deva.sh - Docker-based multi-agent launcher (Claude, Codex, Gemini, Grok) +deva.sh - Docker-based multi-agent launcher (Claude, Codex, Gemini, Grok, Kimi) Usage: deva.sh [deva flags] [agent] [-- agent-flags] @@ -196,6 +196,7 @@ Examples: 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 kimi # Launch kimi (oauth default; api-key via KIMI_CODE_API_KEY) deva.sh claude --rm # Ephemeral: deva-work-myapp-claude-12345 # Container management (current project) @@ -213,6 +214,7 @@ Examples: Advanced: deva.sh codex -v ~/.ssh:/home/deva/.ssh:ro -- -m gpt-5-codex deva.sh claude --trace -- --continue # Trace requests with cctrace + deva.sh kimi --trace # Trace kimi requests with cctrace deva.sh --show-config # Debug configuration deva.sh --no-docker claude # Disable Docker-in-Docker auto-mount USAGE @@ -916,7 +918,7 @@ generate_auth_tag() { fi case "$agent:$auth_method" in - claude:claude|codex:chatgpt|gemini:oauth|gemini:gemini-app-oauth|grok:oauth) + claude:claude|codex:chatgpt|gemini:oauth|gemini:gemini-app-oauth|grok:oauth|kimi:oauth) printf '%s' "auth-default" return ;; @@ -939,6 +941,7 @@ generate_auth_tag() { codex) key_val="${OPENAI_API_KEY:-}" ;; gemini) key_val="${GEMINI_API_KEY:-${GOOGLE_API_KEY:-}}" ;; grok) key_val="${XAI_API_KEY:-}" ;; + kimi) key_val="${KIMI_CODE_API_KEY:-${KIMI_API_KEY:-}}" ;; esac if [ -n "$key_val" ] && [ ${#key_val} -ge 4 ]; then printf '%s' "api-key-${key_val: -4}" @@ -984,6 +987,7 @@ agent_version_tag() { codex) label="org.opencontainers.image.codex_version" ;; gemini) label="org.opencontainers.image.gemini_cli_version" ;; grok) label="org.opencontainers.image.grok_cli_version" ;; + kimi) label="org.opencontainers.image.kimi_code_version" ;; esac local ver="" @@ -1329,7 +1333,7 @@ categorize_mount() { 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/.grok* ]] || \ - [ "$dest" = "/home/deva/.agents" ]; then + [[ "$dest" == /home/deva/.kimi-code* ]] || [ "$dest" = "/home/deva/.agents" ]; then printf 'config' else printf 'user' fi @@ -1712,7 +1716,7 @@ cmd_status() { if [ -d "$config_root" ]; then echo "Agent Homes ($(shorten_path "$config_root")):" - for agent_name in claude codex gemini grok; do + for agent_name in claude codex gemini grok kimi; do local agent_dir="$config_root/$agent_name" if [ -d "$agent_dir" ]; then local canonical="" other_count=0 entry is_canonical @@ -1724,6 +1728,7 @@ cmd_status() { codex:.codex) is_canonical=true ;; gemini:.gemini) is_canonical=true ;; grok:.grok) is_canonical=true ;; + kimi:.kimi-code) is_canonical=true ;; esac if [ "$is_canonical" = true ]; then if [ -L "$agent_dir/$entry" ]; then @@ -2192,6 +2197,16 @@ should_skip_env_for_auth() { ;; esac ;; + kimi) + # kimi ignores shell creds except the KIMI_MODEL_* family, which deva + # injects itself in api-key mode. Drop any host-set KIMI_* auth vars so + # they can't hijack the managed OAuth provider or override our wiring. + case "$name" in + KIMI_CODE_API_KEY | KIMI_API_KEY | KIMI_MODEL_NAME | KIMI_MODEL_API_KEY | KIMI_MODEL_PROVIDER_TYPE | KIMI_MODEL_BASE_URL) + return 0 + ;; + esac + ;; esac return 1 @@ -2249,6 +2264,7 @@ agent_canonical_basenames() { codex) printf '%s\n' '.codex' ;; gemini) printf '%s\n' '.gemini' ;; grok) printf '%s\n' '.grok' ;; + kimi) printf '%s\n' '.kimi-code' ;; *) return 0 ;; esac } @@ -2262,6 +2278,14 @@ grok_api_key_no_mount() { [ "$ACTIVE_AGENT" = "grok" ] && [ "${AUTH_METHOD:-}" = "api-key" ] } +# kimi api-key contract: the key travels as KIMI_MODEL_* env only — kimi +# reads no credential from the shell and the synth key is never written to +# config.toml. A mounted ~/.kimi-code would put its config.toml providers +# above the env-synth model, so api-key mode mounts nothing. +kimi_api_key_no_mount() { + [ "$ACTIVE_AGENT" = "kimi" ] && [ "${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 @@ -2296,6 +2320,9 @@ mount_agent_canonical() { if [ "$agent" = "grok" ] && grok_api_key_no_mount; then return 0 fi + if [ "$agent" = "kimi" ] && kimi_api_key_no_mount; then + return 0 + fi local entry src while IFS= read -r entry; do @@ -2325,7 +2352,7 @@ mount_config_home() { mount_agent_canonical "$ACTIVE_AGENT" "$CONFIG_HOME" } -# Effective config base: where agent config dirs (.claude/, .codex/, .gemini/, .grok/) live. +# Effective config base: where agent config dirs (.claude/, .codex/, .gemini/, .grok/, .kimi-code/) live. resolve_config_base() { if [ -n "$CONFIG_HOME" ]; then printf '%s' "$CONFIG_HOME" @@ -2352,7 +2379,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|grok:oauth) ;; + claude:claude|codex:chatgpt|gemini:oauth|gemini:gemini-app-oauth|grok:oauth|kimi:oauth) ;; *) return 0 ;; esac fi @@ -2364,6 +2391,7 @@ has_auth_override() { codex) auth_vars="OPENAI_API_KEY" ;; gemini) auth_vars="GEMINI_API_KEY" ;; grok) auth_vars="XAI_API_KEY" ;; + kimi) auth_vars="KIMI_CODE_API_KEY KIMI_API_KEY" ;; esac local var @@ -3572,7 +3600,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" ] || [ -d "$CONFIG_HOME/grok" ]; then + if [ -d "$CONFIG_HOME/claude" ] || [ -d "$CONFIG_HOME/codex" ] || [ -d "$CONFIG_HOME/gemini" ] || [ -d "$CONFIG_HOME/grok" ] || [ -d "$CONFIG_HOME/kimi" ]; then CONFIG_ROOT="$CONFIG_HOME" CONFIG_HOME="" CONFIG_HOME_AUTO=false @@ -3634,6 +3662,17 @@ autolink_legacy_into_deva_root() { if [ -d "$CONFIG_ROOT" ]; then [ -d "$CONFIG_ROOT/grok/.grok" ] || [ -L "$CONFIG_ROOT/grok/.grok" ] || mkdir -p "$CONFIG_ROOT/grok/.grok" fi + + if [ -d "$HOME/.kimi-code" ]; then + [ -d "$CONFIG_ROOT/kimi" ] || mkdir -p "$CONFIG_ROOT/kimi" + if [ ! -e "$CONFIG_ROOT/kimi/.kimi-code" ] && [ ! -L "$CONFIG_ROOT/kimi/.kimi-code" ]; then + ln -s "$HOME/.kimi-code" "$CONFIG_ROOT/kimi/.kimi-code" + echo "autolink: ~/.kimi-code -> $CONFIG_ROOT/kimi/.kimi-code" >&2 + fi + fi + if [ -d "$CONFIG_ROOT" ]; then + [ -d "$CONFIG_ROOT/kimi/.kimi-code" ] || [ -L "$CONFIG_ROOT/kimi/.kimi-code" ] || mkdir -p "$CONFIG_ROOT/kimi/.kimi-code" + fi } check_agent "$ACTIVE_AGENT" @@ -3661,6 +3700,9 @@ if [ -n "$CONFIG_HOME" ] && [ "$DRY_RUN" != true ]; then grok) [ -d "$CONFIG_HOME/.grok" ] || mkdir -p "$CONFIG_HOME/.grok" ;; + kimi) + [ -d "$CONFIG_HOME/.kimi-code" ] || mkdir -p "$CONFIG_HOME/.kimi-code" + ;; esac fi @@ -3701,6 +3743,11 @@ if [ "$CONFIG_HOME_FROM_CLI" = true ] && [ -n "$CONFIG_HOME" ] && [ "$_config_ho echo "warning: $CONFIG_HOME/.grok is empty; authentication will need to be set up" >&2 fi ;; + kimi) + if [ ! -d "$CONFIG_HOME/.kimi-code" ] || [ -z "$(ls -A "$CONFIG_HOME/.kimi-code" 2>/dev/null)" ]; then + echo "warning: $CONFIG_HOME/.kimi-code is empty; authentication will need to be set up" >&2 + fi + ;; esac fi @@ -3735,7 +3782,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|grok:oauth) ;; + claude:claude|codex:chatgpt|gemini:oauth|gemini:gemini-app-oauth|grok:oauth|kimi:oauth) ;; *) _needs_rewrite=true ;; esac @@ -3745,6 +3792,12 @@ _step "agent_prepare" fi fi + # --trace changes port mappings; the container must have a distinct name + # so a traced run never reattaches to a non-traced container (#414). + if [ "${DEVA_TRACE_ACTIVE:-false}" = true ]; then + _needs_rewrite=true + fi + if [ "$_needs_rewrite" = true ]; then _rw_slug="$(generate_container_slug)" @@ -3758,7 +3811,9 @@ _step "agent_prepare" [ -z "$_rw_cfg_input" ] && [ -n "$CONFIG_ROOT" ] && _rw_cfg_input="$CONFIG_ROOT" fi - _rw_shape=$(compute_shape_hash "$(docker_image_ref)" "$_rw_vol_input" "$_rw_cfg_input") + _rw_trace_input="" + [ "${DEVA_TRACE_ACTIVE:-false}" = true ] && _rw_trace_input="trace" + _rw_shape=$(compute_shape_hash "$(docker_image_ref)" "$_rw_vol_input" "${_rw_cfg_input}${_rw_trace_input:+|${_rw_trace_input}}") _rw_auth_tag=$(generate_auth_tag "$ACTIVE_AGENT" "$AUTH_METHOD" "${CUSTOM_CREDENTIALS_FILE:-}" "$_env_auth_override") _rw_name=$(build_container_name \ @@ -3835,6 +3890,10 @@ else if [ -d "$HOME/.grok" ] && ! grok_api_key_no_mount; then DOCKER_ARGS+=("-v" "$HOME/.grok:/home/deva/.grok") fi + # kimi api-key uses KIMI_MODEL_* env (no mount); only oauth wants ~/.kimi-code. + if [ -d "$HOME/.kimi-code" ] && ! kimi_api_key_no_mount; then + DOCKER_ARGS+=("-v" "$HOME/.kimi-code:/home/deva/.kimi-code") + fi fi fi _step "mount dispatch: done" diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 0ee579f..53fa988 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -355,22 +355,45 @@ setup_trace_ca() { return 0 fi - # --print-ca generates the CA on first use; it must run as the deva user - # so the key lands under $DEVA_HOME/.local/share/cctrace with sane ownership. - local ca_path - ca_path=$(gosu "$DEVA_USER" env "HOME=$DEVA_HOME" "PATH=$PATH" cctrace --print-ca 2>/dev/null | tail -1) - if [ -z "$ca_path" ] || [ ! -f "$ca_path" ]; then - echo "[entrypoint] warning: cctrace --print-ca gave no usable path; skipping CA trust" >&2 - return 0 - fi + # The persistent-container flow runs this concurrently in PID 1 + # (`docker run -d ... tail -f /dev/null` boot) and in the `docker exec` + # entrypoint that launches the agent. A bare cp loses that race: cp + # creates the destination O_EXCL, and the loser dies under set -e with + # "cannot create regular file '$crt': File exists" (#414). Serialize the + # whole install on a lock; the second entrant skips the copy when the + # installed cert is already current. + local lock=/run/lock/deva-trace-ca.lock + ( + if ! flock -x -w 60 9; then + echo "[entrypoint] warning: trace CA lock timeout; skipping container-wide CA trust (cctrace env-scoped trust still applies)" >&2 + exit 0 + fi - cp "$ca_path" "$crt" - chmod 644 "$crt" - if update-ca-certificates >/dev/null 2>&1; then - [ "$VERBOSE" = "true" ] && echo "[entrypoint] cctrace MITM CA trusted container-wide: $crt" - else - echo "[entrypoint] warning: trust-store update failed; container-wide CA trust incomplete (cctrace env-scoped trust still applies)" >&2 - fi + # --print-ca generates the CA on first use; it must run as the deva user + # so the key lands under $DEVA_HOME/.local/share/cctrace with sane ownership. + local ca_path + ca_path=$(gosu "$DEVA_USER" env "HOME=$DEVA_HOME" "PATH=$PATH" cctrace --print-ca 2>/dev/null | tail -1) + if [ -z "$ca_path" ] || [ ! -f "$ca_path" ]; then + echo "[entrypoint] warning: cctrace --print-ca gave no usable path; skipping CA trust" >&2 + exit 0 + fi + + if [ ! -f "$crt" ] || ! cmp -s "$ca_path" "$crt"; then + cp "$ca_path" "$crt" + chmod 644 "$crt" + fi + if update-ca-certificates >/dev/null 2>&1; then + if [ "$VERBOSE" = "true" ]; then + echo "[entrypoint] cctrace MITM CA trusted container-wide: $crt" + fi + else + echo "[entrypoint] warning: trust-store update failed; container-wide CA trust incomplete (cctrace env-scoped trust still applies)" >&2 + fi + # Explicit success: the subshell's exit status is this script's life + # under set -e — a failed `[ VERBOSE ] && echo` here killed every + # traced launch with no output. + exit 0 + ) 9>"$lock" return 0 } diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index 5f19c35..4f34c07 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -48,7 +48,8 @@ Deva-root layout: ├── claude/ ├── codex/ ├── gemini/ -└── grok/ +├── grok/ +└── kimi/ ``` ```bash @@ -175,8 +176,8 @@ verbatim and is never intercepted by deva. `--trace` wraps the agent with [cctrace](https://github.com/thevibeworks/cctrace), which records every API call the agent makes — messages, OAuth, usage/credits, MCP — not just the chat endpoint. Everything else on the line goes to the -agent unchanged. Codex and Grok use cctrace client profiles (cctrace >= 0.11) -and always run MITM capture. +agent unchanged. Codex, Grok, and Kimi use cctrace client profiles +(cctrace >= 0.11) and always run MITM capture. When tracing is on, the entrypoint installs the cctrace MITM CA into the container's system trust store (`update-ca-certificates`) so subprocesses and diff --git a/docs/authentication.md b/docs/authentication.md index 1684340..d4ee6ef 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -21,6 +21,7 @@ This guide documents what `deva.sh` actually supports, what env vars it reads, a | 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` | +| Kimi | `oauth` | `api-key` | `.kimi-code` (device-code), `KIMI_CODE_API_KEY` -> `KIMI_MODEL_*` | ## Claude @@ -356,6 +357,60 @@ 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. +## Kimi + +### Default: `--auth-with oauth` + +Mounts: + +- `/home/deva/.kimi-code` + +Kimi Code stores config, sessions, and its OAuth token under `~/.kimi-code` +(overridable via `KIMI_CODE_HOME`). First login has no browser, so run +the device-code flow inside the container: + +- run `kimi` then `/login` (or `kimi login`): it prints a URL + code; open + it on any device to authorize, and +- authenticate on the host once; deva auto-links `~/.kimi-code` and the + mount carries the token in. + +### `--auth-with api-key` + +Inputs: + +- `KIMI_CODE_API_KEY` (from [platform.kimi.com](https://platform.kimi.com)); + `KIMI_API_KEY` is accepted as a fallback when `KIMI_CODE_API_KEY` is unset + +Kimi is the odd one out: it reads **no** API key from the shell environment. +Its docs are explicit — `export KIMI_API_KEY=...` gives no provider its key; +credentials come only from `~/.kimi-code/config.toml`. The single exception +is the `KIMI_MODEL_*` family, which reads the shell and synthesizes an +in-memory provider. So deva maps your `KIMI_CODE_API_KEY` onto that channel: + +```text +KIMI_MODEL_NAME=k3 # DEVA_KIMI_MODEL to override +KIMI_MODEL_API_KEY=$KIMI_CODE_API_KEY +KIMI_MODEL_PROVIDER_TYPE=kimi +KIMI_MODEL_BASE_URL=https://api.kimi.com/coding/v1 # DEVA_KIMI_BASE_URL to override +``` + +The key is never written to `config.toml` (it lives in memory), so this mode +mounts no `~/.kimi-code` and nothing lands on disk. `sk-kim…` keys hit the +Kimi Code coding endpoint above; for a direct Moonshot key +(`platform.moonshot.ai`) set `DEVA_KIMI_BASE_URL=https://api.moonshot.ai/v1`. + +```bash +export KIMI_CODE_API_KEY=... +deva.sh kimi --auth-with api-key +# pick a different model: +DEVA_KIMI_MODEL=kimi-for-coding deva.sh kimi --auth-with api-key +``` + +Unlike grok, kimi's npm bin is a plain symlink to `dist/main.mjs` (no +self-update trampoline, no platform binary), so there is no host-mount +shadowing to guard against. The image pin (`KIMI_CODE_VERSION`) is the only +version that matters. + ## Config Homes And Auth Isolation Default homes live under: @@ -365,6 +420,7 @@ Default homes live under: ~/.config/deva/codex ~/.config/deva/gemini ~/.config/deva/grok +~/.config/deva/kimi ``` Use `--config-home` when you want a separate identity: diff --git a/docs/how-it-works.md b/docs/how-it-works.md index 085fba1..f13a524 100644 --- a/docs/how-it-works.md +++ b/docs/how-it-works.md @@ -62,13 +62,14 @@ Default per-agent homes live under: ├── claude/ ├── codex/ ├── gemini/ -└── grok/ +├── grok/ +└── kimi/ ``` `--config-home` supports two layouts: -- leaf home: `DIR/.claude`, `DIR/.claude.json`, `DIR/.codex`, `DIR/.gemini`, `DIR/.grok` -- deva root: `DIR/claude`, `DIR/codex`, `DIR/gemini`, `DIR/grok` +- leaf home: `DIR/.claude`, `DIR/.claude.json`, `DIR/.codex`, `DIR/.gemini`, `DIR/.grok`, `DIR/.kimi-code` +- deva root: `DIR/claude`, `DIR/codex`, `DIR/gemini`, `DIR/grok`, `DIR/kimi` `-Q` disables config-home resolution, autolink, and host config mounts entirely. @@ -83,6 +84,7 @@ Examples: - Codex default: `.codex/auth.json` - Gemini default: `.gemini` - Grok default: `.grok/auth.json` +- Kimi default: `.kimi-code` (device-code OAuth); api-key maps `KIMI_CODE_API_KEY` onto `KIMI_MODEL_*` 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. @@ -114,7 +116,7 @@ Persistent is default: - one default container shape per project - reused across runs -- same workspace can run Claude, Codex, Gemini, and Grok in the same container when mounts, config, and auth line up +- same workspace can run Claude, Codex, Gemini, Grok, and Kimi in the same container when mounts, config, and auth line up - different volumes, explicit config homes, or auth modes create separate persistent containers Ephemeral with `--rm`: diff --git a/docs/index.md b/docs/index.md index 0f18986..91498ea 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,6 @@ # deva.sh -Run Codex, Claude Code, Gemini, and Grok inside Docker without pretending +Run Codex, Claude Code, Gemini, Grok, and Kimi 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. @@ -24,7 +24,7 @@ If you want the internals instead of vague hand-waving: ## What This Is -- a Docker-based launcher for Codex, Claude, Gemini, and Grok +- a Docker-based launcher for Codex, Claude, Gemini, Grok, and Kimi - 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/` diff --git a/docs/philosophy.md b/docs/philosophy.md index 81d5e49..2a808bf 100644 --- a/docs/philosophy.md +++ b/docs/philosophy.md @@ -39,7 +39,7 @@ Persistent per-project containers mean: - warm package caches - stateful shell history and scratch space -- fast switching between Claude, Codex, Gemini, and Grok +- fast switching between Claude, Codex, Gemini, Grok, and Kimi `--rm` still exists. It just is not the default because the default should serve real work instead of screenshots. diff --git a/docs/quick-start.md b/docs/quick-start.md index fd84475..3b66a1e 100644 --- a/docs/quick-start.md +++ b/docs/quick-start.md @@ -27,6 +27,7 @@ That installs: - `agents/codex.sh` - `agents/gemini.sh` - `agents/grok.sh` +- `agents/kimi.sh` - `agents/shared_auth.sh` It also pulls `ghcr.io/thevibeworks/deva:latest`, with Docker Hub as fallback. @@ -75,6 +76,7 @@ Same project, same default container shape: deva.sh claude deva.sh gemini deva.sh grok +deva.sh kimi ``` That is one of the main reasons this wrapper exists. You do not need a separate pet workflow for every vendor. @@ -119,6 +121,14 @@ export XAI_API_KEY=xai-... deva.sh grok --auth-with api-key ``` +Kimi with a Kimi Code API key (deva maps it onto the `KIMI_MODEL_*` channel; +default model `k3`, coding endpoint): + +```bash +export KIMI_CODE_API_KEY=sk-... +deva.sh kimi --auth-with api-key +``` + More auth details live in [Authentication Guide](authentication.md). ## Useful Modes diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index e88a41a..d0d4883 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -84,7 +84,7 @@ If the dry-run shape is correct but the agent still cannot authenticate, the wra Symptom: -- first run warns that `.claude`, `.codex`, `.gemini`, or `.grok` is empty +- first run warns that `.claude`, `.codex`, `.gemini`, `.grok`, or `.kimi-code` is empty Meaning: diff --git a/install.sh b/install.sh index ec910d5..6c02fcc 100644 --- a/install.sh +++ b/install.sh @@ -11,6 +11,7 @@ agent_files=( "codex.sh" "gemini.sh" "grok.sh" + "kimi.sh" "shared_auth.sh" ) @@ -140,6 +141,7 @@ 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/kimi.sh" echo " - $INSTALL_DIR/agents/shared_auth.sh" echo "" echo "Quick start:" @@ -150,6 +152,7 @@ echo " deva.sh codex" echo " deva.sh claude -- --help" echo " deva.sh gemini -- --help" echo " deva.sh grok -- --help" +echo " deva.sh kimi -- --help" echo " deva.sh shell" echo "" echo "warning: do not point deva at your real home directory with dangerous permissions enabled" diff --git a/llms.txt b/llms.txt index 665d57a..5271e1a 100644 --- a/llms.txt +++ b/llms.txt @@ -1,8 +1,8 @@ # deva.sh -> Docker-first launcher for AI coding agents — Claude Code, Codex, Gemini, and Grok. +> Docker-first launcher for AI coding agents — Claude Code, Codex, Gemini, Grok, and Kimi. > The container is the sandbox, mounts are the explicit contract, and one warm -> project container serves all four agents. A bash script, not a framework. MIT. +> project container serves all five agents. A bash script, not a framework. MIT. deva.sh is a single entry point installed on the host; agents run inside `ghcr.io/thevibeworks/deva` images (`:latest`, `:rust` for stable, `:nightly`, @@ -10,7 +10,7 @@ deva.sh is a single entry point installed on the host; agents run inside by design — isolation comes from Docker, not permission theater: `claude --dangerously-skip-permissions`, `codex --dangerously-bypass-approvals-and-sandbox`, -`gemini --yolo`, and `grok --always-approve`. Auth +`gemini --yolo`, `grok --always-approve`, and `kimi --yolo`. Auth lives in per-agent config homes under `~/.config/deva/` with OAuth (default) or API-key modes per agent; nothing crosses the host boundary except what you mount. Known sharp edge: mounting `/var/run/docker.sock` is host-root with @@ -28,6 +28,7 @@ deva.sh # Claude Code (the default) deva.sh codex # same container shape, other agents deva.sh gemini deva.sh grok +deva.sh kimi deva.sh claude --debug --dry-run # inspect the docker run before trusting it ``` diff --git a/scripts/install-agent-tooling.sh b/scripts/install-agent-tooling.sh index 17d5dee..998bd34 100644 --- a/scripts/install-agent-tooling.sh +++ b/scripts/install-agent-tooling.sh @@ -8,6 +8,7 @@ set -euo pipefail : "${CODEX_VERSION:?CODEX_VERSION is required}" : "${GEMINI_CLI_VERSION:?GEMINI_CLI_VERSION is required}" : "${GROK_CLI_VERSION:?GROK_CLI_VERSION is required}" +: "${KIMI_CODE_VERSION:?KIMI_CODE_VERSION is required}" CCTRACE_VERSION="${CCTRACE_VERSION:-0.4.0}" CCX_VERSION="${CCX_VERSION:-v0.7.0}" @@ -137,7 +138,7 @@ install_npm_agent_tooling() { log "Installing npm agent tooling" log "Proxy config:" log_proxy_config - log "Requested versions: claude=${CLAUDE_CODE_VERSION} codex=${CODEX_VERSION} gemini=${GEMINI_CLI_VERSION} grok=${GROK_CLI_VERSION}" + log "Requested versions: claude=${CLAUDE_CODE_VERSION} codex=${CODEX_VERSION} gemini=${GEMINI_CLI_VERSION} grok=${GROK_CLI_VERSION} kimi=${KIMI_CODE_VERSION}" mkdir -p "$DEVA_HOME/.npm-global" "$DEVA_HOME/.local/bin" npm config set prefix "$DEVA_HOME/.npm-global" @@ -149,6 +150,7 @@ install_npm_agent_tooling() { "@openai/codex@${CODEX_VERSION}" \ "@google/gemini-cli@${GEMINI_CLI_VERSION}" \ "@xai-official/grok@${GROK_CLI_VERSION}" \ + "@moonshot-ai/kimi-code@${KIMI_CODE_VERSION}" \ || die "npm install failed" npm cache clean --force @@ -158,7 +160,10 @@ install_npm_agent_tooling() { "$DEVA_HOME/.npm-global/bin/codex" --version "$DEVA_HOME/.npm-global/bin/gemini" --version pin_grok_platform_binary - (npm list -g --depth=0 @anthropic-ai/claude-code @openai/codex @google/gemini-cli @xai-official/grok || true) + # kimi's npm bin is a plain symlink to dist/main.mjs (no self-update + # trampoline, no platform binary), so it needs no pinning — just verify. + "$DEVA_HOME/.npm-global/bin/kimi" --version + (npm list -g --depth=0 @anthropic-ai/claude-code @openai/codex @google/gemini-cli @xai-official/grok @moonshot-ai/kimi-code || true) } # grok's npm postinstall puts the real binary in ~/.grok/bin (the CLI's diff --git a/scripts/release-utils.sh b/scripts/release-utils.sh index 94ab801..d738f7f 100755 --- a/scripts/release-utils.sh +++ b/scripts/release-utils.sh @@ -30,6 +30,7 @@ TOOL_REGISTRY=( "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" + "kimi-code|npm|@moonshot-ai/kimi-code|org.opencontainers.image.kimi_code_version|https://www.npmjs.com/package/@moonshot-ai/kimi-code||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" diff --git a/scripts/resolve-tool-versions.sh b/scripts/resolve-tool-versions.sh index 96d387c..948274e 100644 --- a/scripts/resolve-tool-versions.sh +++ b/scripts/resolve-tool-versions.sh @@ -31,6 +31,7 @@ main() { resolve_tool "codex_version" "codex" resolve_tool "gemini_cli_version" "gemini-cli" resolve_tool "grok_cli_version" "grok-cli" + resolve_tool "kimi_code_version" "kimi-code" 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 ccee700..59f3c67 100755 --- a/scripts/test-install-agent-tooling.sh +++ b/scripts/test-install-agent-tooling.sh @@ -34,7 +34,7 @@ config) ;; install) mkdir -p "$DEVA_HOME/.npm-global/bin" - for bin in claude codex gemini grok; do + for bin in claude codex gemini grok kimi; do cat >"$DEVA_HOME/.npm-global/bin/$bin" <<'BIN' #!/usr/bin/env bash case "$(basename "$0")" in @@ -42,6 +42,7 @@ case "$(basename "$0")" in codex) echo "codex-cli __CODEX_VERSION__" ;; gemini) echo "__GEMINI_CLI_VERSION__" ;; grok) echo "grok __GROK_CLI_VERSION__" ;; + kimi) echo "__KIMI_CODE_VERSION__" ;; esac BIN chmod +x "$DEVA_HOME/.npm-global/bin/$bin" @@ -84,6 +85,7 @@ sed -i \ -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" \ + -e "s#__KIMI_CODE_VERSION__#$KIMI_CODE_VERSION#g" \ "$fake_bin/npm" cat >"$fake_bin/curl" <<'EOF' diff --git a/scripts/test-kimi-auth.sh b/scripts/test-kimi-auth.sh new file mode 100755 index 0000000..434a462 --- /dev/null +++ b/scripts/test-kimi-auth.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +# test-kimi-auth.sh - kimi agent auth wiring (oauth mount + api-key env synth) +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +tmp_home="$(mktemp -d)" +cleanup() { rm -rf "$tmp_home"; } +trap cleanup EXIT + +fail=0 +run_dry() { + ( + cd "$REPO_ROOT" + HOME="$tmp_home" \ + XDG_CONFIG_HOME="$tmp_home/.config" \ + XDG_CACHE_HOME="$tmp_home/.cache" \ + DEVA_NO_DOCKER=1 \ + ./deva.sh "$@" + ) 2>&1 +} + +want() { + local desc="$1" needle="$2" hay="$3" + if grep -F -- "$needle" <<<"$hay" >/dev/null; then + echo " PASS $desc" + else + echo " FAIL $desc" + echo " expected to find: $needle" >&2 + fail=1 + fi +} +want_absent() { + local desc="$1" needle="$2" hay="$3" + if grep -F -- "$needle" <<<"$hay" >/dev/null; then + echo " FAIL $desc" + echo " expected absent: $needle" >&2 + fail=1 + else + echo " PASS $desc" + fi +} + +echo "=== kimi oauth (default) ===" +oauth_out="$(run_dry kimi --debug --dry-run || true)" +want "runs kimi --yolo" "kimi --yolo" "$oauth_out" +want "auth method is oauth" "DEVA_AUTH_METHOD=oauth" "$oauth_out" + +echo "=== kimi api-key: KIMI_MODEL_* synth, no mount ===" +apikey_out="$(KIMI_CODE_API_KEY=sk-test-kimi-1234 run_dry kimi --auth-with api-key --dry-run -- -p hi || true)" +want "model name defaults to k3" "KIMI_MODEL_NAME=k3" "$apikey_out" +want "api key wired + redacted" "KIMI_MODEL_API_KEY=" "$apikey_out" +want "key last-4 tags container" "--api-key-1234--" "$apikey_out" +want "provider type kimi" "KIMI_MODEL_PROVIDER_TYPE=kimi" "$apikey_out" +want "coding endpoint base url" "KIMI_MODEL_BASE_URL=https://api.kimi.com/coding/v1" "$apikey_out" +want "passes agent args after --" "kimi --yolo -p hi" "$apikey_out" +want_absent "no ~/.kimi-code mount in api-key mode" ":/home/deva/.kimi-code" "$apikey_out" + +echo "=== kimi api-key: no mount on the hybrid config-root path either ===" +# The centralized mount walk (mount_agent_canonical) is a different code path +# from the legacy \$HOME fallback: seed the autolinked hybrid layout that any +# prior oauth run leaves behind and assert api-key mode still mounts nothing. +mkdir -p "$tmp_home/.config/deva/kimi/.kimi-code" +hybrid_out="$(KIMI_CODE_API_KEY=sk-test-kimi-1234 run_dry kimi --auth-with api-key --dry-run || true)" +want_absent "hybrid layout: no .kimi-code mount in api-key mode" ":/home/deva/.kimi-code" "$hybrid_out" +hybrid_oauth_out="$(run_dry kimi --dry-run || true)" +want "hybrid layout: oauth still mounts .kimi-code" ":/home/deva/.kimi-code" "$hybrid_oauth_out" +rm -rf "$tmp_home/.config/deva/kimi" + +echo "=== kimi api-key: DEVA_KIMI_MODEL / DEVA_KIMI_BASE_URL overrides ===" +override_out="$(KIMI_CODE_API_KEY=sk-test-kimi-1234 DEVA_KIMI_MODEL=kimi-for-coding \ + DEVA_KIMI_BASE_URL=https://api.moonshot.ai/v1 \ + run_dry kimi --auth-with api-key --dry-run || true)" +want "model override honored" "KIMI_MODEL_NAME=kimi-for-coding" "$override_out" +want "base url override honored" "KIMI_MODEL_BASE_URL=https://api.moonshot.ai/v1" "$override_out" + +echo "=== kimi --trace: cctrace wrapping ===" +trace_out="$(run_dry kimi --trace --dry-run || true)" +want "wraps with cctrace kimi" "cctrace kimi --no-open --" "$trace_out" +want "DEVA_TRACE=1 injected" "DEVA_TRACE=1" "$trace_out" +want "--yolo still present" "--yolo" "$trace_out" + +echo "=== kimi --trace after -- is passthrough ===" +trace_pass_out="$(run_dry kimi --dry-run -- --trace || true)" +want_absent "--trace before -- absorbed" "cctrace" "$trace_pass_out" +want "--trace passed to agent" "kimi --yolo --trace" "$trace_pass_out" + +echo "=== kimi api-key + trace ===" +trace_apikey_out="$(KIMI_CODE_API_KEY=sk-test-kimi-5678 run_dry kimi --auth-with api-key --trace --dry-run || true)" +want "trace + api-key: cctrace" "cctrace kimi --no-open --" "$trace_apikey_out" +want "trace + api-key: key wired" "KIMI_MODEL_API_KEY=" "$trace_apikey_out" + +echo "=== kimi api-key: missing key errors ===" +missing_out="$(KIMI_CODE_API_KEY= run_dry kimi --auth-with api-key --dry-run || true)" +want "errors when key unset" "KIMI_CODE_API_KEY not set" "$missing_out" + +if [ "$fail" -ne 0 ]; then + echo "FAIL: kimi auth wiring" >&2 + exit 1 +fi +echo "OK: kimi auth wiring" diff --git a/scripts/update-version-pins.sh b/scripts/update-version-pins.sh index 8b0d06b..099c19f 100644 --- a/scripts/update-version-pins.sh +++ b/scripts/update-version-pins.sh @@ -67,6 +67,7 @@ CCTRACE_VERSION=$CCTRACE_VERSION CODEX_VERSION=$CODEX_VERSION GEMINI_CLI_VERSION=$GEMINI_CLI_VERSION GROK_CLI_VERSION=$GROK_CLI_VERSION +KIMI_CODE_VERSION=$KIMI_CODE_VERSION CCX_VERSION=$CCX_VERSION COPILOT_API_VERSION=$COPILOT_API_VERSION PLAYWRIGHT_VERSION=$PLAYWRIGHT_VERSION @@ -105,6 +106,7 @@ main() { 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 KIMI_CODE_VERSION "$(fetch_npm_version @moonshot-ai/kimi-code)" 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 a95341b..7c8e460 100644 --- a/scripts/version-pins.sh +++ b/scripts/version-pins.sh @@ -18,6 +18,7 @@ VERSION_PIN_VARS=( CODEX_VERSION GEMINI_CLI_VERSION GROK_CLI_VERSION + KIMI_CODE_VERSION CCX_VERSION COPILOT_API_VERSION PLAYWRIGHT_VERSION diff --git a/scripts/version-upgrade.sh b/scripts/version-upgrade.sh index 3479b70..d02447f 100755 --- a/scripts/version-upgrade.sh +++ b/scripts/version-upgrade.sh @@ -17,6 +17,7 @@ _CLI_CCTRACE="${CCTRACE_VERSION:-}" _CLI_CODEX="${CODEX_VERSION:-}" _CLI_GEMINI="${GEMINI_CLI_VERSION:-}" _CLI_GROK="${GROK_CLI_VERSION:-}" +_CLI_KIMI="${KIMI_CODE_VERSION:-}" _CLI_CCX="${CCX_VERSION:-}" _CLI_COPILOT="${COPILOT_API_VERSION:-}" _CLI_PLAYWRIGHT="${PLAYWRIGHT_VERSION:-}" @@ -60,8 +61,8 @@ Options: -y, --yes Skip confirmation countdown --only LIST Upgrade only these tools (comma-separated); the rest stay pinned to versions.env. Tools: claude-code, - cctrace, codex, gemini-cli, grok-cli, ccx, - copilot-api, playwright + cctrace, codex, gemini-cli, grok-cli, kimi-code, + ccx, copilot-api, playwright -h, --help Show this help Environment: @@ -75,6 +76,7 @@ Environment: CODEX_VERSION Override codex version GEMINI_CLI_VERSION Override gemini-cli version GROK_CLI_VERSION Override grok-cli version + KIMI_CODE_VERSION Override kimi-code version CCX_VERSION Override ccx version COPILOT_API_VERSION Override copilot-api version PLAYWRIGHT_VERSION Override playwright version (rust image only) @@ -104,7 +106,7 @@ apply_only_filter() { [[ -n $ONLY ]] || return 0 local tool - local known="claude-code cctrace codex gemini-cli grok-cli ccx copilot-api playwright" + local known="claude-code cctrace codex gemini-cli grok-cli kimi-code ccx copilot-api playwright" for tool in ${ONLY//,/ }; do case " $known " in *" $tool "*) ;; @@ -119,6 +121,7 @@ apply_only_filter() { tool_selected codex || _CLI_CODEX="${_CLI_CODEX:-$CODEX_VERSION}" tool_selected gemini-cli || _CLI_GEMINI="${_CLI_GEMINI:-$GEMINI_CLI_VERSION}" tool_selected grok-cli || _CLI_GROK="${_CLI_GROK:-$GROK_CLI_VERSION}" + tool_selected kimi-code || _CLI_KIMI="${_CLI_KIMI:-$KIMI_CODE_VERSION}" tool_selected ccx || _CLI_CCX="${_CLI_CCX:-$CCX_VERSION}" tool_selected copilot-api || _CLI_COPILOT="${_CLI_COPILOT:-$COPILOT_API_VERSION}" tool_selected playwright || _CLI_PLAYWRIGHT="${_CLI_PLAYWRIGHT:-$PLAYWRIGHT_VERSION}" @@ -168,12 +171,13 @@ 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 grok_ver ccx_ver copilot_ver playwright_ver + local claude_ver cctrace_ver codex_ver gemini_ver grok_ver kimi_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")}" + kimi_ver="${_CLI_KIMI:-$(get_latest "kimi-code")}" ccx_ver="${_CLI_CCX:-$(get_latest "ccx")}" copilot_ver="${_CLI_COPILOT:-$(get_latest "copilot-api")}" playwright_ver="${_CLI_PLAYWRIGHT:-${PLAYWRIGHT_VERSION}}" @@ -183,6 +187,7 @@ main() { [[ -z $codex_ver ]] && missing+=("CODEX_VERSION") [[ -z $gemini_ver ]] && missing+=("GEMINI_CLI_VERSION") [[ -z $grok_ver ]] && missing+=("GROK_CLI_VERSION") + [[ -z $kimi_ver ]] && missing+=("KIMI_CODE_VERSION") [[ -z $ccx_ver ]] && missing+=("CCX_VERSION") [[ -z $copilot_ver ]] && missing+=("COPILOT_API_VERSION") [[ -z $playwright_ver ]] && missing+=("PLAYWRIGHT_VERSION") @@ -200,6 +205,7 @@ main() { "cctrace|cctrace_ver|_CLI_CCTRACE|cctrace" "Codex|codex_ver|_CLI_CODEX|codex" "Grok CLI|grok_ver|_CLI_GROK|grok-cli" + "Kimi Code|kimi_ver|_CLI_KIMI|kimi-code" "CCX|ccx_ver|_CLI_CCX|ccx" "Copilot API|copilot_ver|_CLI_COPILOT|copilot-api" "Playwright|playwright_ver|_CLI_PLAYWRIGHT|playwright" @@ -315,6 +321,7 @@ main() { --build-arg CODEX_VERSION="$codex_ver" \ --build-arg GEMINI_CLI_VERSION="$gemini_ver" \ --build-arg GROK_CLI_VERSION="$grok_ver" \ + --build-arg KIMI_CODE_VERSION="$kimi_ver" \ --build-arg CCX_VERSION="$ccx_ver" \ --build-arg COPILOT_API_VERSION="$copilot_ver" \ -t "$BUILD_IMAGE" . @@ -329,6 +336,7 @@ main() { --build-arg CODEX_VERSION="$codex_ver" \ --build-arg GEMINI_CLI_VERSION="$gemini_ver" \ --build-arg GROK_CLI_VERSION="$grok_ver" \ + --build-arg KIMI_CODE_VERSION="$kimi_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 bcde3f1..678b7bc 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 grok-cli" +expected_agent="cctrace ccx claude-code codex copilot-api gemini-cli grok-cli kimi-code" 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 grok-cli" +expected_main="cctrace ccx claude-code codex copilot-api gemini-cli grok-cli kimi-code" 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 grok-cli playwright" -assert_eq "default returns all 8" "$expected_all" "$default_all" +expected_all="cctrace ccx claude-code codex copilot-api gemini-cli grok-cli kimi-code playwright" +assert_eq "default returns all 9" "$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 4cc8b8d..9884212 100644 --- a/tests/version-upgrade.sh +++ b/tests/version-upgrade.sh @@ -27,6 +27,7 @@ inspect) "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.kimi_code_version":"0.28.0", "org.opencontainers.image.ccx_version":"v0.7.0", "org.opencontainers.image.copilot_api_version":"0ea08febdd7e3e055b03dd298bf57e669500b5c1", "org.opencontainers.image.playwright_version":"1.59.0" @@ -103,12 +104,14 @@ case "$url" in */-/package/@openai/codex/dist-tags) echo '{"latest":"0.117.0"}' ;; */-/package/@google/gemini-cli/dist-tags) echo '{"latest":"0.35.3"}' ;; */-/package/@xai-official/grok/dist-tags) echo '{"latest":"0.2.93"}' ;; +*/-/package/@moonshot-ai/kimi-code/dist-tags) echo '{"latest":"0.28.0"}' ;; */-/package/playwright/dist-tags) echo '{"latest":"1.60.0"}' ;; *registry.npmjs.org/@anthropic-ai%2fclaude-code) echo '{"time":{"2.1.87":"2026-03-29T01:40:00Z"}}' ;; *registry.npmjs.org/@thevibeworks%2fcctrace) echo '{"time":{"0.4.0":"2026-03-29T01:40:00Z"}}' ;; *registry.npmjs.org/@openai%2fcodex) echo '{"time":{"0.117.0":"2026-03-26T22:28:00Z"}}' ;; *registry.npmjs.org/@google%2fgemini-cli) echo '{"time":{"0.35.3":"2026-03-28T03:17:00Z"}}' ;; *registry.npmjs.org/@xai-official%2fgrok) echo '{"time":{"0.2.93":"2026-07-01T00:00:00Z"}}' ;; +*registry.npmjs.org/@moonshot-ai%2fkimi-code) echo '{"time":{"0.28.0":"2026-07-10T00:00:00Z"}}' ;; *registry.npmjs.org/playwright) echo '{"time":{"1.60.0":"2026-05-14T08:00:00Z"}}' ;; *) echo "unexpected curl url: $url" >&2 @@ -157,6 +160,7 @@ for expected in \ "--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 KIMI_CODE_VERSION=0.28.0" \ "--build-arg CCX_VERSION=v0.7.0" \ "--build-arg COPILOT_API_VERSION=0ea08febdd7e3e055b03dd298bf57e669500b5c1" \ "--build-arg GO_VERSION=1.26.2" @@ -174,6 +178,7 @@ for expected in \ "--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 KIMI_CODE_VERSION=0.28.0" \ "--build-arg CCX_VERSION=v0.7.0" \ "--build-arg PLAYWRIGHT_VERSION=1.60.0" do @@ -257,3 +262,28 @@ if [[ -s "$OUTAGE_BUILD_LOG" ]]; then echo "no builds should run during a registry outage" >&2 exit 1 fi + +# ───── update-version-pins: write_version_pins round-trips versions.env ───── +# The heredoc in write_version_pins is a second copy of the file layout: a +# pin present in versions.env but missing from the heredoc is silently +# deleted on the next `make versions-pin`. With every upstream fetch failing +# (curl still exits 22 from the outage block, git stubbed below), a rewrite +# must reproduce the file byte-for-byte, comments included. +cat >"$FAKE_BIN/git" <<'EOF' +#!/usr/bin/env bash +exit 1 +EOF +chmod +x "$FAKE_BIN/git" + +PINS_COPY="$TMP_ROOT/versions.env" +cp "$REPO_ROOT/versions.env" "$PINS_COPY" +if ! PATH="$FAKE_BIN:$PATH" \ + VERSION_PINS_FILE="$PINS_COPY" \ + bash "$REPO_ROOT/scripts/update-version-pins.sh" >/dev/null 2>&1; then + echo "update-version-pins.sh failed during round-trip check" >&2 + exit 1 +fi +if ! diff -u "$REPO_ROOT/versions.env" "$PINS_COPY"; then + echo "write_version_pins does not round-trip versions.env: pins or comments lost" >&2 + exit 1 +fi diff --git a/versions.env b/versions.env index 19c7863..029154c 100644 --- a/versions.env +++ b/versions.env @@ -2,20 +2,21 @@ # Update this file when we intentionally move default toolchain or CLI versions. NODE_MAJOR=22 -GO_VERSION=1.26.3 +GO_VERSION=1.26.5 PYTHON_VERSION=3.14t DELTA_VERSION=0.19.2 TMUX_VERSION=3.6a TMUX_SHA256=b6d8d9c76585db8ef5fa00d4931902fa4b8cbe8166f528f44fc403961a3f3759 -CLAUDE_CODE_VERSION=2.1.143 -CCTRACE_VERSION=0.16.0 -CODEX_VERSION=0.131.0 -GEMINI_CLI_VERSION=0.42.0 -GROK_CLI_VERSION=0.2.93 -CCX_VERSION=v0.7.0 +CLAUDE_CODE_VERSION=2.1.217 +CCTRACE_VERSION=0.19.0 +CODEX_VERSION=0.145.0 +GEMINI_CLI_VERSION=0.51.0 +GROK_CLI_VERSION=0.2.106 +KIMI_CODE_VERSION=0.28.1 +CCX_VERSION=v0.11.0 COPILOT_API_VERSION=0ea08febdd7e3e055b03dd298bf57e669500b5c1 -PLAYWRIGHT_VERSION=1.60.0 +PLAYWRIGHT_VERSION=1.61.1 RUST_TOOLCHAINS=stable RUST_DEFAULT_TOOLCHAIN=stable