From 682b21aee18088dd154c531ef457d1f9ebd63b00 Mon Sep 17 00:00:00 2001 From: Eric Wang Date: Mon, 23 Feb 2026 22:26:26 -0800 Subject: [PATCH 1/6] fix(docker): improve build robustness and suppress locale warnings - Generate en_US.UTF-8 locale to prevent setlocale warnings at startup - Add retry loop (5 attempts) for npm global installs to handle flaky network - Verify CLI tools by invoking binaries directly instead of npm list - Add set -eux for stricter error handling in user npm install step --- Dockerfile | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3a0ec4a..5e44a80 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,6 +33,11 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ procps psmisc zsh socat \ libevent-dev libncurses-dev bison +# Prevent noisy setlocale warnings at shell startup +RUN sed -i 's/^# *en_US\.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \ + locale-gen en_US.UTF-8 && \ + update-locale LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 + RUN git lfs install --system # Install language runtimes in parallel-friendly layers @@ -72,7 +77,20 @@ ARG COPILOT_API_VERSION LABEL org.opencontainers.image.copilot_api_version=${COPILOT_API_VERSION} RUN --mount=type=cache,target=/root/.npm,sharing=locked \ - npm install -g npm@latest pnpm && \ + set -eu && \ + i=0 && \ + while :; do \ + i=$((i + 1)) && \ + if npm install -g npm@latest pnpm; then \ + break; \ + fi; \ + if [ "$i" -ge 5 ]; then \ + echo "npm install failed after $i attempts" >&2; \ + exit 1; \ + fi; \ + echo "npm install failed (attempt $i), retrying..." >&2; \ + sleep $((i * 5)); \ + done && \ git clone --branch "${COPILOT_API_BRANCH}" "${COPILOT_API_REPO}" /tmp/copilot-api && \ cd /tmp/copilot-api && \ git checkout "${COPILOT_API_COMMIT}" && \ @@ -202,6 +220,7 @@ LABEL org.opencontainers.image.gemini_cli_version=${GEMINI_CLI_VERSION} # Install CLI tools via npm RUN --mount=type=cache,target=/home/deva/.npm,uid=${DEVA_UID},gid=${DEVA_GID},sharing=locked \ + set -eux && \ npm config set prefix "$DEVA_HOME/.npm-global" && \ npm install -g --no-audit --no-fund \ @anthropic-ai/claude-code@${CLAUDE_CODE_VERSION} \ @@ -209,7 +228,11 @@ RUN --mount=type=cache,target=/home/deva/.npm,uid=${DEVA_UID},gid=${DEVA_GID},sh @openai/codex@${CODEX_VERSION} \ @google/gemini-cli@${GEMINI_CLI_VERSION} && \ npm cache clean --force && \ - npm list -g --depth=0 @anthropic-ai/claude-code @openai/codex @google/gemini-cli || true + "$DEVA_HOME/.npm-global/bin/claude" --version && \ + "$DEVA_HOME/.npm-global/bin/codex" --version && \ + "$DEVA_HOME/.npm-global/bin/gemini" --version && \ + "$DEVA_HOME/.npm-global/bin/claude-trace" --help >/dev/null && \ + (npm list -g --depth=0 @anthropic-ai/claude-code @openai/codex @google/gemini-cli || true) # Volatile packages: Install at the end to avoid cascading rebuilds ARG ATLAS_CLI_VERSION=main From ac74d82483dc9700ed249a3e8698ba7b80e2862d Mon Sep 17 00:00:00 2001 From: Eric Wang Date: Mon, 9 Mar 2026 20:35:56 -0700 Subject: [PATCH 2/6] feat: add -Q bare mode and credential backup system - Add -Q/--quick flag for bare mode: no config mounts, no .deva, implies --rm - Backup credentials to tmpdir when auth override active, restore on exit - Include config hash in container name for explicit --config-home - Skip ~/.config/deva and ~/.cache/deva when -c is explicit (isolation) - Warn when explicit config-home has empty auth directories - Skip filesystem operations during --dry-run - Redact secrets in debug output --- agents/gemini.sh | 38 ++-- deva.sh | 460 +++++++++++++++++++++++++++++++---------------- 2 files changed, 330 insertions(+), 168 deletions(-) diff --git a/agents/gemini.sh b/agents/gemini.sh index 85f2754..df4852b 100644 --- a/agents/gemini.sh +++ b/agents/gemini.sh @@ -31,12 +31,16 @@ setup_gemini_auth() { case "$method" in gemini-app-oauth|oauth) AUTH_DETAILS="gemini-app-oauth (~/.gemini)" - if [ -d "$HOME/.gemini" ]; then - DOCKER_ARGS+=("-v" "$HOME/.gemini:/home/deva/.gemini") - else - echo "Warning: ~/.gemini directory not found, creating it" >&2 - mkdir -p "$HOME/.gemini" - DOCKER_ARGS+=("-v" "$HOME/.gemini:/home/deva/.gemini") + # Only mount host ~/.gemini 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/.gemini" ]; then + DOCKER_ARGS+=("-v" "$HOME/.gemini:/home/deva/.gemini") + else + echo "Warning: ~/.gemini directory not found, creating it" >&2 + mkdir -p "$HOME/.gemini" + DOCKER_ARGS+=("-v" "$HOME/.gemini:/home/deva/.gemini") + fi fi ;; api-key|gemini-api-key) @@ -49,7 +53,9 @@ setup_gemini_auth() { DOCKER_ARGS+=("-e" "GEMINI_API_KEY=$GEMINI_API_KEY") local gemini_config_dir - if [ -n "${CONFIG_ROOT:-}" ]; then + if [ -n "${CONFIG_HOME:-}" ] && [ "${CONFIG_HOME_FROM_CLI:-false}" = true ]; then + gemini_config_dir="$CONFIG_HOME/.gemini" + elif [ -n "${CONFIG_ROOT:-}" ]; then case "$CONFIG_ROOT" in /*) ;; *) auth_error "CONFIG_ROOT must be absolute path: $CONFIG_ROOT" ;; @@ -72,12 +78,13 @@ setup_gemini_auth() { gemini_config_dir="$HOME/.gemini" fi - mkdir -p "$gemini_config_dir" - rm -f "$gemini_config_dir/mcp-oauth-tokens-v2.json" + if [ "${DRY_RUN:-false}" != true ]; then + mkdir -p "$gemini_config_dir" + rm -f "$gemini_config_dir/mcp-oauth-tokens-v2.json" - local settings_file="$gemini_config_dir/settings.json" - if [ ! -f "$settings_file" ] || ! grep -q '"selectedType"' "$settings_file" 2>/dev/null; then - cat > "$settings_file" <<'EOF' + local settings_file="$gemini_config_dir/settings.json" + if [ ! -f "$settings_file" ] || ! grep -q '"selectedType"' "$settings_file" 2>/dev/null; then + cat > "$settings_file" <<'EOF' { "security": { "auth": { @@ -86,9 +93,10 @@ setup_gemini_auth() { } } EOF - echo "Created gemini settings with API key auth: $settings_file" >&2 - else - echo "Using existing gemini settings: $settings_file" >&2 + echo "Created gemini settings with API key auth: $settings_file" >&2 + else + echo "Using existing gemini settings: $settings_file" >&2 + fi fi ;; vertex) diff --git a/deva.sh b/deva.sh index e2e2ee6..d705e4a 100755 --- a/deva.sh +++ b/deva.sh @@ -39,6 +39,7 @@ AGENT_ARGS=() AGENT_EXPLICIT=false EPHEMERAL_MODE=false +QUICK_MODE=false GLOBAL_MODE=false DEBUG_MODE=false DRY_RUN=false @@ -72,6 +73,8 @@ Deva flags: -e VAR[=VALUE] Pass environment variable into the container (pulls from host when VALUE omitted) -p NAME, --profile NAME Select profile: base (default), rust. Pulls tag, falls back to Dockerfile. + -Q, --quick Bare mode: no host config mounts, no .deva loading, no autolink, + implies --rm. Like emacs -Q. Mutually exclusive with -c. --host-net Use host networking for the agent container --no-docker Disable auto-mount of Docker socket (default: auto-mount if present) --dry-run Show docker command without executing (implies --debug) @@ -612,19 +615,36 @@ prepare_base_docker_args() { volume_hash=$(compute_volume_hash) fi - if [ "$EPHEMERAL_MODE" = true ]; then - if [ -n "$volume_hash" ]; then - container_name="${DEVA_CONTAINER_PREFIX}-${slug}..v${volume_hash}-${ACTIVE_AGENT}-$$" + # Include config-home in container identity when explicit. + # Use CONFIG_HOME if set, else CONFIG_ROOT (root mode clears CONFIG_HOME). + local config_hash="" + local config_hash_source="" + if [ "$CONFIG_HOME_FROM_CLI" = true ]; then + if [ -n "$CONFIG_HOME" ]; then + config_hash_source="$CONFIG_HOME" + elif [ -n "$CONFIG_ROOT" ]; then + config_hash_source="$CONFIG_ROOT" + fi + fi + if [ -n "$config_hash_source" ]; then + if command -v md5sum >/dev/null 2>&1; then + config_hash=$(printf '%s' "$config_hash_source" | md5sum | cut -c1-6) + elif command -v shasum >/dev/null 2>&1; then + config_hash=$(printf '%s' "$config_hash_source" | shasum | cut -c1-6) else - container_name="${DEVA_CONTAINER_PREFIX}-${slug}-${ACTIVE_AGENT}-$$" + config_hash=$(printf '%s' "$config_hash_source" | cksum | cut -d' ' -f1 | cut -c1-6) fi + fi + + local suffix="" + [ -n "$volume_hash" ] && suffix="..v${volume_hash}" + [ -n "$config_hash" ] && suffix="${suffix}..c${config_hash}" + + if [ "$EPHEMERAL_MODE" = true ]; then + container_name="${DEVA_CONTAINER_PREFIX}-${slug}${suffix}-${ACTIVE_AGENT}-$$" DOCKER_ARGS=(run --rm -it) else - if [ -n "$volume_hash" ]; then - container_name="${DEVA_CONTAINER_PREFIX}-${slug}..v${volume_hash}" - else - container_name="${DEVA_CONTAINER_PREFIX}-${slug}" - fi + container_name="${DEVA_CONTAINER_PREFIX}-${slug}${suffix}" DOCKER_ARGS=(run -d) fi @@ -925,6 +945,114 @@ mount_config_home() { done } +# Credential backup system. +# - .claude.json: always cp'd to XDG_STATE (outside mount tree, corruption protection). +# - Credential files: mv'd to tmpdir when auth override is detected. +# - Auth override = non-default --auth-with OR auth env vars reaching container. +# Entries are "orig_path:backup_path" pairs for restore. +BACKED_UP_CREDS=() +CRED_BACKUP_TMPDIR="" + +# Effective config base: where agent config dirs (.claude/, .codex/, .gemini/) live. +resolve_config_base() { + if [ -n "$CONFIG_HOME" ]; then + printf '%s' "$CONFIG_HOME" + elif [ -n "$CONFIG_ROOT" ]; then + printf '%s' "$CONFIG_ROOT/$ACTIVE_AGENT" + else + printf '%s' "$HOME" + fi +} + +user_envs_has() { + local name="$1" spec + for spec in "${USER_ENVS[@]+"${USER_ENVS[@]}"}"; do + [ "$spec" = "$name" ] || [[ "$spec" == "$name="* ]] && return 0 + done + return 1 +} + +# Detect auth override: non-default --auth-with OR auth env vars reaching container. +# Claude Code auth priority: env vars > .credentials.json (file is lowest priority). +# When env-var auth is active, mounting credential files is a leak + corruption risk. +# Only counts env vars that survive should_skip_env_for_auth filtering. +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) ;; + *) return 0 ;; + esac + fi + + # Auth env vars that override file-based credentials. + local auth_vars="" + case "$ACTIVE_AGENT" in + 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" ;; + esac + + local var + for var in $auth_vars; do + # Skip vars that would be blocked by auth-env filtering + should_skip_env_for_auth "$var" && continue + docker_args_has_env "$var" && return 0 + user_envs_has "$var" && return 0 + done + + return 1 +} + +backup_credentials() { + local config_base + config_base=$(resolve_config_base) + + local agent_dir cred_file + case "$ACTIVE_AGENT" in + claude) agent_dir=".claude"; cred_file=".credentials.json" ;; + codex) agent_dir=".codex"; cred_file="auth.json" ;; + gemini) agent_dir=".gemini"; cred_file="mcp-oauth-tokens-v2.json" ;; + *) return 0 ;; + esac + + # .claude.json corruption backup: persistent, outside container mount tree. + if [ "$ACTIVE_AGENT" = "claude" ]; then + local state_dir="${XDG_STATE_HOME:-$HOME/.local/state}/deva/backups" + local claude_json="$config_base/.claude.json" + if [ -f "$claude_json" ]; then + mkdir -p "$state_dir" + cp "$claude_json" "$state_dir/.claude.json.bak" + fi + fi + + # Credential backup: tmpdir outside all mount trees. + if has_auth_override; then + local cred_path="$config_base/$agent_dir/$cred_file" + if [ -f "$cred_path" ]; then + CRED_BACKUP_TMPDIR=$(mktemp -d "${TMPDIR:-/tmp}/deva-cred.XXXXXX") + mv "$cred_path" "$CRED_BACKUP_TMPDIR/$cred_file" + BACKED_UP_CREDS+=("$cred_path:$CRED_BACKUP_TMPDIR/$cred_file") + echo "info: backed up $cred_file -> tmpdir (auth override active)" >&2 + fi + fi +} + +restore_backed_up_credentials() { + local entry + for entry in "${BACKED_UP_CREDS[@]+"${BACKED_UP_CREDS[@]}"}"; do + local orig="${entry%%:*}" + local backup="${entry##*:}" + if [ -f "$backup" ]; then + mv "$backup" "$orig" + echo "info: restored $(basename "$orig")" >&2 + fi + done + if [ -n "$CRED_BACKUP_TMPDIR" ] && [ -d "$CRED_BACKUP_TMPDIR" ]; then + rm -rf "$CRED_BACKUP_TMPDIR" + fi +} + mount_dir_contents_into_home() { local base="$1" [ -d "$base" ] || return @@ -1418,6 +1546,14 @@ parse_wrapper_args() { i=$((i + 1)) continue ;; + -Q | --quick) + QUICK_MODE=true + SKIP_CONFIG=true + AUTOLINK=false + EPHEMERAL_MODE=true + i=$((i + 1)) + continue + ;; -g | --global) GLOBAL_MODE=true i=$((i + 1)) @@ -1869,7 +2005,18 @@ if [ "$AGENT_EXPLICIT" = false ]; then ACTIVE_AGENT="$DEFAULT_AGENT" fi -if [ -z "$CONFIG_HOME" ]; then +# -Q and -c are mutually exclusive +if [ "$QUICK_MODE" = true ] && [ "$CONFIG_HOME_FROM_CLI" = true ]; then + echo "error: -Q/--quick and -c/--config-home are mutually exclusive" >&2 + exit 1 +fi + +# -Q bare mode: skip all config-home resolution and scaffolding +if [ "$QUICK_MODE" = true ]; then + CONFIG_HOME="" + CONFIG_ROOT="" + CONFIG_HOME_AUTO=false +elif [ -z "$CONFIG_HOME" ]; then set_config_home_value "$(default_config_home_for_agent "$ACTIVE_AGENT")" CONFIG_HOME_AUTO=true fi @@ -1933,16 +2080,53 @@ autolink_legacy_into_deva_root() { check_agent "$ACTIVE_AGENT" -if [ -n "$CONFIG_HOME" ]; then +if [ -n "$CONFIG_HOME" ] && [ "$DRY_RUN" != true ]; then if [ ! -d "$CONFIG_HOME" ]; then mkdir -p "$CONFIG_HOME" fi - if [ "$ACTIVE_AGENT" = "claude" ] && [ ! -f "$CONFIG_HOME/.claude.json" ]; then - echo '{}' >"$CONFIG_HOME/.claude.json" - fi - if [ "$ACTIVE_AGENT" = "gemini" ] && [ ! -f "$CONFIG_HOME/settings.json" ]; then - echo '{}' >"$CONFIG_HOME/settings.json" + case "$ACTIVE_AGENT" in + claude) + [ -d "$CONFIG_HOME/.claude" ] || mkdir -p "$CONFIG_HOME/.claude" + [ -f "$CONFIG_HOME/.claude.json" ] || echo '{}' >"$CONFIG_HOME/.claude.json" + ;; + codex) + [ -d "$CONFIG_HOME/.codex" ] || mkdir -p "$CONFIG_HOME/.codex" + ;; + gemini) + [ -d "$CONFIG_HOME/.gemini" ] || mkdir -p "$CONFIG_HOME/.gemini" + [ -f "$CONFIG_HOME/.gemini/settings.json" ] || echo '{}' >"$CONFIG_HOME/.gemini/settings.json" + ;; + esac +fi + +# Warn if explicit --config-home is missing the agent's auth directory. +# Only warn for default OAuth flows — api-key/bedrock/vertex/copilot don't need local auth dirs. +# Peek at AGENT_ARGV + AGENT_ARGS to detect --auth-with before agent_prepare() runs. +_config_home_uses_default_auth=true +for _arg in "${AGENT_ARGV[@]+"${AGENT_ARGV[@]}"}" "${AGENT_ARGS[@]+"${AGENT_ARGS[@]}"}"; do + if [ "$_arg" = "--auth-with" ]; then + _config_home_uses_default_auth=false + break fi +done +if [ "$CONFIG_HOME_FROM_CLI" = true ] && [ -n "$CONFIG_HOME" ] && [ "$_config_home_uses_default_auth" = true ]; then + case "$ACTIVE_AGENT" in + claude) + if [ ! -d "$CONFIG_HOME/.claude" ] || [ -z "$(ls -A "$CONFIG_HOME/.claude" 2>/dev/null)" ]; then + echo "warning: $CONFIG_HOME/.claude is empty; OAuth credentials will need to be set up" >&2 + fi + ;; + codex) + if [ ! -d "$CONFIG_HOME/.codex" ] || [ -z "$(ls -A "$CONFIG_HOME/.codex" 2>/dev/null)" ]; then + echo "warning: $CONFIG_HOME/.codex is empty; authentication will need to be set up" >&2 + fi + ;; + gemini) + if [ ! -d "$CONFIG_HOME/.gemini" ] || [ -z "$(ls -A "$CONFIG_HOME/.gemini" 2>/dev/null)" ]; then + echo "warning: $CONFIG_HOME/.gemini is empty; authentication will need to be set up" >&2 + fi + ;; + esac fi if dangerous_directory; then @@ -1968,10 +2152,20 @@ fi if [ -n "${AUTH_METHOD:-}" ]; then # Determine if we need auth suffix needs_auth_suffix=false + _env_auth_override=false if [ "$ACTIVE_AGENT" = "claude" ] && [ "$AUTH_METHOD" != "claude" ]; then needs_auth_suffix=true elif [ "$ACTIVE_AGENT" = "codex" ] && [ "$AUTH_METHOD" != "chatgpt" ]; then needs_auth_suffix=true + elif [ "$ACTIVE_AGENT" = "gemini" ] && [ "$AUTH_METHOD" != "oauth" ] && [ "$AUTH_METHOD" != "gemini-app-oauth" ]; then + needs_auth_suffix=true + fi + + # Env-var auth override: default method but auth env vars reaching container. + # Container name must change when effective auth source changes. + if [ "$needs_auth_suffix" = false ] && has_auth_override; then + needs_auth_suffix=true + _env_auth_override=true fi if [ "$needs_auth_suffix" = true ]; then @@ -1981,6 +2175,26 @@ if [ -n "${AUTH_METHOD:-}" ]; then volume_hash=$(compute_volume_hash) fi + # Recompute config hash to preserve in auth-rewritten name + auth_config_hash="" + auth_config_src="" + if [ "$CONFIG_HOME_FROM_CLI" = true ]; then + if [ -n "$CONFIG_HOME" ]; then + auth_config_src="$CONFIG_HOME" + elif [ -n "$CONFIG_ROOT" ]; then + auth_config_src="$CONFIG_ROOT" + fi + fi + if [ -n "$auth_config_src" ]; then + if command -v md5sum >/dev/null 2>&1; then + auth_config_hash=$(printf '%s' "$auth_config_src" | md5sum | cut -c1-6) + elif command -v shasum >/dev/null 2>&1; then + auth_config_hash=$(printf '%s' "$auth_config_src" | shasum | cut -c1-6) + else + auth_config_hash=$(printf '%s' "$auth_config_src" | cksum | cut -d' ' -f1 | cut -c1-6) + fi + fi + # Hash credential file path for credentials-file auth creds_hash="" if [ "$AUTH_METHOD" = "credentials-file" ] && [ -n "${CUSTOM_CREDENTIALS_FILE:-}" ]; then @@ -1994,21 +2208,23 @@ if [ -n "${AUTH_METHOD:-}" ]; then fi new_container_name="" - auth_suffix="${AUTH_METHOD}" + if [ "$_env_auth_override" = true ]; then + auth_suffix="env" + else + auth_suffix="${AUTH_METHOD}" + fi [ -n "$creds_hash" ] && auth_suffix="${AUTH_METHOD}-${creds_hash}" + # Build suffix chain: volume + config + auth + name_suffix="" + [ -n "$volume_hash" ] && name_suffix="..v${volume_hash}" + [ -n "$auth_config_hash" ] && name_suffix="${name_suffix}..c${auth_config_hash}" + name_suffix="${name_suffix}..${auth_suffix}" + if [ "$EPHEMERAL_MODE" = true ]; then - if [ -n "$volume_hash" ]; then - new_container_name="${DEVA_CONTAINER_PREFIX}-${slug}..v${volume_hash}..${auth_suffix}-${ACTIVE_AGENT}-$$" - else - new_container_name="${DEVA_CONTAINER_PREFIX}-${slug}..${auth_suffix}-${ACTIVE_AGENT}-$$" - fi + new_container_name="${DEVA_CONTAINER_PREFIX}-${slug}${name_suffix}-${ACTIVE_AGENT}-$$" else - if [ -n "$volume_hash" ]; then - new_container_name="${DEVA_CONTAINER_PREFIX}-${slug}..v${volume_hash}..${auth_suffix}" - else - new_container_name="${DEVA_CONTAINER_PREFIX}-${slug}..${auth_suffix}" - fi + new_container_name="${DEVA_CONTAINER_PREFIX}-${slug}${name_suffix}" fi # Update container name in DOCKER_ARGS @@ -2038,129 +2254,35 @@ for ((i = 0; i < ${#DOCKER_ARGS[@]}; i++)); do done # Always export container context (regardless of auth method) +# Note: DEVA_AGENT already set in prepare_base_docker_args (line 636) DOCKER_ARGS+=(-e "DEVA_CONTAINER_NAME=${CONTAINER_NAME}") -DOCKER_ARGS+=(-e "DEVA_AGENT=${ACTIVE_AGENT}") DOCKER_ARGS+=(-e "DEVA_WORKSPACE=$(pwd)") DOCKER_ARGS+=(-e "DEVA_EPHEMERAL=${EPHEMERAL_MODE}") -# Centralized mounting logic based on auth method -# If --config-home is set, use it exclusively and skip auth-based mounting -if [ -n "$CONFIG_HOME" ]; then +# Credential backup: move credential files to tmpdir before mounting. +# Skipped during --dry-run to avoid side effects (mkdir, cp, mv). +if [ "$QUICK_MODE" != true ] && [ "$DRY_RUN" != true ]; then + backup_credentials +fi + +# Centralized mounting logic. +# -Q bare mode: skip all config/auth mounts entirely. +if [ "$QUICK_MODE" = true ]; then + : # bare mode: no config mounts +elif [ -n "$CONFIG_HOME" ]; then mount_config_home -elif [ -n "${AUTH_METHOD:-}" ]; then - is_default_auth=false - if [ "$ACTIVE_AGENT" = "claude" ] && [ "$AUTH_METHOD" = "claude" ]; then - is_default_auth=true - elif [ "$ACTIVE_AGENT" = "codex" ] && [ "$AUTH_METHOD" = "chatgpt" ]; then - is_default_auth=true - fi - - if [ "$is_default_auth" = true ]; then - # Default auth: mount all OAuth credentials for shared container - if [ -n "$CONFIG_ROOT" ] && [ -d "$CONFIG_ROOT" ]; then - # CONFIG_ROOT mode: mount all agent dirs (includes OAuth via symlinks) - for d in "$CONFIG_ROOT"/*; do - [ -d "$d" ] || continue - [ "$(basename "$d")" = "_shared" ] && continue - mount_dir_contents_into_home "$d" - done - else - # Direct mode: mount both ~/.claude and ~/.codex - if [ -d "$HOME/.claude" ]; then - DOCKER_ARGS+=("-v" "$HOME/.claude:/home/deva/.claude") - fi - if [ -f "$HOME/.claude.json" ]; then - DOCKER_ARGS+=("-v" "$HOME/.claude.json:/home/deva/.claude.json") - fi - if [ -d "$HOME/.codex" ]; then - DOCKER_ARGS+=("-v" "$HOME/.codex:/home/deva/.codex") - fi - fi +else + if [ -n "$CONFIG_ROOT" ] && [ -d "$CONFIG_ROOT" ]; then + for d in "$CONFIG_ROOT"/*; do + [ -d "$d" ] || continue + [ "$(basename "$d")" = "_shared" ] && continue + mount_dir_contents_into_home "$d" + done else - # Non-default auth: exclude OAuth credential files - if [ -n "$CONFIG_ROOT" ] && [ -d "$CONFIG_ROOT" ]; then - # CONFIG_ROOT mode: selectively mount, excluding credentials - for agent_dir in "$CONFIG_ROOT"/*; do - [ -d "$agent_dir" ] || continue - agent_name=$(basename "$agent_dir") - [ "$agent_name" = "_shared" ] && continue - - # Determine credential file to exclude - exclude_file="" - case "$agent_name" in - claude) exclude_file=".credentials.json" ;; - codex) exclude_file="auth.json" ;; - esac - - # Mount agent dir contents, excluding OAuth credentials - for item in "$agent_dir"/.* "$agent_dir"/*; do - [ -e "$item" ] || continue - name=$(basename "$item") - case "$name" in - . | ..) continue ;; - esac - - # Skip OAuth credential files - if [ -n "$exclude_file" ]; then - # Check if item is the credential file or contains it - if [ "$name" = "$exclude_file" ]; then - continue - elif [ -d "$item" ] && [ -f "$item/$exclude_file" ]; then - # It's a .claude or .codex directory containing credentials - # Mount contents individually, excluding credential - for subitem in "$item"/* "$item"/.*; do - [ -e "$subitem" ] || continue - subname=$(basename "$subitem") || { - echo "warning: failed to get basename for $subitem" >&2 - continue - } - [ -n "$subname" ] || continue - case "$subname" in - . | .. | "$exclude_file") continue ;; - esac - DOCKER_ARGS+=("-v" "$subitem:/home/deva/$name/$subname") - done - continue - fi - fi - - DOCKER_ARGS+=("-v" "$item:/home/deva/$name") - done - done - else - # Direct mode: mount ~/.claude and ~/.codex, excluding credentials - if [ -d "$HOME/.claude" ]; then - for item in "$HOME/.claude"/* "$HOME/.claude"/.*; do - [ -e "$item" ] || continue - name=$(basename "$item") || { - echo "warning: failed to get basename for $item" >&2 - continue - } - [ -n "$name" ] || continue - case "$name" in - . | .. | .credentials.json) continue ;; - esac - DOCKER_ARGS+=("-v" "$item:/home/deva/.claude/$name") - done - fi - if [ -f "$HOME/.claude.json" ]; then - DOCKER_ARGS+=("-v" "$HOME/.claude.json:/home/deva/.claude.json") - fi - if [ -d "$HOME/.codex" ]; then - for item in "$HOME/.codex"/* "$HOME/.codex"/.*; do - [ -e "$item" ] || continue - name=$(basename "$item") || { - echo "warning: failed to get basename for $item" >&2 - continue - } - [ -n "$name" ] || continue - case "$name" in - . | .. | auth.json) continue ;; - esac - DOCKER_ARGS+=("-v" "$item:/home/deva/.codex/$name") - done - fi - fi + # Fallback: direct mount from $HOME (CONFIG_ROOT should always be set) + [ -d "$HOME/.claude" ] && DOCKER_ARGS+=("-v" "$HOME/.claude:/home/deva/.claude") + [ -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") fi fi @@ -2169,16 +2291,19 @@ DOCKER_ARGS+=("-e" "CLAUDE_DATA_DIR=/home/deva/.config/deva/claude") DOCKER_ARGS+=("-e" "CLAUDE_CACHE_DIR=/home/deva/.cache/deva/claude/sessions") # Mount deva config and cache directories for statusline usage tracking -if [ -d "$HOME/.config/deva" ]; then - DOCKER_ARGS+=("-v" "$HOME/.config/deva:/home/deva/.config/deva") -fi -if [ -d "$HOME/.cache/deva" ]; then - DOCKER_ARGS+=("-v" "$HOME/.cache/deva:/home/deva/.cache/deva") +# Skip when --config-home is explicit or -Q bare mode to preserve isolation +if [ "$CONFIG_HOME_FROM_CLI" = false ] && [ "$QUICK_MODE" = false ]; then + if [ -d "$HOME/.config/deva" ]; then + DOCKER_ARGS+=("-v" "$HOME/.config/deva:/home/deva/.config/deva") + fi + if [ -d "$HOME/.cache/deva" ]; then + DOCKER_ARGS+=("-v" "$HOME/.cache/deva:/home/deva/.cache/deva") + fi fi -# Mount project-local .claude directory if exists +# Mount project-local .claude directory if exists (skip in bare mode) append_user_envs -if [ -d "$(pwd)/.claude" ]; then +if [ "$QUICK_MODE" = false ] && [ -d "$(pwd)/.claude" ]; then DOCKER_ARGS+=("-v" "$(pwd)/.claude:$(pwd)/.claude") fi @@ -2201,6 +2326,28 @@ for ((i = 0; i < ${#DOCKER_ARGS[@]}; i++)); do fi done +mask_secrets_in_args() { + local arg + for arg in "$@"; do + if [[ "$arg" =~ ^-e$ ]]; then + printf '%s ' "$arg" + elif [[ "$arg" =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*) ]]; then + local name="${BASH_REMATCH[1]}" + local value="${BASH_REMATCH[2]}" + case "$name" in + *TOKEN*|*KEY*|*SECRET*|*PASSWORD*|*CREDENTIALS*|*BARK_KEY*) + printf '%s= ' "$name" + ;; + *) + printf '%s ' "$arg" + ;; + esac + else + printf '%s ' "$arg" + fi + done +} + if [ "$DEBUG_MODE" = true ]; then echo "=== DEBUG: Docker command ===" >&2 echo "Container name: $CONTAINER_NAME" >&2 @@ -2208,15 +2355,20 @@ if [ "$DEBUG_MODE" = true ]; then echo "Ephemeral mode: $EPHEMERAL_MODE" >&2 echo "" >&2 if [ "$EPHEMERAL_MODE" = false ]; then - echo "docker run -d ${DOCKER_ARGS[*]:2} tail -f /dev/null" >&2 + echo "docker run -d $(mask_secrets_in_args "${DOCKER_ARGS[@]:2}") tail -f /dev/null" >&2 echo "docker exec -it $CONTAINER_NAME /usr/local/bin/docker-entrypoint.sh ${AGENT_COMMAND[*]}" >&2 else - echo "docker ${DOCKER_ARGS[*]} ${AGENT_COMMAND[*]}" >&2 + echo "docker $(mask_secrets_in_args "${DOCKER_ARGS[@]}") ${AGENT_COMMAND[*]}" >&2 fi echo "===========================" >&2 echo "" >&2 fi +# Restore backed-up credential files on exit (covers crashes, signals, normal exit) +if [ ${#BACKED_UP_CREDS[@]} -gt 0 ]; then + trap restore_backed_up_credentials EXIT +fi + if [ "$DRY_RUN" = true ]; then exit 0 fi @@ -2277,6 +2429,8 @@ if [ "$EPHEMERAL_MODE" = false ]; then update_session_file fi + # Restore credentials before exec (exec replaces the process, trap won't fire) + restore_backed_up_credentials exec docker exec -it "$CONTAINER_NAME" /usr/local/bin/docker-entrypoint.sh "${AGENT_COMMAND[@]}" else echo "Launching ${ACTIVE_AGENT} (ephemeral mode) via ${DEVA_DOCKER_IMAGE}:${DEVA_DOCKER_TAG}" From 93af788fd43a3c559ae5a5551249a9da97038694 Mon Sep 17 00:00:00 2001 From: Eric Wang Date: Wed, 11 Mar 2026 20:18:50 -0700 Subject: [PATCH 3/6] refactor(auth): replace credential backup with overlay isolation - Add ANTHROPIC_AUTH_TOKEN and ANTHROPIC_BASE_URL env var support - Replace mv/restore credential backup with overlay file mounting - Filter sensitive files (.credentials.json, auth.json) from home mounts - Extract should_mount_home_item() for cleaner mount logic --- agents/claude.sh | 12 +++- agents/shared_auth.sh | 2 +- deva.sh | 155 ++++++++++++++++++++++++++---------------- 3 files changed, 108 insertions(+), 61 deletions(-) diff --git a/agents/claude.sh b/agents/claude.sh index d2b73ce..81d2f3e 100644 --- a/agents/claude.sh +++ b/agents/claude.sh @@ -80,6 +80,10 @@ setup_claude_auth() { DOCKER_ARGS+=("-e" "CLAUDE_CODE_OAUTH_TOKEN=$CLAUDE_CODE_OAUTH_TOKEN") AUTH_DETAILS="oauth-token (CLAUDE_CODE_OAUTH_TOKEN)" echo "Using OAuth token from CLAUDE_CODE_OAUTH_TOKEN" >&2 + elif [ -n "${ANTHROPIC_AUTH_TOKEN:-}" ]; then + DOCKER_ARGS+=("-e" "ANTHROPIC_AUTH_TOKEN=$ANTHROPIC_AUTH_TOKEN") + AUTH_DETAILS="auth-token (ANTHROPIC_AUTH_TOKEN)" + echo "Using auth token from ANTHROPIC_AUTH_TOKEN" >&2 elif [ -n "${ANTHROPIC_API_KEY:-}" ] && is_oauth_token_pattern "$ANTHROPIC_API_KEY"; then DOCKER_ARGS+=("-e" "CLAUDE_CODE_OAUTH_TOKEN=$ANTHROPIC_API_KEY") AUTH_DETAILS="oauth-token (auto-detected from ANTHROPIC_API_KEY)" @@ -89,7 +93,10 @@ setup_claude_auth() { AUTH_DETAILS="api-key (ANTHROPIC_API_KEY)" else auth_error "No API key found for --auth-with api-key" \ - "Set: export ANTHROPIC_API_KEY=sk-ant-... or export CLAUDE_CODE_OAUTH_TOKEN=sk-ant-oat01-..." + "Set: export ANTHROPIC_API_KEY=sk-ant-..., export ANTHROPIC_AUTH_TOKEN=token, or export CLAUDE_CODE_OAUTH_TOKEN=sk-ant-oat01-..." + fi + if [ -n "${ANTHROPIC_BASE_URL:-}" ]; then + DOCKER_ARGS+=("-e" "ANTHROPIC_BASE_URL=$ANTHROPIC_BASE_URL") fi ;; copilot) @@ -122,6 +129,9 @@ setup_claude_auth() { fi AUTH_DETAILS="oauth-token (CLAUDE_CODE_OAUTH_TOKEN)" DOCKER_ARGS+=("-e" "CLAUDE_CODE_OAUTH_TOKEN=$CLAUDE_CODE_OAUTH_TOKEN") + if [ -n "${ANTHROPIC_BASE_URL:-}" ]; then + DOCKER_ARGS+=("-e" "ANTHROPIC_BASE_URL=$ANTHROPIC_BASE_URL") + fi ;; bedrock) AUTH_DETAILS="aws-bedrock (region: ${AWS_REGION:-default})" diff --git a/agents/shared_auth.sh b/agents/shared_auth.sh index 38e37d4..ba4a3e2 100644 --- a/agents/shared_auth.sh +++ b/agents/shared_auth.sh @@ -43,7 +43,7 @@ validate_github_token() { } validate_anthropic_key() { - [ -n "${ANTHROPIC_API_KEY:-}" ] || [ -n "${CLAUDE_CODE_OAUTH_TOKEN:-}" ] + [ -n "${ANTHROPIC_API_KEY:-}" ] || [ -n "${ANTHROPIC_AUTH_TOKEN:-}" ] || [ -n "${CLAUDE_CODE_OAUTH_TOKEN:-}" ] } validate_openai_key() { diff --git a/deva.sh b/deva.sh index d705e4a..907f7cf 100755 --- a/deva.sh +++ b/deva.sh @@ -784,28 +784,28 @@ should_skip_env_for_auth() { case "${AUTH_METHOD:-claude}" in claude) case "$name" in - ANTHROPIC_API_KEY | ANTHROPIC_BASE_URL | CLAUDE_CODE_OAUTH_TOKEN | OPENAI_API_KEY | OPENAI_BASE_URL | openai_base_url) + ANTHROPIC_API_KEY | ANTHROPIC_AUTH_TOKEN | ANTHROPIC_BASE_URL | CLAUDE_CODE_OAUTH_TOKEN | OPENAI_API_KEY | OPENAI_BASE_URL | openai_base_url) return 0 ;; esac ;; api-key | oat) case "$name" in - ANTHROPIC_API_KEY | ANTHROPIC_BASE_URL | CLAUDE_CODE_OAUTH_TOKEN | OPENAI_API_KEY | OPENAI_BASE_URL | openai_base_url) + ANTHROPIC_API_KEY | ANTHROPIC_AUTH_TOKEN | ANTHROPIC_BASE_URL | CLAUDE_CODE_OAUTH_TOKEN | OPENAI_API_KEY | OPENAI_BASE_URL | openai_base_url) return 0 ;; esac ;; copilot) case "$name" in - ANTHROPIC_API_KEY | ANTHROPIC_BASE_URL | CLAUDE_CODE_OAUTH_TOKEN) + ANTHROPIC_API_KEY | ANTHROPIC_AUTH_TOKEN | ANTHROPIC_BASE_URL | CLAUDE_CODE_OAUTH_TOKEN) return 0 ;; esac ;; bedrock | vertex | credentials-file) case "$name" in - ANTHROPIC_API_KEY | ANTHROPIC_BASE_URL | CLAUDE_CODE_OAUTH_TOKEN | OPENAI_API_KEY | OPENAI_BASE_URL | openai_base_url) + ANTHROPIC_API_KEY | ANTHROPIC_AUTH_TOKEN | ANTHROPIC_BASE_URL | CLAUDE_CODE_OAUTH_TOKEN | OPENAI_API_KEY | OPENAI_BASE_URL | openai_base_url) return 0 ;; esac @@ -938,20 +938,36 @@ mount_config_home() { [ -e "$item" ] || continue local name name="$(basename "$item")" - if [ "$name" = "." ] || [ "$name" = ".." ]; then + if ! should_mount_home_item "$item" "$name"; then continue fi DOCKER_ARGS+=(-v "$item:/home/deva/$name") done } -# Credential backup system. -# - .claude.json: always cp'd to XDG_STATE (outside mount tree, corruption protection). -# - Credential files: mv'd to tmpdir when auth override is detected. -# - Auth override = non-default --auth-with OR auth env vars reaching container. -# Entries are "orig_path:backup_path" pairs for restore. -BACKED_UP_CREDS=() -CRED_BACKUP_TMPDIR="" +should_mount_home_item() { + local item="$1" + local name="$2" + + case "$name" in + . | .. | .DS_Store | .git | .gitignore) + return 1 + ;; + .claude.json.backup | .claude.json.backup.* | .claude.json.bak.after-corrupted.*) + return 1 + ;; + *.credentials.json | auth.json | mcp-oauth-tokens-v2.json) + # Loose credential files should only enter the container through explicit auth mounts. + [ -f "$item" ] && return 1 + ;; + esac + + if [ -n "${CUSTOM_CREDENTIALS_FILE:-}" ] && [ "$item" = "$CUSTOM_CREDENTIALS_FILE" ]; then + return 1 + fi + + return 0 +} # Effective config base: where agent config dirs (.claude/, .codex/, .gemini/) live. resolve_config_base() { @@ -1004,18 +1020,10 @@ has_auth_override() { return 1 } -backup_credentials() { +backup_claude_json() { local config_base config_base=$(resolve_config_base) - local agent_dir cred_file - case "$ACTIVE_AGENT" in - claude) agent_dir=".claude"; cred_file=".credentials.json" ;; - codex) agent_dir=".codex"; cred_file="auth.json" ;; - gemini) agent_dir=".gemini"; cred_file="mcp-oauth-tokens-v2.json" ;; - *) return 0 ;; - esac - # .claude.json corruption backup: persistent, outside container mount tree. if [ "$ACTIVE_AGENT" = "claude" ]; then local state_dir="${XDG_STATE_HOME:-$HOME/.local/state}/deva/backups" @@ -1026,31 +1034,66 @@ backup_credentials() { fi fi - # Credential backup: tmpdir outside all mount trees. - if has_auth_override; then - local cred_path="$config_base/$agent_dir/$cred_file" - if [ -f "$cred_path" ]; then - CRED_BACKUP_TMPDIR=$(mktemp -d "${TMPDIR:-/tmp}/deva-cred.XXXXXX") - mv "$cred_path" "$CRED_BACKUP_TMPDIR/$cred_file" - BACKED_UP_CREDS+=("$cred_path:$CRED_BACKUP_TMPDIR/$cred_file") - echo "info: backed up $cred_file -> tmpdir (auth override active)" >&2 - fi +} + +default_credential_target_path() { + case "$ACTIVE_AGENT" in + claude) + printf '%s' "/home/deva/.claude/.credentials.json" + ;; + codex) + printf '%s' "/home/deva/.codex/auth.json" + ;; + gemini) + printf '%s' "/home/deva/.gemini/mcp-oauth-tokens-v2.json" + ;; + *) + return 1 + ;; + esac +} + +append_auth_credential_overlay() { + if ! has_auth_override; then + return + fi + + case "$ACTIVE_AGENT:$AUTH_METHOD" in + claude:credentials-file | codex:credentials-file) + # Explicit file mount already overlays the default auth file path. + return + ;; + esac + + local target_path + if ! target_path=$(default_credential_target_path); then + return + fi + + 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) + overlay_key="env" + ;; + esac + local overlay_file + overlay_file="$state_dir/$(workspace_hash).${overlay_key}.blank" + if [ "$DRY_RUN" != true ]; then + mkdir -p "$state_dir" + printf '{}\n' > "$overlay_file" fi + DOCKER_ARGS+=("-v" "$overlay_file:$target_path") } -restore_backed_up_credentials() { - local entry - for entry in "${BACKED_UP_CREDS[@]+"${BACKED_UP_CREDS[@]}"}"; do - local orig="${entry%%:*}" - local backup="${entry##*:}" - if [ -f "$backup" ]; then - mv "$backup" "$orig" - echo "info: restored $(basename "$orig")" >&2 - fi - done - if [ -n "$CRED_BACKUP_TMPDIR" ] && [ -d "$CRED_BACKUP_TMPDIR" ]; then - rm -rf "$CRED_BACKUP_TMPDIR" +mount_loose_home_item() { + local item="$1" + local name + name="$(basename "$item")" + if ! should_mount_home_item "$item" "$name"; then + return fi + DOCKER_ARGS+=(-v "$item:/home/deva/$name") } mount_dir_contents_into_home() { @@ -1059,12 +1102,7 @@ mount_dir_contents_into_home() { local item for item in "$base"/.* "$base"/*; do [ -e "$item" ] || continue - local name - name="$(basename "$item")" - if [ "$name" = "." ] || [ "$name" = ".." ]; then - continue - fi - DOCKER_ARGS+=(-v "$item:/home/deva/$name") + mount_loose_home_item "$item" done } @@ -2035,6 +2073,7 @@ fi autolink_legacy_into_deva_root() { [ "$AUTOLINK" = true ] || return 0 + [ "$DRY_RUN" != true ] || return 0 [ "$CONFIG_HOME_FROM_CLI" = false ] || return 0 [ -n "${CONFIG_ROOT:-}" ] || return 0 [ -d "$CONFIG_ROOT" ] || mkdir -p "$CONFIG_ROOT" @@ -2259,10 +2298,9 @@ DOCKER_ARGS+=(-e "DEVA_CONTAINER_NAME=${CONTAINER_NAME}") DOCKER_ARGS+=(-e "DEVA_WORKSPACE=$(pwd)") DOCKER_ARGS+=(-e "DEVA_EPHEMERAL=${EPHEMERAL_MODE}") -# Credential backup: move credential files to tmpdir before mounting. -# Skipped during --dry-run to avoid side effects (mkdir, cp, mv). +# Back up .claude.json before mounting, without touching live credential files. if [ "$QUICK_MODE" != true ] && [ "$DRY_RUN" != true ]; then - backup_credentials + backup_claude_json fi # Centralized mounting logic. @@ -2286,6 +2324,12 @@ else fi fi +# Hide default OAuth credential files for non-default auth modes. +# For credentials-file auth on Claude/Codex, the agent-specific file mount already overlays the path. +if [ "$QUICK_MODE" = false ]; then + append_auth_credential_overlay +fi + # Set statusline log paths via env vars (XDG-compliant) DOCKER_ARGS+=("-e" "CLAUDE_DATA_DIR=/home/deva/.config/deva/claude") DOCKER_ARGS+=("-e" "CLAUDE_CACHE_DIR=/home/deva/.cache/deva/claude/sessions") @@ -2335,7 +2379,7 @@ mask_secrets_in_args() { local name="${BASH_REMATCH[1]}" local value="${BASH_REMATCH[2]}" case "$name" in - *TOKEN*|*KEY*|*SECRET*|*PASSWORD*|*CREDENTIALS*|*BARK_KEY*) + *TOKEN*|*KEY*|*SECRET*|*PASSWORD*|*CREDENTIALS*) printf '%s= ' "$name" ;; *) @@ -2364,11 +2408,6 @@ if [ "$DEBUG_MODE" = true ]; then echo "" >&2 fi -# Restore backed-up credential files on exit (covers crashes, signals, normal exit) -if [ ${#BACKED_UP_CREDS[@]} -gt 0 ]; then - trap restore_backed_up_credentials EXIT -fi - if [ "$DRY_RUN" = true ]; then exit 0 fi @@ -2429,8 +2468,6 @@ if [ "$EPHEMERAL_MODE" = false ]; then update_session_file fi - # Restore credentials before exec (exec replaces the process, trap won't fire) - restore_backed_up_credentials exec docker exec -it "$CONTAINER_NAME" /usr/local/bin/docker-entrypoint.sh "${AGENT_COMMAND[@]}" else echo "Launching ${ACTIVE_AGENT} (ephemeral mode) via ${DEVA_DOCKER_IMAGE}:${DEVA_DOCKER_TAG}" From 33bedb95484b359e6fef302770dd0243584108b3 Mon Sep 17 00:00:00 2001 From: Eric Wang Date: Wed, 11 Mar 2026 20:26:24 -0700 Subject: [PATCH 4/6] chore: release v0.9.2 - formalize the repo with MIT, SECURITY, and CONTRIBUTING docs - rewrite README and fix installer module drift - ship auth token forwarding and safer credential overlays --- CHANGELOG.md | 18 ++ CONTRIBUTING.md | 71 +++++++ DEV-LOGS.md | 11 ++ LICENSE | 21 +++ README.md | 438 +++++++++++++++++-------------------------- SECURITY.md | 63 +++++++ deva.sh | 2 +- install.sh | 114 +++++------ workflows/RELEASE.md | 6 +- 9 files changed, 404 insertions(+), 340 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 SECURITY.md mode change 100755 => 100644 install.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 00b0405..f3d9929 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,24 @@ All notable changes to Claude Code YOLO will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.9.2] - 2026-03-11 + +### Added +- `LICENSE` with the standard MIT license text +- `SECURITY.md` with private vulnerability reporting guidance +- `CONTRIBUTING.md` with the repo workflow, local checks, and release rules + +### Fixed +- Claude `--auth-with api-key` now forwards `ANTHROPIC_AUTH_TOKEN` and `ANTHROPIC_BASE_URL` +- Non-default auth no longer moves live host credential files out of the way; it overlays the default credential path with a safe placeholder instead +- `--dry-run` no longer mutates config homes through autolink or scaffold writes +- Config-home fan-out skips loose credential files, backup files, VCS junk, and `.DS_Store` +- `install.sh` now installs the full current agent set, including Gemini and `shared_auth.sh` + +### Changed +- Rewrote `README.md` into a cleaner OSS landing page with badges, quick start, auth matrix, and security warnings +- Updated `workflows/RELEASE.md` to use `deva.sh` as the source of truth for version bumps + ## [0.9.1] - 2026-01-09 ### Fixed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..690bbce --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,71 @@ +# Contributing + +Thanks. Keep it tight. + +## Before You Send Anything + +- open or find the issue first +- keep one branch per issue +- read the workflow docs in `workflows/` + +Use these, not your imagination: + +- `workflows/GITHUB-ISSUE.md` +- `workflows/GITHUB-PR.md` +- `workflows/GIT-COMMIT.md` +- `workflows/RELEASE.md` + +## Local Checks + +Run the obvious stuff before you ask anyone else to look: + +```bash +./deva.sh --help +./deva.sh --version +./claude-yolo --help +./scripts/version-check.sh +shellcheck deva.sh agents/*.sh docker-entrypoint.sh install.sh scripts/*.sh +``` + +If you change Docker image behavior, auth flows, or release logic, test those paths directly. Do not ship "should work". + +## What We Want + +- small, focused changes +- direct docs +- boring shell scripts that still work tomorrow +- explicit auth and mount behavior +- no surprise regressions + +## What We Do Not Want + +- prompt-engineering fluff in docs +- magical wrappers around simple shell code +- untested auth changes +- random formatting churn +- force-push chaos on shared branches + +## Docs Rules + +Update docs when behavior changes: + +- `README.md` for user-facing workflow +- `CHANGELOG.md` for release notes +- `DEV-LOGS.md` for significant work +- `SECURITY.md` when the reporting path or threat model changes + +## Pull Requests + +A good PR does three things: + +1. says what changed +2. says why it changed +3. says how you tested it + +If it touches auth, container boundaries, or release mechanics, include the exact command you ran. + +## Releases + +Do not freestyle releases. + +Follow `workflows/RELEASE.md`. Update version, changelog, and docs together, then tag the release. If the tree is dirty and you do not understand why, stop. diff --git a/DEV-LOGS.md b/DEV-LOGS.md index 48ac20d..70f1681 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-03-11] Dev Log: OSS repo polish and auth mount cleanup +- Why: the repo still looked half-finished in public, the installer lagged behind the actual agent set, and recent auth switching work exposed ugly mount behavior +- What: + - added `LICENSE`, `SECURITY.md`, and `CONTRIBUTING.md` + - rewrote `README.md` into a cleaner OSS landing page with badges, quick start, auth, config-home, and security sections + - fixed `install.sh` to install `gemini.sh` and `shared_auth.sh`, and cleaned the installer output + - fixed Claude `--auth-with api-key` to pass `ANTHROPIC_AUTH_TOKEN` and `ANTHROPIC_BASE_URL` + - replaced credential backup/restore with auth-file overlay mounts, filtered junk from config-home fan-out, and stopped `--dry-run` from writing files + - fixed `workflows/RELEASE.md` to use `deva.sh` as the version source +- Result: the repo now reads like an actual OSS project, fresh installs match the current feature set, and auth switching is less fragile ahead of the 0.9.2 release + # [2026-01-07] Dev Log: Fix version-upgrade build resilience - Why: `make versions-up` exited 56 during GitHub API changelog fetch - GitHub API 403 rate limit (60/hour) from unauthenticated curl diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6a657e9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 The Vibe Works + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 0922107..a605a63 100644 --- a/README.md +++ b/README.md @@ -1,350 +1,254 @@ -# deva.sh Multi-Agent Wrapper +# deva -> **REBRANDED**: Claude Code YOLO → **deva.sh Multi-Agent Wrapper** -> We've evolved from a Claude-specific wrapper into a unified development environment supporting Claude Code, OpenAI Codex, and future coding agents. +[![CI](https://img.shields.io/github/actions/workflow/status/thevibeworks/deva/ci.yml?branch=main&label=ci)](https://github.com/thevibeworks/deva/actions/workflows/ci.yml) +[![Release](https://img.shields.io/github/v/release/thevibeworks/deva?sort=semver)](https://github.com/thevibeworks/deva/releases) +[![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) -deva.sh launches Claude Code, Codex, and future coding agents inside a Docker container with **full control** over mounts, environments, and authentication contexts. +Run Claude Code, Codex, and Gemini inside Docker. -**Key Features**: -- **Advanced Directory Access**: Beyond Claude's `--add-dir` - mount any directories with precise permissions (`-v`) -- **Multiple Config Homes**: Isolated auth contexts with `--config-home` for different accounts/orgs -- **Multi-Auth Support**: OAuth, API keys, Bedrock, Vertex, Copilot - all in one wrapper -- **Safe Dangerous Mode**: Full permissions inside containers, zero host risk +The container is the sandbox. That is the whole trick. -**What Changed**: `claude.sh` → `deva.sh` as the primary interface. Your existing `claude-yolo` commands still work via compatibility shims. +## What This Is -## Quick Start +`deva.sh` launches coding agents inside a reusable Docker container with: -```bash -# install the CLI wrapper -curl -fsSL https://raw.githubusercontent.com/thevibeworks/claude-code-yolo/main/install.sh | bash +- explicit volume mounts instead of blind filesystem access +- isolated auth homes with `--config-home` +- persistent per-project containers by default +- support for Claude, Codex, and Gemini in the same wrapper -# run from a project directory -cd ~/wrk/my-project +Legacy commands still exist: -deva.sh # Claude by default +- `deva.sh` is the real entry point +- `claude.sh` and `claude-yolo` are compatibility wrappers -deva.sh codex -- --help # Codex CLI passthrough -``` +## What This Is Not -`claude-yolo` simply calls `deva.sh claude` for backwards compatibility. +- Not a security boundary if you mount `/var/run/docker.sock`. That is root-equivalent on the host. +- Not safe for random untrusted repos just because it says "Docker". +- Not a PaaS, orchestrator, or generic devcontainer manager. -> **Note** -> The older `claude.sh` / `claudeb.sh` wrappers are deprecated and now print a reminder before forwarding to `deva.sh`. +If you point this thing at your real home directory and hand it dangerous permissions, you did not discover a clever workflow. You discovered a foot-gun. -## Multi-Agent Capabilities +## Why This Exists -**Supported Agents**: -- **Claude Code** (`deva.sh claude`) - Anthropic's Claude with Code capabilities, auto-adds `--dangerously-skip-permissions` -- **OpenAI Codex** (`deva.sh codex`) - OpenAI's Codex CLI, auto-adds `--dangerously-bypass-approvals-and-sandbox` +Claude Code, Codex, and friends are useful. Their native local security prompts are also annoying in trusted workspaces. -**Agent-Specific Features**: -- **Claude**: OAuth/API key auth, mounts `~/.claude`, project-specific `.claude` configs -- **Codex**: OAuth protection (strips conflicting `OPENAI_*` env vars), exposes port 1455, mounts `~/.codex` +`deva` moves the risk boundary: -**Shared Infrastructure**: -- Project-scoped containers (`deva---`) -- Unified config system (`.deva*` files) -- Container management (`--ps`, `--inspect`, `shell`) +- agent gets broad power inside the container +- you decide exactly what is mounted from the host +- auth can be swapped per project or per org +- one warm container can serve multiple agents -## Safety Checklist +That is a better trade when you actually know what you are doing. -- Always `cd` into the project root before launching; deva.sh mounts the working directory recursively. -- Never aim `--config-home` at your real `$HOME`; use a dedicated auth directory. -- Use `deva.sh --ps` to confirm which containers are running before attaching. +## Quick Start + +```bash +curl -fsSL https://raw.githubusercontent.com/thevibeworks/deva/main/install.sh | bash -## Advanced Directory Access (Beyond `--add-dir`) +cd ~/work/my-project +deva.sh claude +``` -**Claude's `--add-dir` vs deva.sh's Military-Grade Control**: +A few more sane first commands: ```bash -# Claude built-in: Dangerous direct filesystem access -claude --add-dir ~/projects/backend --add-dir ~/shared-libs -# ❌ Claude can read/write EVERYTHING in those directories -# ❌ No permission control -# ❌ Direct access to your real files -# ❌ Can accidentally modify/delete host files - -# deva.sh: Fortress-level security with surgical precision -deva.sh claude \ - -v ~/projects/backend:/home/deva/backend:ro \ # READ-ONLY - -v ~/shared-libs:/home/deva/libs:ro \ # READ-ONLY - -v ~/api-keys:/home/deva/secrets:ro \ # READ-ONLY SECRETS - -v /tmp/build-output:/home/deva/output:rw # ONLY writable area -# ✅ Granular per-directory permissions -# ✅ Container isolation = zero host risk -# ✅ Secrets are read-only, impossible to leak -# ✅ Only designated output dir is writable +deva.sh codex -- --help +deva.sh gemini -- --help +deva.sh shell +deva.sh --ps ``` -**Why deva.sh's Volume Mounting is Vastly More Secure & Controllable**: -- **Granular Permissions**: Per-directory read-only (`ro`) vs read-write (`rw`) vs no access -- **Complete Isolation**: Claude never touches your real filesystem - only container copies -- **Selective Exposure**: Choose exactly which files/dirs are visible, hide everything else -- **Path Remapping**: Control exactly where files appear in container (`/home/deva/project` vs `~/my-secret-project`) -- **Zero Host Risk**: Even with `--dangerously-skip-permissions`, your host filesystem is protected -- **Credential Sandboxing**: Mount secrets read-only, impossible for Claude to modify/leak them -- **Audit Trail**: Docker logs every file access, unlike native `--add-dir` -- **Performance**: Docker volume caching + no host filesystem traversal - -**Real-World Security Scenarios**: -```bash -# MAXIMUM SECURITY: Code review with zero risk -deva.sh claude \ - -v ~/client-project:/home/deva/code:ro \ # REVIEW ONLY - -v ~/api-keys:/home/deva/keys:ro \ # SECRETS READ-ONLY - -v /tmp/review-output:/home/deva/output:rw # SAFE OUTPUT AREA -# Claude can analyze code but CANNOT modify source or leak secrets +## Install -# SURGICAL ACCESS: Database migration with controlled risk -deva.sh claude \ - -v ~/migrations:/home/deva/migrations:ro \ # SCRIPTS READ-ONLY - -v ~/.db-config:/home/deva/config:ro \ # CONFIG READ-ONLY - -v /tmp/migration-logs:/home/deva/logs:rw # LOGS ONLY -# Claude can read configs but CANNOT modify production scripts +Requirements: -# CREDENTIAL FORTRESS: API development with bulletproof secrets -deva.sh claude \ - -v ~/project/src:/home/deva/src:rw \ # CODE ACCESS - -v ~/.aws:/home/deva/.aws:ro \ # AWS CREDS READ-ONLY - -v ~/api-keys:/home/deva/keys:ro \ # API KEYS READ-ONLY - -v /tmp/build:/home/deva/build:rw # BUILD OUTPUT ONLY -# Impossible for Claude to accidentally commit secrets or modify credentials - -# Compare with dangerous --add-dir approach: -# claude --add-dir ~/project --add-dir ~/.aws --add-dir ~/api-keys -# ❌ Claude has FULL WRITE ACCESS to ALL your secrets! -``` +- Docker +- a supported agent auth method +- a trusted workspace -**XDG Config Home (Per-Agent)**: -```bash -$XDG_CONFIG_HOME defaults to ~/.config +The installer drops these into your PATH: -# Default layout (each agent directory under ~/.config/deva mounts into /home/deva): +- `deva.sh` +- `claude.sh` +- `claude-yolo` +- `agents/claude.sh` +- `agents/codex.sh` +- `agents/gemini.sh` +- `agents/shared_auth.sh` -~/.config/deva/ -├── claude/ # Claude-only auth/config -│ ├── .claude/ -│ ├── .claude.json -│ ├── .aws/ # optional: Bedrock creds -│ └── .config/gcloud/ # optional: Vertex AI creds -└── codex/ # Codex-only auth/config - └── .codex/ # contains auth.json +If Docker pull from GHCR fails, the installer falls back to Docker Hub. -# Mount destinations inside container: -# - claude/* → /home/deva/* -# - codex/* → /home/deva/* +## Common Commands -# Override per invocation -deva.sh claude -c ~/auth-homes/personal -deva.sh codex -c ~/auth-homes/codex-prod -- -m gpt-5-codex -deva.sh claude -c ~/auth-homes/client-aws --auth-with bedrock -``` +```bash +# Claude in the persistent project container +deva.sh claude -Notes: -- By default, we mount all agent homes found under `~/.config/deva/` so you can run multiple agents in the same container. -- If you pass `-c` to a directory that contains `claude/` and/or `codex/`, we treat it as a DEVA ROOT and mount all agent homes found there. -- If `-c` points to a leaf directory with dotfiles directly (e.g., only `.claude*`), we mount only that directory. +# One-shot run, auto-remove container +deva.sh claude --rm -Migration from legacy dotfiles: -```bash -# 1) Create the new XDG tree -mkdir -p ~/.config/deva/{claude,codex} +# Add a read-only mount +deva.sh claude -v ~/.ssh:/home/deva/.ssh:ro -# 2) Move Claude Code OAuth files -if [ -d ~/.claude ]; then mv ~/.claude ~/.config/deva/claude/.claude; fi -if [ -f ~/.claude.json ]; then mv ~/.claude.json ~/.config/deva/claude/.claude.json; fi +# Use a separate auth home +deva.sh claude -c ~/auth-homes/work -# 3) Move Codex OAuth directory -if [ -d ~/.codex ]; then mv ~/.codex ~/.config/deva/codex/.codex; fi +# Claude with API or token-based auth +deva.sh claude --auth-with api-key -- -p "say hi" -# 4) Optional: Bedrock / Vertex creds (choose one spot) -# Either keep per-agent: -if [ -d ~/.aws ]; then cp -a ~/.aws ~/.config/deva/claude/.aws; fi -if [ -d ~/.config/gcloud ]; then mkdir -p ~/.config/deva/claude/.config && \ - cp -a ~/.config/gcloud ~/.config/deva/claude/.config/; fi +# Claude with Bedrock +deva.sh claude -c ~/auth-homes/aws --auth-with bedrock -# 5) Verify -tree -a ~/.config/deva | sed -n '1,200p' +# Codex with explicit model +deva.sh codex -- -m gpt-5-codex -# Done. Now just run -deva.sh claude -deva.sh codex +# Gemini in the same project container +deva.sh gemini ``` -**Symlink Setup** +## Auth Modes -Prefer links if you don’t want to move files. Docker resolves symlinks at start and binds the target. +| Agent | Default auth | Other auth modes | +| --- | --- | --- | +| Claude | `claude` | `api-key`, `oat`, `bedrock`, `vertex`, `copilot`, credentials file path | +| Codex | `chatgpt` | `api-key`, `copilot`, credentials file path | +| Gemini | `oauth` | `api-key`, `gemini-api-key`, `vertex`, `compute-adc`, credentials file path | -```bash -# Link specific files/dirs into deva root -ln -s ~/.claude ~/.config/deva/claude/.claude -ln -s ~/.claude.json ~/.config/deva/claude/.claude.json -ln -s ~/.codex ~/.config/deva/codex/.codex - -# Or link whole agent directories (cleaner) -ln -s ~/auth-homes/claude ~/.config/deva/claude -ln -s ~/auth-homes/codex ~/.config/deva/codex - -# Verify symlinks resolve -ls -l ~/.config/deva/claude -readlink -f ~/.config/deva/claude/.claude -``` +Examples: -Notes: -- Use absolute paths for links. Relative is fine if it resolves correctly from the link’s parent. -- The symlink’s target must exist, or the container mount will fail. -- Changing the link after the container starts won’t retarget the running bind; restart to pick up changes. -- macOS: ensure the target paths are allowed under Docker Desktop File Sharing. - -Auto-linking (default) -- On first run with the default XDG root (`~/.config/deva`) and no explicit `-c`, we auto-link legacy creds if the deva root is missing them: - - `~/.claude` → `~/.config/deva/claude/.claude` - - `~/.claude.json` → `~/.config/deva/claude/.claude.json` - - `~/.codex` → `~/.config/deva/codex/.codex` -- Disable with any of: - - CLI: `--no-autolink` - - Config: add `AUTOLINK=false` to `.deva` - - Env: `DEVA_NO_AUTOLINK=1` (export and run) - -**Container Management**: ```bash -# List all running containers for this project -% deva.sh --ps -NAME AGENT STATUS CREATED AT -deva-claude-my-project-12345 claude Up 2 minutes 2025-09-18 18:10:02 +0000 UTC -deva-codex-my-project-67890 codex Up 5 minutes 2025-09-18 18:07:15 +0000 UTC - -# Attach to any container (fzf picker if multiple) -deva.sh --inspect -deva.sh shell # alias for --inspect -``` +# Claude OAuth from config home +deva.sh claude -c ~/auth-homes/personal -## Multi-Account Auth Architecture +# Claude via custom HTTP endpoint + token +export ANTHROPIC_BASE_URL=https://example.net/api +export ANTHROPIC_AUTH_TOKEN=token +deva.sh claude --auth-with api-key -**Config Home Structure** (`--config-home DIR` / `-c DIR`): +# Codex via OpenAI API key +export OPENAI_API_KEY=sk-... +deva.sh codex --auth-with api-key -deva.sh mounts entire auth directories into `/home/deva`, enabling **isolated authentication contexts** for different accounts, organizations, or projects. +# Gemini via service account file +deva.sh gemini --auth-with ~/keys/gcp-service-account.json +``` -```bash -# Organize by account type -~/auth-homes/ -├── personal/ # Personal accounts +## Config Homes + +By default, deva uses per-agent homes under `~/.config/deva/`. + +```text +~/.config/deva/ +├── claude/ │ ├── .claude/ │ ├── .claude.json -│ └── .config/gcloud/ -├── work-corp/ # Corporate accounts -│ ├── .claude/ # Work Claude Pro -│ ├── .aws/ # AWS Bedrock access -│ └── .codex/ # OpenAI org license -└── client-proj/ # Client-specific - ├── .claude/ # Client Claude account - └── .aws/ # Client AWS Bedrock - -# Use different auth contexts seamlessly -deva.sh claude -c ~/auth-homes/personal -deva.sh claude -c ~/auth-homes/work-corp --auth-with bedrock -deva.sh codex -c ~/auth-homes/work-corp +│ ├── .aws/ # optional +│ └── .config/gcloud/ # optional +├── codex/ +│ └── .codex/ +└── gemini/ + └── .gemini/ ``` -**Auth Protection**: When `.codex/auth.json` is mounted, deva.sh strips conflicting `OPENAI_*` env vars to ensure OAuth sessions aren't shadowed by stale API credentials. +`--config-home DIR` supports two layouts: + +- leaf home: `DIR` contains `.claude`, `.claude.json`, `.aws`, `.config`, and so on +- deva root: `DIR` contains `claude/`, `codex/`, `gemini/` -## Container Management +Default runs also auto-link legacy auth dirs into `~/.config/deva/` unless you disable that with `--no-autolink`, `AUTOLINK=false`, or `DEVA_NO_AUTOLINK=1`. -- `deva.sh --ps` – list `deva-*` containers scoped to the current project (includes inferred agent column). -- `deva.sh --inspect` / `deva.sh shell` – attach to a running container (`fzf` picker if more than one, otherwise auto-attach). +## Container Model -Container naming pattern: `deva---`. +Persistent is the default. -## Config Files +- one container per project +- reused across runs +- faster warm starts +- one workspace can host Claude, Codex, and Gemini together -We load configuration in this order (later wins): +Ephemeral mode: -1. `$XDG_CONFIG_HOME/deva/.deva` -2. `$HOME/.deva` -3. `.deva` -4. `.deva.local` -5. Legacy `.claude-yolo*` files (still honoured for compatibility) +- `--rm` creates a throwaway container +- `-Q` is the bare mode: no config loading, no autolink, no host config mounts -Example `.deva` file: +Useful management commands: ```bash -VOLUME=~/.ssh:/home/deva/.ssh:ro -VOLUME=~/.gitconfig:/home/deva/.gitconfig:ro -ENV=DEBUG=${DEBUG:-development} -ENV=GH_TOKEN -CONFIG_HOME=~/auth-homes/claude-max -DEFAULT_AGENT=claude -PROFILE=rust # pick a dev profile (same as -p rust) -HOST_NET=false +deva.sh --ps +deva.sh shell +deva.sh stop +deva.sh rm +deva.sh clean ``` -Supported keys: `VOLUME`, `ENV`, `CONFIG_HOME`, `DEFAULT_AGENT`, `HOST_NET`, plus any valid shell variable names you want exported. +## Profiles -## Flexible Authentication Matrix +Profiles choose the image tag: -**All Authentication Methods Supported**: +- `base` -> `ghcr.io/thevibeworks/deva:latest` +- `rust` -> `ghcr.io/thevibeworks/deva:rust` -| Agent | Auth Method | Command Example | Auth Context | -|-------|-------------|-----------------|--------------| -| **Claude** | OAuth | `deva.sh claude -c ~/auth-homes/personal` | `.claude/`, `.claude.json` | -| **Claude** | API Key | `deva.sh claude --auth-with api-key` | `ANTHROPIC_API_KEY` | -| **Claude** | Bedrock | `deva.sh claude -c ~/auth-homes/aws --auth-with bedrock` | `.aws/` credentials | -| **Claude** | Vertex AI | `deva.sh claude -c ~/auth-homes/gcp --auth-with vertex` | `.config/gcloud/` | -| **Claude** | Copilot | `deva.sh claude --auth-with copilot` | GitHub token via copilot-api | -| **Claude** | OAuth Token | `deva.sh claude -- --auth-with oat -p "task"` | `CLAUDE_CODE_OAUTH_TOKEN` | -| **Codex** | OAuth | `deva.sh codex -c ~/auth-homes/openai` | `.codex/auth.json` | +Examples: -**Multi-Org Support Examples**: ```bash -# Personal dev work -deva.sh claude -c ~/auth-homes/personal - -# Corporate Bedrock account -deva.sh claude -c ~/auth-homes/corp-aws --auth-with bedrock - -# Client project with their OpenAI license -deva.sh codex -c ~/auth-homes/client-openai - -# Quick API key for testing -ANTHROPIC_API_KEY=sk-... deva.sh claude -- --auth-with api-key -p "test this" +deva.sh claude -p rust +make build +make build-rust ``` -## Image Contents +## Security Model -- Languages: Python 3.12, Node.js 22, Go 1.22, Rust. -- Tooling: git, gh, docker CLI, awscli, bun, uv, ripgrep, shellcheck, claude-trace, codex CLI, etc. -- User: non-root `deva` with host UID/GID mirroring. -- Networking: `localhost` → `host.docker.internal` rewrites for HTTP/HTTPS/gRPC. +Be honest about the sharp edges: -## Developer Notes +- mounting `docker.sock` is host root by another name +- `--host-net` gives broad network visibility +- `--dangerously-skip-permissions` is still dangerous; Docker just changes where the blast radius lands +- `--config-home` should point to a dedicated auth home, not your real `$HOME` + +If you need locked-down review mode, use explicit read-only mounts: ```bash -make build # build the deva image locally -make shell # open an interactive shell inside the image -./deva.sh --help # list all options +deva.sh claude \ + -v ~/src/project:/home/deva/project:ro \ + -v ~/.ssh:/home/deva/.ssh:ro \ + -v /tmp/deva-out:/home/deva/out ``` -See `CHANGELOG.md` and `DEV-LOGS.md` in this directory for history and daily notes. +Security policy lives in [SECURITY.md](SECURITY.md). + +## Repo Layout -## Image Profiles and Local Dockerfiles +```text +deva.sh main launcher +agents/ agent-specific auth and command wiring +Dockerfile* container images +workflows/ issue, commit, PR, release conventions +.github/workflows/ CI and release automation +scripts/ release and helper scripts +``` + +## Development -Select a development image profile (deva flags can appear before or after the agent): +Local sanity checks: ```bash -# Use base image (default) -deva.sh claude +./deva.sh --help +./deva.sh --version +./claude-yolo --help +./scripts/version-check.sh +shellcheck deva.sh agents/*.sh docker-entrypoint.sh install.sh scripts/*.sh +``` -# Use rust toolchain image (tries pull, then local Dockerfile.rust if present) -deva.sh -p rust claude -deva.sh claude -p rust # equivalent +Contributing guide: [CONTRIBUTING.md](CONTRIBUTING.md) -# If the tag isn't available locally and pull fails, build it -make build-rust # or: docker build -f Dockerfile.rust -t ghcr.io/thevibeworks/deva:rust . -``` +Changelog: [CHANGELOG.md](CHANGELOG.md) + +Dev notes: [DEV-LOGS.md](DEV-LOGS.md) -Profiles map to images: -- base → `ghcr.io/thevibeworks/deva:latest` -- rust → `ghcr.io/thevibeworks/deva:rust` (falls back to local `Dockerfile.rust` when available) +## License -You can also pin a custom image via env: `DEVA_DOCKER_IMAGE`, `DEVA_DOCKER_TAG`. +MIT. See [LICENSE](LICENSE). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..933a9bd --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,63 @@ +# Security Policy + +## Supported Versions + +We support: + +| Version | Status | +| --- | --- | +| latest release | supported | +| `main` | best effort | +| older tags | no guarantees | + +If you are filing a security report against an old tag, reproduce it on the latest release first. + +## Report a Vulnerability + +Do not open a public GitHub issue for security problems. + +Preferred path: + +- GitHub private vulnerability reporting, if it is enabled for the repo + +Fallback: + +- email: `wrqatw@gmail.com` + +Include: + +- affected version or commit +- exact command or config that triggers the problem +- what the impact is +- whether secrets, host files, or container boundaries are involved +- logs, screenshots, or proof-of-concept if you have them + +## What Counts + +We care about: + +- container escape or host privilege escalation +- auth bypass or auth mix-up +- secret leakage +- unsafe default mounts +- command injection +- release or installer supply-chain issues + +We care less about: + +- theoretical issues with no realistic exploit path +- self-inflicted damage from mounting your whole home and then giving the agent full power + +That second one is not a clever exploit. That is just bad operational judgment. + +## Response Expectations + +Best effort, not corporate theater. + +- acknowledgement target: within 7 days +- status updates when there is real progress +- coordinated disclosure after a fix lands + +## Safe Harbor + +If you act in good faith, avoid data destruction, and do not exfiltrate other people's data, we will treat your report as research, not abuse. diff --git a/deva.sh b/deva.sh index 907f7cf..5fb9367 100755 --- a/deva.sh +++ b/deva.sh @@ -13,7 +13,7 @@ if [ -n "${DEVA_DOCKER_TAG+x}" ]; then DEVA_DOCKER_TAG_ENV_SET=true fi -VERSION="0.9.1" +VERSION="0.9.2" DEVA_DOCKER_IMAGE="${DEVA_DOCKER_IMAGE:-ghcr.io/thevibeworks/deva}" DEVA_DOCKER_TAG="${DEVA_DOCKER_TAG:-latest}" DEVA_CONTAINER_PREFIX="${DEVA_CONTAINER_PREFIX:-deva}" diff --git a/install.sh b/install.sh old mode 100755 new mode 100644 index 6a8f7ff..f906456 --- a/install.sh +++ b/install.sh @@ -1,113 +1,89 @@ #!/bin/bash -set -e +set -euo pipefail -# deva Multi-Agent Development Environment Installer - -SCRIPT_NAME="claude.sh" -YOLO_WRAPPER="claude-yolo" DEVA_LAUNCHER="deva.sh" +LEGACY_WRAPPER="claude.sh" +YOLO_WRAPPER="claude-yolo" DOCKER_IMAGE="ghcr.io/thevibeworks/deva:latest" +DOCKER_IMAGE_FALLBACK="thevibeworks/deva:latest" GITHUB_RAW="https://raw.githubusercontent.com/thevibeworks/deva/main" -echo "deva Multi-Agent Environment Installer" -echo "==========================" +agent_files=( + "claude.sh" + "codex.sh" + "gemini.sh" + "shared_auth.sh" +) + +echo "deva installer" +echo "==============" echo "" if [ -d "$HOME/.local/bin" ] && [[ ":$PATH:" == *":$HOME/.local/bin:"* ]]; then INSTALL_DIR="$HOME/.local/bin" - echo "Installing to: $INSTALL_DIR (user directory)" elif [ -w "/usr/local/bin" ]; then INSTALL_DIR="/usr/local/bin" - echo "Installing to: $INSTALL_DIR (system directory)" else INSTALL_DIR="$HOME/.local/bin" - echo "Installing to: $INSTALL_DIR (user directory)" - echo "Creating $INSTALL_DIR..." mkdir -p "$INSTALL_DIR" - - # Check if ~/.local/bin is in PATH if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then echo "warning: $INSTALL_DIR is not in PATH" - echo "Add this to your shell profile:" + echo "add this to your shell profile:" echo " export PATH=\"\$HOME/.local/bin:\$PATH\"" echo "" fi fi -# Download claude.sh -echo "Downloading claude.sh script..." -curl -fsSL "$GITHUB_RAW/claude.sh" -o "$INSTALL_DIR/$SCRIPT_NAME" -chmod +x "$INSTALL_DIR/$SCRIPT_NAME" +echo "Installing to: $INSTALL_DIR" -# Download claude-yolo script -echo "Downloading claude-yolo script..." -curl -fsSL "$GITHUB_RAW/claude-yolo" -o "$INSTALL_DIR/$YOLO_WRAPPER" -chmod +x "$INSTALL_DIR/$YOLO_WRAPPER" +download() { + local path="$1" + local dest="$2" + curl -fsSL "$GITHUB_RAW/$path" -o "$dest" + chmod +x "$dest" +} -# Download deva.sh dispatcher -echo "Downloading deva.sh multi-agent launcher..." -curl -fsSL "$GITHUB_RAW/deva.sh" -o "$INSTALL_DIR/$DEVA_LAUNCHER" -chmod +x "$INSTALL_DIR/$DEVA_LAUNCHER" +echo "Downloading launchers..." +download "$LEGACY_WRAPPER" "$INSTALL_DIR/$LEGACY_WRAPPER" +download "$YOLO_WRAPPER" "$INSTALL_DIR/$YOLO_WRAPPER" +download "$DEVA_LAUNCHER" "$INSTALL_DIR/$DEVA_LAUNCHER" -# Download agent modules echo "Downloading agent modules..." mkdir -p "$INSTALL_DIR/agents" -curl -fsSL "$GITHUB_RAW/agents/claude.sh" -o "$INSTALL_DIR/agents/claude.sh" -curl -fsSL "$GITHUB_RAW/agents/codex.sh" -o "$INSTALL_DIR/agents/codex.sh" -chmod +x "$INSTALL_DIR/agents/claude.sh" -chmod +x "$INSTALL_DIR/agents/codex.sh" +for file in "${agent_files[@]}"; do + download "agents/$file" "$INSTALL_DIR/agents/$file" +done -# Pull Docker image echo "" echo "Pulling Docker image..." if ! docker pull "$DOCKER_IMAGE"; then - echo "Failed to pull from GitHub Container Registry, trying Docker Hub..." - DOCKER_IMAGE_FALLBACK="thevibeworks/deva:latest" + echo "GHCR pull failed. Trying Docker Hub..." docker pull "$DOCKER_IMAGE_FALLBACK" echo "" - echo -e "\033[93mNOTE: Using Docker Hub fallback image\033[0m" - echo "To use Docker Hub by default, set: export DEVA_DOCKER_IMAGE=thevibeworks/deva" - echo "Add this to your shell profile (.bashrc, .zshrc) to make it permanent" + echo "warning: using Docker Hub fallback image" + echo "set this if you want Docker Hub by default:" + echo " export DEVA_DOCKER_IMAGE=thevibeworks/deva" fi -# Success message echo "" -echo "✓ Installation complete!" +echo "Install complete." echo "" -echo "Scripts installed to: $INSTALL_DIR" +echo "Installed:" echo " - $INSTALL_DIR/deva.sh" echo " - $INSTALL_DIR/claude.sh" 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/shared_auth.sh" echo "" -echo "Commands available:" -echo "==================" -echo "" -echo " claude.sh - Full Claude wrapper with all options" -echo " claude-yolo - Quick alias for 'claude.sh --yolo'" -echo " deva.sh - Multi-agent Docker launcher (Claude, Codex)" -echo "" -echo "Quick Start:" -echo "============" -echo "" -echo "1. Make sure Docker is running" -echo "" -echo "2. Navigate to your project directory:" -echo " cd ~/projects/my-project" -echo "" -echo "3. Start with deva.sh or Claude YOLO:" -echo " claude-yolo # Run Claude with full permissions" -echo " deva.sh run codex -m gpt-5-codex # Launch Codex agent" -echo "" -echo "4. Claude YOLO still supports Claude-specific flags:" -echo " claude-yolo --auth-with bedrock # Use AWS Bedrock" -echo " claude-yolo --auth-with api-key # Use API key (may have to rerun \`/login\`)" -echo " claude-yolo --trace # Enable request tracing" -echo " claude-yolo -v ~/.ssh:/home/deva/.ssh:ro # Mount SSH keys" -echo " claude-yolo --continue/--resume # Resume conversation" -echo " claude.sh --help # Advanced options reference" -echo " deva.sh shell codex # Enter the Codex container" -echo "" -echo -e "\033[93mWARNING: Never run --yolo in your home directory or system directories!\033[0m" +echo "Quick start:" +echo " 1. Make sure Docker is running" +echo " 2. cd into a project" +echo " 3. Run one of:" +echo " deva.sh claude" +echo " deva.sh codex -- --help" +echo " deva.sh gemini -- --help" +echo " deva.sh shell" echo "" +echo "warning: do not point deva at your real home directory with dangerous permissions enabled" diff --git a/workflows/RELEASE.md b/workflows/RELEASE.md index eb73298..9194d19 100644 --- a/workflows/RELEASE.md +++ b/workflows/RELEASE.md @@ -5,10 +5,10 @@ Execute when user requests: `patch`, `minor`, or `major` release. ## Steps 1. **Prerequisites**: Clean git, synced upstream, CI passing -2. **Current version**: Extract from `claude.sh` VERSION variable +2. **Current version**: Extract from `deva.sh` VERSION variable 3. **New version**: Increment per semver (patch/minor/major) 4. **Changelog**: Generate from `git log --oneline --no-merges v{last}..HEAD` -5. **Update files**: `claude.sh` VERSION, `CHANGELOG.md` entry +5. **Update files**: `deva.sh` VERSION, `CHANGELOG.md` entry 6. **Commit**: `chore: release v{version}` 7. **Tag & push**: `git tag -a v{version} && git push --tags` 8. **Verify**: Show final git log confirmation @@ -25,4 +25,4 @@ Execute when user requests: `patch`, `minor`, or `major` release. git reset --hard HEAD~1 # if not pushed git tag -d v{version} # delete local tag git push origin :v{version} # delete remote tag -``` \ No newline at end of file +``` From a69ce81b86e298d1c042540c62879005d3684101 Mon Sep 17 00:00:00 2001 From: Eric Wang Date: Wed, 11 Mar 2026 21:19:51 -0700 Subject: [PATCH 5/6] docs: add MkDocs documentation site with GitHub Pages deployment - Add comprehensive docs pages (quick-start, auth, troubleshooting, etc.) - Add GitHub Pages workflow and CI docs build job - Streamline README by moving detailed content to docs site --- .github/workflows/ci.yml | 20 +++ .github/workflows/pages.yml | 60 +++++++ .gitignore | 3 + CHANGELOG.md | 9 +- CONTRIBUTING.md | 3 +- DEV-LOGS.md | 12 ++ README.md | 261 ++++++++++------------------- agents/claude.sh | 8 +- agents/codex.sh | 8 +- deva.sh | 14 +- docs-requirements.txt | 2 + docs/advanced-usage.md | 200 +++++++++++++++++++++++ docs/authentication.md | 316 ++++++++++++++++++++++++++++++++++++ docs/how-it-works.md | 164 +++++++++++++++++++ docs/index.md | 69 ++++++++ docs/philosophy.md | 90 ++++++++++ docs/quick-start.md | 145 +++++++++++++++++ docs/troubleshooting.md | 169 +++++++++++++++++++ mkdocs.yml | 38 +++++ 19 files changed, 1400 insertions(+), 191 deletions(-) create mode 100644 .github/workflows/pages.yml create mode 100644 docs-requirements.txt create mode 100644 docs/advanced-usage.md create mode 100644 docs/authentication.md create mode 100644 docs/how-it-works.md create mode 100644 docs/index.md create mode 100644 docs/philosophy.md create mode 100644 docs/quick-start.md create mode 100644 docs/troubleshooting.md create mode 100644 mkdocs.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a5519d0..ccf3022 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,3 +43,23 @@ jobs: run: | chmod +x scripts/version-check.sh ./scripts/version-check.sh + + docs: + name: Docs Build + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + + - name: Install MkDocs + run: | + python -m pip install --upgrade pip + python -m pip install -r docs-requirements.txt + + - name: Build docs + run: mkdocs build --strict diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..8f8e5ba --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,60 @@ +name: Pages + +on: + push: + branches: [ main ] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + build: + name: Build Docs Site + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Configure Pages + uses: actions/configure-pages@v5 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + + - name: Install MkDocs + run: | + python -m pip install --upgrade pip + python -m pip install -r docs-requirements.txt + + - name: Build site + run: mkdocs build --strict + + - name: Remove unpublished artifacts + run: rm -rf site/devlog + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: site + + deploy: + name: Deploy Docs Site + if: github.ref == 'refs/heads/main' + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 6a8fd4f..1bd415d 100644 --- a/.gitignore +++ b/.gitignore @@ -150,3 +150,6 @@ claude-yolo-pro/ # Claude YOLO Config Files - Local overrides .claude-yolo.local + +# MkDocs output +site/ diff --git a/CHANGELOG.md b/CHANGELOG.md index f3d9929..22c941b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -All notable changes to Claude Code YOLO will be documented in this file. +All notable changes to deva.sh will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). @@ -11,16 +11,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `LICENSE` with the standard MIT license text - `SECURITY.md` with private vulnerability reporting guidance - `CONTRIBUTING.md` with the repo workflow, local checks, and release rules +- `docs/` guide set for quick start, internals, philosophy, authentication, advanced usage, and troubleshooting +- `mkdocs.yml`, `docs/index.md`, and GitHub Pages workflow for publishing the docs site ### Fixed - Claude `--auth-with api-key` now forwards `ANTHROPIC_AUTH_TOKEN` and `ANTHROPIC_BASE_URL` - Non-default auth no longer moves live host credential files out of the way; it overlays the default credential path with a safe placeholder instead - `--dry-run` no longer mutates config homes through autolink or scaffold writes +- Copilot `--dry-run` no longer starts the local proxy as a side effect - Config-home fan-out skips loose credential files, backup files, VCS junk, and `.DS_Store` +- Auth-specific persistent containers now include the agent in the name suffix, avoiding cross-agent reuse with the wrong env or mounts - `install.sh` now installs the full current agent set, including Gemini and `shared_auth.sh` ### Changed -- Rewrote `README.md` into a cleaner OSS landing page with badges, quick start, auth matrix, and security warnings +- Rewrote `README.md` into a deva.sh front page with a real docs index and sharper OSS positioning +- CI now builds the MkDocs site so Pages breakage gets caught before merge - Updated `workflows/RELEASE.md` to use `deva.sh` as the source of truth for version bumps ## [0.9.1] - 2026-01-09 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 690bbce..5354235 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,7 +49,8 @@ If you change Docker image behavior, auth flows, or release logic, test those pa Update docs when behavior changes: -- `README.md` for user-facing workflow +- `README.md` for the front page and project positioning +- `docs/` for long-form user guides - `CHANGELOG.md` for release notes - `DEV-LOGS.md` for significant work - `SECURITY.md` when the reporting path or threat model changes diff --git a/DEV-LOGS.md b/DEV-LOGS.md index 70f1681..83fcfb1 100644 --- a/DEV-LOGS.md +++ b/DEV-LOGS.md @@ -13,6 +13,18 @@ - Minimal markdown markers, no unnecessary formatting, minimal emojis. - Reference issue numbers in the format `#` for easy linking. +# [2026-03-11] Dev Log: deva.sh docs spine for OSS release +- Why: the repo had a decent landing page but still dumped too much context into one README and did not read like an organized OSS project +- What: + - rewrote `README.md` as the deva.sh front page instead of a giant mixed-purpose document + - added `docs/index.md`, `docs/quick-start.md`, `docs/how-it-works.md`, `docs/philosophy.md`, `docs/authentication.md`, `docs/advanced-usage.md`, and `docs/troubleshooting.md` + - revalidated the docs against real `--dry-run` output instead of just `--help` + - corrected the docs and CLI help to describe persistent containers as project-scoped shapes, not a naive single-container story + - fixed auth-specific persistent naming to include the agent and fixed Copilot `--dry-run` so it no longer starts the proxy + - added MkDocs config, a GitHub Pages deploy workflow, a dedicated docs site home page, and CI docs-build validation + - aligned `CHANGELOG.md` and contribution guidance with the new docs split +- Result: the repo now has an actual docs spine for onboarding, internals, auth, and advanced workflows, the documented behavior matches the observed runtime shape, and the repo is ready to publish docs through GitHub Pages + # [2026-03-11] Dev Log: OSS repo polish and auth mount cleanup - Why: the repo still looked half-finished in public, the installer lagged behind the actual agent set, and recent auth switching work exposed ugly mount behavior - What: diff --git a/README.md b/README.md index a605a63..4dc2b31 100644 --- a/README.md +++ b/README.md @@ -1,48 +1,39 @@ -# deva +# deva.sh [![CI](https://img.shields.io/github/actions/workflow/status/thevibeworks/deva/ci.yml?branch=main&label=ci)](https://github.com/thevibeworks/deva/actions/workflows/ci.yml) [![Release](https://img.shields.io/github/v/release/thevibeworks/deva?sort=semver)](https://github.com/thevibeworks/deva/releases) [![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-claude%20%7C%20codex%20%7C%20gemini-222222)](#what-this-is) -Run Claude Code, Codex, and Gemini inside Docker. +Run Claude Code, Codex, and Gemini inside Docker without pretending the agent's own sandbox is the thing keeping you safe. -The container is the sandbox. That is the whole trick. +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. -## What This Is - -`deva.sh` launches coding agents inside a reusable Docker container with: - -- explicit volume mounts instead of blind filesystem access -- isolated auth homes with `--config-home` -- persistent per-project containers by default -- support for Claude, Codex, and Gemini in the same wrapper - -Legacy commands still exist: +This repo is the source of truth for `deva.sh`. -- `deva.sh` is the real entry point -- `claude.sh` and `claude-yolo` are compatibility wrappers - -## What This Is Not +## What This Is -- Not a security boundary if you mount `/var/run/docker.sock`. That is root-equivalent on the host. -- Not safe for random untrusted repos just because it says "Docker". -- Not a PaaS, orchestrator, or generic devcontainer manager. +- a Docker-based launcher for Claude, Codex, and Gemini +- 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/` +- a shell script, not framework cosplay -If you point this thing at your real home directory and hand it dangerous permissions, you did not discover a clever workflow. You discovered a foot-gun. +Primary entry point: -## Why This Exists +- `deva.sh` -Claude Code, Codex, and friends are useful. Their native local security prompts are also annoying in trusted workspaces. +Compatibility wrappers still exist: -`deva` moves the risk boundary: +- `claude.sh` +- `claude-yolo` -- agent gets broad power inside the container -- you decide exactly what is mounted from the host -- auth can be swapped per project or per org -- one warm container can serve multiple agents +## What This Is Not -That is a better trade when you actually know what you are doing. +- Not a real safety boundary if you mount `/var/run/docker.sock`. That is host-root with extra steps. +- Not a general-purpose devcontainer platform. +- Not magic. If you mount your whole home read-write and hand the agent dangerous permissions, the agent can touch your whole home. Amazing how that works. ## Quick Start @@ -53,201 +44,115 @@ cd ~/work/my-project deva.sh claude ``` -A few more sane first commands: +Then inspect the container if you want: ```bash -deva.sh codex -- --help -deva.sh gemini -- --help deva.sh shell -deva.sh --ps +deva.sh ps +deva.sh stop ``` -## Install - -Requirements: - -- Docker -- a supported agent auth method -- a trusted workspace - -The installer drops these into your PATH: - -- `deva.sh` -- `claude.sh` -- `claude-yolo` -- `agents/claude.sh` -- `agents/codex.sh` -- `agents/gemini.sh` -- `agents/shared_auth.sh` - -If Docker pull from GHCR fails, the installer falls back to Docker Hub. - -## Common Commands - -```bash -# Claude in the persistent project container -deva.sh claude - -# One-shot run, auto-remove container -deva.sh claude --rm - -# Add a read-only mount -deva.sh claude -v ~/.ssh:/home/deva/.ssh:ro - -# Use a separate auth home -deva.sh claude -c ~/auth-homes/work +If you already use Claude, Codex, 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. -# Claude with API or token-based auth -deva.sh claude --auth-with api-key -- -p "say hi" +## Docs -# Claude with Bedrock -deva.sh claude -c ~/auth-homes/aws --auth-with bedrock +Start here if you want the short path: -# Codex with explicit model -deva.sh codex -- -m gpt-5-codex +- [Quick Start](docs/quick-start.md) +- [Authentication Guide](docs/authentication.md) +- [Troubleshooting](docs/troubleshooting.md) -# Gemini in the same project container -deva.sh gemini -``` - -## Auth Modes - -| Agent | Default auth | Other auth modes | -| --- | --- | --- | -| Claude | `claude` | `api-key`, `oat`, `bedrock`, `vertex`, `copilot`, credentials file path | -| Codex | `chatgpt` | `api-key`, `copilot`, credentials file path | -| Gemini | `oauth` | `api-key`, `gemini-api-key`, `vertex`, `compute-adc`, credentials file path | +Read these if you want to understand the machinery instead of cargo-culting commands: -Examples: +- [How It Works](docs/how-it-works.md) +- [Philosophy](docs/philosophy.md) +- [Advanced Usage](docs/advanced-usage.md) +- [Docs Home](docs/index.md) -```bash -# Claude OAuth from config home -deva.sh claude -c ~/auth-homes/personal - -# Claude via custom HTTP endpoint + token -export ANTHROPIC_BASE_URL=https://example.net/api -export ANTHROPIC_AUTH_TOKEN=token -deva.sh claude --auth-with api-key +Project policy and OSS housekeeping: -# Codex via OpenAI API key -export OPENAI_API_KEY=sk-... -deva.sh codex --auth-with api-key +- [Contributing](CONTRIBUTING.md) +- [Security Policy](SECURITY.md) +- [MIT License](LICENSE) -# Gemini via service account file -deva.sh gemini --auth-with ~/keys/gcp-service-account.json -``` +Deep research note: -## Config Homes +- [UID/GID Handling Research](docs/UID-GID-HANDLING-RESEARCH.md) -By default, deva uses per-agent homes under `~/.config/deva/`. +## How It Feels ```text -~/.config/deva/ -├── claude/ -│ ├── .claude/ -│ ├── .claude.json -│ ├── .aws/ # optional -│ └── .config/gcloud/ # optional -├── codex/ -│ └── .codex/ -└── gemini/ - └── .gemini/ +host workspace + auth home + | + v + deva.sh + | + v + docker run / docker exec + | + v + persistent project container + /home/deva + chosen agent ``` -`--config-home DIR` supports two layouts: - -- leaf home: `DIR` contains `.claude`, `.claude.json`, `.aws`, `.config`, and so on -- deva root: `DIR` contains `claude/`, `codex/`, `gemini/` +Default mode reuses one persistent container shape per project. Different mounts, explicit config homes, or auth modes split into separate containers. That keeps your packages, build cache, and scratch state warm without pretending every run is identical. `--rm` gives you a throwaway run when you actually want that. -Default runs also auto-link legacy auth dirs into `~/.config/deva/` unless you disable that with `--no-autolink`, `AUTOLINK=false`, or `DEVA_NO_AUTOLINK=1`. - -## Container Model - -Persistent is the default. - -- one container per project -- reused across runs -- faster warm starts -- one workspace can host Claude, Codex, and Gemini together - -Ephemeral mode: - -- `--rm` creates a throwaway container -- `-Q` is the bare mode: no config loading, no autolink, no host config mounts - -Useful management commands: +## Common Commands ```bash -deva.sh --ps -deva.sh shell -deva.sh stop -deva.sh rm -deva.sh clean -``` +# Default agent is Claude +deva.sh -## Profiles +# Same container, different agents +deva.sh codex +deva.sh gemini -Profiles choose the image tag: +# Throwaway run +deva.sh claude --rm -- `base` -> `ghcr.io/thevibeworks/deva:latest` -- `rust` -> `ghcr.io/thevibeworks/deva:rust` +# Inspect what deva would run +deva.sh claude --debug --dry-run -Examples: +# Open a shell in the project container +deva.sh shell -```bash -deva.sh claude -p rust -make build -make build-rust +# Read resolved config state +deva.sh --show-config ``` -## Security Model - -Be honest about the sharp edges: +## Sharp Edges -- mounting `docker.sock` is host root by another name -- `--host-net` gives broad network visibility -- `--dangerously-skip-permissions` is still dangerous; Docker just changes where the blast radius lands -- `--config-home` should point to a dedicated auth home, not your real `$HOME` +- `--no-docker` exists for a reason. If you do not need Docker-in-Docker, do not mount the socket. +- `--host-net` gives the container broad network visibility. Use it when you mean it. +- `-Q` is the bare mode. It skips config loading, autolink, and host config mounts. Good for clean repros. +- `--config-home` is for isolated identities. Point it at a dedicated auth home, not your real `$HOME`. +- The debug `docker run` line is for inspection, not guaranteed copy-paste shell syntax. -If you need locked-down review mode, use explicit read-only mounts: +## Why This Exists -```bash -deva.sh claude \ - -v ~/src/project:/home/deva/project:ro \ - -v ~/.ssh:/home/deva/.ssh:ro \ - -v /tmp/deva-out:/home/deva/out -``` +Agent CLIs are useful. Their native permission theater is often not. -Security policy lives in [SECURITY.md](SECURITY.md). +deva moves the line: -## Repo Layout +- give the agent broad power inside a container +- decide exactly what crosses the host boundary +- swap auth methods per project or per run +- reuse the same default container shape across agents when mounts, config, and auth line up -```text -deva.sh main launcher -agents/ agent-specific auth and command wiring -Dockerfile* container images -workflows/ issue, commit, PR, release conventions -.github/workflows/ CI and release automation -scripts/ release and helper scripts -``` +That is a better trade if you are working in a trusted repo and you actually want to get work done. ## Development -Local sanity checks: +Basic checks: ```bash ./deva.sh --help ./deva.sh --version ./claude-yolo --help ./scripts/version-check.sh -shellcheck deva.sh agents/*.sh docker-entrypoint.sh install.sh scripts/*.sh ``` -Contributing guide: [CONTRIBUTING.md](CONTRIBUTING.md) - -Changelog: [CHANGELOG.md](CHANGELOG.md) - -Dev notes: [DEV-LOGS.md](DEV-LOGS.md) +If you changed auth, mounts, or container lifecycle, run the real path. Do not ship "should work". ## License diff --git a/agents/claude.sh b/agents/claude.sh index 81d2f3e..ccd07a8 100644 --- a/agents/claude.sh +++ b/agents/claude.sh @@ -102,13 +102,17 @@ setup_claude_auth() { copilot) validate_github_token || auth_error "No GitHub token found for copilot auth" \ "Run: copilot-api auth, or set GH_TOKEN=\$(gh auth token)" - start_copilot_proxy + if [ "${DRY_RUN:-false}" = true ]; then + echo "Skipping copilot proxy start during --dry-run" >&2 + else + start_copilot_proxy + fi AUTH_DETAILS="github-copilot (proxy port $COPILOT_PROXY_PORT)" DOCKER_ARGS+=("-e" "ANTHROPIC_BASE_URL=http://$COPILOT_HOST_MAPPING:$COPILOT_PROXY_PORT") DOCKER_ARGS+=("-e" "ANTHROPIC_API_KEY=dummy") - if [ -z "${ANTHROPIC_MODEL:-}" ] || [ -z "${ANTHROPIC_SMALL_FAST_MODEL:-}" ]; then + if [ "${DRY_RUN:-false}" != true ] && { [ -z "${ANTHROPIC_MODEL:-}" ] || [ -z "${ANTHROPIC_SMALL_FAST_MODEL:-}" ]; }; then local models models=$(pick_copilot_models "http://$COPILOT_LOCALHOST_MAPPING:$COPILOT_PROXY_PORT") local main_model="${models%% *}" diff --git a/agents/codex.sh b/agents/codex.sh index fce137d..13041cb 100644 --- a/agents/codex.sh +++ b/agents/codex.sh @@ -69,13 +69,17 @@ setup_codex_auth() { copilot) validate_github_token || auth_error "No GitHub token found for copilot auth" \ "Run: copilot-api auth, or set GH_TOKEN=\$(gh auth token)" - start_copilot_proxy + if [ "${DRY_RUN:-false}" = true ]; then + echo "Skipping copilot proxy start during --dry-run" >&2 + else + start_copilot_proxy + fi AUTH_DETAILS="github-copilot (proxy port $COPILOT_PROXY_PORT)" DOCKER_ARGS+=("-e" "OPENAI_BASE_URL=http://$COPILOT_HOST_MAPPING:$COPILOT_PROXY_PORT") DOCKER_ARGS+=("-e" "OPENAI_API_KEY=dummy") - if [ -z "${OPENAI_MODEL:-}" ]; then + if [ "${DRY_RUN:-false}" != true ] && [ -z "${OPENAI_MODEL:-}" ]; then local models models=$(pick_copilot_models "http://$COPILOT_LOCALHOST_MAPPING:$COPILOT_PROXY_PORT") local main_model="${models%% *}" diff --git a/deva.sh b/deva.sh index 5fb9367..1398193 100755 --- a/deva.sh +++ b/deva.sh @@ -77,20 +77,21 @@ Deva flags: implies --rm. Like emacs -Q. Mutually exclusive with -c. --host-net Use host networking for the agent container --no-docker Disable auto-mount of Docker socket (default: auto-mount if present) - --dry-run Show docker command without executing (implies --debug) + --dry-run Show docker command without executing the container (implies --debug) --verbose, --debug Print full docker command before execution -- Everything after this sentinel is passed to the agent unchanged Container Behavior (NEW in v0.8.0): - Default (persistent): One container per project, reused across runs. + Default (persistent): Shared per project by default, but split when container shape changes + (extra volumes, explicit config-home, auth mode). Preserves state (npm packages, builds, etc). - Faster startup, run any agent (claude/codex/gemini). + Faster startup, and default-auth runs can share one warm container. With --rm (ephemeral): Create new container, auto-remove after exit. Agent-specific naming for parallel runs. Container Naming (NEW): - Persistent: deva-- # One per project + Persistent: deva--[..shape] # shape may encode volumes/config/auth Ephemeral: deva---- # Agent-specific Example: @@ -101,8 +102,8 @@ Examples: # Launch agents (persistent by default) deva.sh # Launch claude in persistent container deva.sh claude # Same - deva.sh codex # Launch codex in same container - deva.sh gemini # Launch gemini in same container + deva.sh codex # Launch codex in the same default container shape + deva.sh gemini # Launch gemini in the same default container shape deva.sh claude --rm # Ephemeral: deva-work-myapp-claude-12345 # Container management (current project) @@ -2253,6 +2254,7 @@ if [ -n "${AUTH_METHOD:-}" ]; then auth_suffix="${AUTH_METHOD}" fi [ -n "$creds_hash" ] && auth_suffix="${AUTH_METHOD}-${creds_hash}" + auth_suffix="${auth_suffix}-${ACTIVE_AGENT}" # Build suffix chain: volume + config + auth name_suffix="" diff --git a/docs-requirements.txt b/docs-requirements.txt new file mode 100644 index 0000000..455b696 --- /dev/null +++ b/docs-requirements.txt @@ -0,0 +1,2 @@ +mkdocs>=1.6,<2 +mkdocs-material>=9.6,<10 diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md new file mode 100644 index 0000000..8029edb --- /dev/null +++ b/docs/advanced-usage.md @@ -0,0 +1,200 @@ +# Advanced Usage + +This is where the sharp tools live. + +## Use `.deva` Instead Of Giant Commands + +If you keep typing the same mounts and env vars, stop doing that. + +Put them in `.deva`: + +```text +VOLUME=$HOME/.ssh:/home/deva/.ssh:ro +VOLUME=$HOME/.config/git:/home/deva/.config/git:ro +ENV=EDITOR=nvim +PROFILE=rust +``` + +Local override that should not be committed: + +```text +# .deva.local +ENV=GH_TOKEN=${GH_TOKEN} +``` + +Load order is: + +1. `$XDG_CONFIG_HOME/deva/.deva` +2. `$HOME/.deva` +3. `./.deva` +4. `./.deva.local` + +See [`.deva.example`](https://github.com/thevibeworks/deva/blob/main/.deva.example). + +## Separate Identities With `--config-home` + +This is the clean way to keep work and personal auth apart. + +Leaf layout: + +```bash +deva.sh claude -c ~/auth-homes/work +``` + +Deva-root layout: + +```text +~/auth-roots/team-a/ +├── claude/ +├── codex/ +└── gemini/ +``` + +```bash +deva.sh claude -c ~/auth-roots/team-a +deva.sh codex -c ~/auth-roots/team-a +``` + +If you pass an explicit config home, deva does not also mount your default `~/.config/deva`. That is deliberate isolation. + +## Bare Mode With `-Q` + +`-Q` is the clean-room mode: + +- implies `--rm` +- no `.deva` loading +- no autolink +- no config-home mounts + +Use it when you need a repro that is not contaminated by your local habits. + +```bash +deva.sh claude -Q +deva.sh claude -Q -v "$PWD:/workspace" -- -p "summarize this repo" +``` + +`-Q` and `--config-home` are mutually exclusive. They solve opposite problems. + +## Read-Only Review Mode + +If you want the agent to inspect more than it edits, mount most of the world read-only and give it one scratch path. + +```bash +deva.sh claude \ + -v "$PWD:/workspace:ro" \ + -v "$HOME/.ssh:/home/deva/.ssh:ro" \ + -v /tmp/deva-out:/home/deva/out +``` + +That is still not "safe" in some absolute sense. It is just a saner blast radius than handing over your laptop. + +## Profiles + +Supported profiles: + +- `base` -> `ghcr.io/thevibeworks/deva:latest` +- `rust` -> `ghcr.io/thevibeworks/deva:rust` + +Use them like this: + +```bash +deva.sh claude -p rust +deva.sh codex -p rust +``` + +If the image tag is missing locally, deva pulls it. If that fails and a matching Dockerfile exists, it points you at the build command. + +## Multi-Agent Workflow + +One default container shape can serve all supported agents in the same project: + +```bash +deva.sh claude +deva.sh codex +deva.sh gemini +``` + +That keeps package installs, build output, and scratch files hot between agents. + +If you change volumes, config-home, or auth mode, deva intentionally uses a different persistent container instead of reusing one with the wrong mounts or env. + +## Container Management + +Current project: + +```bash +deva.sh ps +deva.sh status +deva.sh shell +deva.sh stop +deva.sh rm +deva.sh clean +``` + +All projects: + +```bash +deva.sh ps -g +deva.sh shell -g +deva.sh stop -g +``` + +## Debugging + +These are the three commands that matter: + +```bash +deva.sh --show-config +deva.sh claude --debug --dry-run +deva.sh shell +``` + +Use them in that order: + +1. inspect config resolution +2. inspect Docker shape +3. inspect the live container + +The printed `docker run` line is diagnostic output. It masks secrets and may contain unquoted values. Read it. Do not blindly paste it back into a shell and then complain when your shell parses spaces like spaces. + +## Risk Knobs + +### Docker Socket + +Default behavior auto-mounts `/var/run/docker.sock` when it exists. + +That means the container can control Docker on the host. Translation: host-root in practice. + +Disable it: + +```bash +deva.sh claude --no-docker +``` + +Or: + +```bash +export DEVA_NO_DOCKER=1 +``` + +### Host Networking + +Use only when you need direct host networking behavior: + +```bash +deva.sh claude --host-net +``` + +Again, this is not a subtle switch. It broadens what the container can see. + +## Custom Auth Files + +If you have a separate JSON credential file, pass the file itself: + +```bash +deva.sh claude --auth-with ~/work/claude-prod.credentials.json +deva.sh codex --auth-with ~/work/codex-auth.json +deva.sh gemini --auth-with ~/keys/gcp-service-account.json +``` + +Deva mounts the file onto the agent's expected credential path. It does not need to dump a directory full of backup junk into the container to make that work. diff --git a/docs/authentication.md b/docs/authentication.md new file mode 100644 index 0000000..0f0209c --- /dev/null +++ b/docs/authentication.md @@ -0,0 +1,316 @@ +# Authentication Guide + +Auth is where wrappers usually become untrustworthy. + +This guide documents what `deva.sh` actually supports, what env vars it reads, and how credential files are mounted. + +## Rules First + +- Every agent has its own default auth home. +- `--auth-with ` selects a non-default auth path. +- `--auth-with ` is treated as an explicit credential file mount. +- Non-default auth masks the agent's default credential file with a blank overlay unless the explicit credential file already occupies that path. +- `--dry-run` is useful for mount and env inspection. It does not prove the credentials work. +- Copilot `--dry-run` no longer starts the local proxy; it only shows the planned wiring. + +## Auth Matrix + +| Agent | Default auth | Other methods | Main inputs | +| --- | --- | --- | --- | +| 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 | + +## Claude + +### Default: `--auth-with claude` + +Default Claude auth uses: + +- `/home/deva/.claude` +- `/home/deva/.claude.json` + +By default those come from the selected config home. + +Example: + +```bash +deva.sh claude +deva.sh claude -c ~/auth-homes/work +``` + +### `--auth-with api-key` + +This name is a little muddy because Claude supports more than one token shape here. + +Accepted host inputs: + +- `ANTHROPIC_API_KEY` +- `ANTHROPIC_AUTH_TOKEN` +- `CLAUDE_CODE_OAUTH_TOKEN` + +Optional endpoint override: + +- `ANTHROPIC_BASE_URL` + +Examples: + +```bash +export ANTHROPIC_API_KEY=sk-ant-... +deva.sh claude --auth-with api-key +``` + +```bash +export ANTHROPIC_BASE_URL=https://example.net/api +export ANTHROPIC_AUTH_TOKEN=token +deva.sh claude --auth-with api-key +``` + +If `ANTHROPIC_API_KEY` looks like a Claude OAuth token (`sk-ant-oat01-...`), deva auto-routes it as `CLAUDE_CODE_OAUTH_TOKEN`. + +### `--auth-with oat` + +Requires: + +- `CLAUDE_CODE_OAUTH_TOKEN` + +Optional: + +- `ANTHROPIC_BASE_URL` + +Example: + +```bash +export CLAUDE_CODE_OAUTH_TOKEN=sk-ant-oat01-... +deva.sh claude --auth-with oat +``` + +### `--auth-with bedrock` + +Uses AWS credentials from: + +- `~/.aws` +- `AWS_ACCESS_KEY_ID` +- `AWS_SECRET_ACCESS_KEY` +- `AWS_SESSION_TOKEN` +- `AWS_REGION` + +It also sets `CLAUDE_CODE_USE_BEDROCK=1`. + +Example: + +```bash +export AWS_REGION=us-west-2 +deva.sh claude --auth-with bedrock +``` + +### `--auth-with vertex` + +Uses Google credentials from: + +- `~/.config/gcloud` +- `GOOGLE_APPLICATION_CREDENTIALS` when set to a host file path + +It also sets `CLAUDE_CODE_USE_VERTEX=1`. + +Example: + +```bash +export GOOGLE_APPLICATION_CREDENTIALS=$HOME/keys/work-sa.json +deva.sh claude --auth-with vertex +``` + +### `--auth-with copilot` + +Requires either: + +- saved `copilot-api` token +- `GH_TOKEN` +- `GITHUB_TOKEN` + +Deva starts the local `copilot-api` proxy, points Claude at the Anthropic-compatible endpoint, and injects dummy API key values where the CLI expects them. + +Example: + +```bash +export GH_TOKEN="$(gh auth token)" +deva.sh claude --auth-with copilot +``` + +### `--auth-with /path/to/file.json` + +Custom credential files are mounted directly to: + +```text +/home/deva/.claude/.credentials.json +``` + +Example: + +```bash +deva.sh claude --auth-with ~/work/claude-prod.credentials.json +``` + +## Codex + +### Default: `--auth-with chatgpt` + +Uses: + +- `/home/deva/.codex/auth.json` + +Usually from the selected config home. + +### `--auth-with api-key` + +Requires: + +- `OPENAI_API_KEY` + +Example: + +```bash +export OPENAI_API_KEY=sk-... +deva.sh codex --auth-with api-key +``` + +### `--auth-with copilot` + +Requires either: + +- saved `copilot-api` token +- `GH_TOKEN` +- `GITHUB_TOKEN` + +Deva points Codex at the OpenAI-compatible side of the proxy and defaults the model to `gpt-5-codex` unless you supplied one. + +Example: + +```bash +export GH_TOKEN="$(gh auth token)" +deva.sh codex --auth-with copilot +``` + +### `--auth-with /path/to/file.json` + +Custom credential files are mounted to: + +```text +/home/deva/.codex/auth.json +``` + +Example: + +```bash +deva.sh codex --auth-with ~/work/codex-auth.json +``` + +## Gemini + +### Default: `--auth-with oauth` + +Uses: + +- `/home/deva/.gemini` + +`gemini-app-oauth` is treated as the same app-style OAuth family. + +### `--auth-with api-key` or `gemini-api-key` + +Requires: + +- `GEMINI_API_KEY` + +When this mode is active and not running under `--dry-run`, deva makes sure the Gemini settings file in the chosen config home selects API-key auth. Gemini state can include both `.gemini/` content and a top-level `settings.json`, depending on what the CLI has already written there. + +Example: + +```bash +export GEMINI_API_KEY=... +deva.sh gemini --auth-with api-key +``` + +### `--auth-with vertex` + +Uses: + +- `~/.config/gcloud` +- `GOOGLE_APPLICATION_CREDENTIALS` +- `GOOGLE_CLOUD_PROJECT` +- `GOOGLE_CLOUD_LOCATION` + +Example: + +```bash +export GOOGLE_CLOUD_PROJECT=my-project +export GOOGLE_CLOUD_LOCATION=us-central1 +deva.sh gemini --auth-with vertex +``` + +### `--auth-with compute-adc` + +Uses Google Compute Engine application default credentials from the metadata server. That is mostly for workloads already running on GCP. + +### `--auth-with /path/to/file.json` + +Custom service-account files are mounted to: + +```text +/home/deva/.config/gcloud/service-account-key.json +``` + +And `GOOGLE_APPLICATION_CREDENTIALS` is set to that container path. + +Example: + +```bash +deva.sh gemini --auth-with ~/keys/gcp-service-account.json +``` + +## Config Homes And Auth Isolation + +Default homes live under: + +```text +~/.config/deva/claude +~/.config/deva/codex +~/.config/deva/gemini +``` + +Use `--config-home` when you want a separate identity: + +```bash +deva.sh claude -c ~/auth-homes/work +deva.sh codex -c ~/auth-homes/personal +``` + +Good reasons to split auth homes: + +- work vs personal accounts +- OAuth vs API-key experiments +- different org endpoints +- reproducing auth bugs without contaminating your default state + +## Debugging Auth + +Useful commands: + +```bash +deva.sh --show-config +deva.sh claude --auth-with api-key --debug --dry-run +deva.sh shell +``` + +What to check in `--dry-run`: + +- the chosen auth label +- expected env vars are present +- unexpected auth env vars are absent +- the explicit credential file mount points at the right container path +- the blank overlay exists when non-default auth is active + +What `--dry-run` cannot tell you: + +- whether the remote endpoint accepts the token +- whether the agent CLI likes that token shape +- whether your cloud credentials are actually authorized diff --git a/docs/how-it-works.md b/docs/how-it-works.md new file mode 100644 index 0000000..f4222d6 --- /dev/null +++ b/docs/how-it-works.md @@ -0,0 +1,164 @@ +# How It Works + +This is the real startup model. No mythology. + +## Short Version + +`deva.sh` does five things: + +1. resolves wrapper config and agent choice +2. resolves the config home and auth mode +3. builds the Docker mount and env list +4. creates or reuses a project-scoped container +5. `docker exec`s the selected agent inside that container + +That is it. + +## Startup Flow + +### 1. Deva parses wrapper args and agent args separately + +Wrapper flags include things like: + +- `--rm` +- `-v` +- `-e` +- `-c`, `--config-home` +- `-p`, `--profile` +- `-Q`, `--quick` +- `--host-net` +- `--no-docker` +- `--debug`, `--dry-run` + +Everything after `--` goes to the agent unchanged. + +### 2. Deva loads config files + +Config files load in this order: + +1. `$XDG_CONFIG_HOME/deva/.deva` +2. `$HOME/.deva` +3. `./.deva` +4. `./.deva.local` + +Supported directives are simple: + +- `VOLUME=host:container[:mode]` +- `ENV=NAME=value` +- `AUTH_METHOD=...` +- `PROFILE=...` +- `EPHEMERAL=...` + +See [`.deva.example`](https://github.com/thevibeworks/deva/blob/main/.deva.example). + +### 3. Deva resolves the config home + +Default per-agent homes live under: + +```text +~/.config/deva/ +├── claude/ +├── codex/ +└── gemini/ +``` + +`--config-home` supports two layouts: + +- leaf home: `DIR/.claude`, `DIR/.claude.json`, `DIR/.codex`, `DIR/.gemini` +- deva root: `DIR/claude`, `DIR/codex`, `DIR/gemini` + +`-Q` disables config-home resolution, autolink, and host config mounts entirely. + +### 4. Deva resolves auth + +Each agent owns its auth modes. The wrapper does not fake a universal auth abstraction because those usually turn into garbage. + +Examples: + +- Claude default: `.claude` and `.claude.json` +- 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` + +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. + +If `--auth-with /path/to/file.json` is used, that explicit file is mounted directly over the agent's default credential path. + +### 5. Deva builds Docker args + +The wrapper always mounts: + +- the current workspace at the same absolute path +- the current workspace as container working directory +- UID/GID, timezone, locale, and a few useful host envs + +It may also mount: + +- additional user volumes from `-v` or `.deva` +- config-home contents into `/home/deva` +- `~/.config/deva` and `~/.cache/deva` when using the default config root +- project-local `.claude` if present +- `/var/run/docker.sock` if present and not disabled + +Loose credential files, backup files, `.DS_Store`, and VCS junk are intentionally skipped during config-home fan-out. + +### 6. Deva creates or reuses a container + +Persistent is default: + +- one default container shape per project +- reused across runs +- same workspace can run Claude, Codex, and Gemini 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`: + +- new container every time +- removed after exit +- useful for clean repros or one-shot tasks + +Container names include the workspace slug and may also include hashes for: + +- extra volumes +- explicit config-home +- auth mode + +That prevents collisions between materially different container shapes. + +### 7. Deva execs the agent + +For persistent mode, the runtime shape is: + +1. `docker run -d ... tail -f /dev/null` +2. `docker exec -it ... /usr/local/bin/docker-entrypoint.sh ` + +For ephemeral mode, it runs the agent directly in `docker run`. + +## Agent Defaults + +- Claude: injects `--dangerously-skip-permissions` unless you already supplied it +- Codex: injects `--dangerously-bypass-approvals-and-sandbox` and defaults model to `gpt-5-codex` +- Gemini: injects `--yolo` + +This is not subtle. The container is the trust boundary, so the agent's internal approval system is intentionally bypassed. + +## Proxy and Network Behavior + +- `HTTP_PROXY` and `HTTPS_PROXY` are passed through +- `localhost` in those proxy URLs is translated to `host.docker.internal` +- `--host-net` opts into host networking +- `--no-docker` disables Docker socket auto-mount + +If you mount the Docker socket, stop pretending the container is isolated from the host. + +## Debugging the Runtime Shape + +Use: + +```bash +deva.sh --show-config +deva.sh claude --debug --dry-run +deva.sh shell +``` + +`--dry-run` shows the container shape without starting the container. That is good for checking env and mount wiring. It is not a proof that the agent can actually authenticate or complete a request. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..97f7b45 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,69 @@ +# deva.sh + +Run Claude Code, Codex, and Gemini 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. + +## Start Here + +- [Quick Start](quick-start.md) +- [Authentication Guide](authentication.md) +- [Troubleshooting](troubleshooting.md) + +If you want the internals instead of vague hand-waving: + +- [How It Works](how-it-works.md) +- [Philosophy](philosophy.md) +- [Advanced Usage](advanced-usage.md) + +## What This Is + +- a Docker-based launcher for Claude, Codex, and Gemini +- 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/` +- a shell script, not framework cosplay + +## What This Is Not + +- Not a real safety boundary if you mount `/var/run/docker.sock`. +- Not a general-purpose devcontainer platform. +- Not magic. If you mount your whole home read-write and hand the agent + dangerous permissions, the agent can touch your whole home. + +## Quick Start + +```bash +curl -fsSL https://raw.githubusercontent.com/thevibeworks/deva/main/install.sh | bash + +cd ~/work/my-project +deva.sh claude +``` + +Then inspect the container if you want: + +```bash +deva.sh shell +deva.sh ps +deva.sh stop +``` + +## Sharp Edges + +- `--no-docker` exists for a reason. If you do not need Docker-in-Docker, + do not mount the socket. +- `--host-net` gives the container broad network visibility. +- `-Q` skips config loading, autolink, and host config mounts. +- `--config-home` is for isolated identities, not your real home. +- The debug `docker run` line is diagnostic output, not guaranteed + copy-paste shell syntax. + +## Repo And Policy + +- [Repository](https://github.com/thevibeworks/deva) +- [Contributing](https://github.com/thevibeworks/deva/blob/main/CONTRIBUTING.md) +- [Security Policy](https://github.com/thevibeworks/deva/blob/main/SECURITY.md) +- [MIT License](https://github.com/thevibeworks/deva/blob/main/LICENSE) diff --git a/docs/philosophy.md b/docs/philosophy.md new file mode 100644 index 0000000..515d0ec --- /dev/null +++ b/docs/philosophy.md @@ -0,0 +1,90 @@ +# Philosophy + +Tools like this go bad when they start lying about what they are. + +`deva.sh` tries not to do that. + +## The Container Is The Sandbox + +The whole design starts here. + +We do not rely on the agent's interactive approval prompts as the main safety story. We run the agent inside Docker and make the host boundary explicit with mounts and env vars. + +That has consequences: + +- inside the container, the agent gets broad power +- outside the container, it only sees what we mounted or forwarded +- the quality of the boundary depends on the mounts you chose, not on wishful thinking + +If you punch holes through that boundary with `docker.sock`, host networking, or your entire home directory, that is your decision. The docs should say that plainly. + +## Explicit Beats Magical + +Hidden config and silent fallback behavior are where auth bugs and secret leaks come from. + +So deva prefers: + +- explicit mount lists +- explicit auth method switches +- explicit config homes +- explicit debug output + +When auth changes, the container identity changes too. When non-default auth is active, the default credential file gets masked. That is boring, and boring is good. + +## Persistent Beats Disposable + +One-shot containers look neat in demos and get old fast in real work. + +Persistent per-project containers mean: + +- warm package caches +- stateful shell history and scratch space +- fast switching between Claude, Codex, and Gemini + +`--rm` still exists. It just is not the default because the default should serve real work instead of screenshots. + +## Separate Auth Homes Beat Shared Mess + +Most auth trouble comes from mixing identities: + +- personal and work credentials +- OAuth state and API keys +- one agent's config assumptions with another's + +`~/.config/deva/` exists to stop that drift. `--config-home` exists when you need even harder separation. + +## Shell Script Over Platform Theater + +This repo is mostly shell because the job is orchestration, not empire-building. + +That means: + +- easy to inspect +- easy to patch +- easy to debug with `--dry-run` +- hard to hide nonsense in ten abstraction layers + +You can absolutely write bad shell. Plenty of people do. But for this job, a readable shell script is still better than building a fake platform because someone got bored. + +## Multi-Agent, Not Single-Vendor + +This started in the Claude world. It would have been stupid to stay trapped there. + +The useful abstraction is not "Claude but renamed." The useful abstraction is: + +- one container workflow +- several agents +- explicit auth and model wiring per agent + +That is why `deva.sh` is the entry point and the old wrappers are just compatibility shims. + +## Honest Docs Or Nothing + +The docs should tell you: + +- what works +- what is sharp +- what is slower than you expect +- what the wrapper does on your behalf + +If the docs skip the ugly parts, then they are marketing. We do not need more marketing. diff --git a/docs/quick-start.md b/docs/quick-start.md new file mode 100644 index 0000000..b0c834d --- /dev/null +++ b/docs/quick-start.md @@ -0,0 +1,145 @@ +# Quick Start + +This is the shortest path from zero to a working `deva.sh` container. + +## Prerequisites + +You need: + +- Docker +- a project directory you trust +- one agent auth path that actually works + +If your plan is "mount my whole laptop and see what happens", that is not a prerequisite. That is a mistake. + +## Install + +```bash +curl -fsSL https://raw.githubusercontent.com/thevibeworks/deva/main/install.sh | bash +``` + +That installs: + +- `deva.sh` +- `claude.sh` +- `claude-yolo` +- `agents/claude.sh` +- `agents/codex.sh` +- `agents/gemini.sh` +- `agents/shared_auth.sh` + +It also pulls `ghcr.io/thevibeworks/deva:latest`, with Docker Hub as fallback. + +## First Run + +```bash +cd ~/work/my-project +deva.sh claude +``` + +By default, deva: + +- mounts the current project at the same absolute path inside the container +- creates or reuses one persistent container for that project +- uses the per-agent config home under `~/.config/deva/` +- auto-links legacy local auth homes into that config root unless you disable autolink + +If you already have local agent auth, first run is usually boring. Good. Boring is the point. + +## First Useful Commands + +```bash +# See the container for this project +deva.sh ps + +# Open a shell inside it +deva.sh shell + +# Show the resolved wrapper config +deva.sh --show-config + +# Show the docker command without running it +deva.sh claude --debug --dry-run + +# Stop or remove the project container +deva.sh stop +deva.sh rm +``` + +## Use Another Agent + +Same project, same default container shape: + +```bash +deva.sh codex +deva.sh gemini +``` + +That is one of the main reasons this wrapper exists. You do not need a separate pet workflow for every vendor. + +If you change mounts, explicit config-home, or auth mode, deva will split into a different persistent container shape instead of pretending those runs are equivalent. + +## Quick Auth Examples + +Claude with a direct Anthropic-style key or token: + +```bash +export ANTHROPIC_API_KEY=sk-ant-... +deva.sh claude --auth-with api-key +``` + +Claude with a custom endpoint: + +```bash +export ANTHROPIC_BASE_URL=https://example.net/api +export ANTHROPIC_AUTH_TOKEN=token +deva.sh claude --auth-with api-key +``` + +Codex with OpenAI API key: + +```bash +export OPENAI_API_KEY=sk-... +deva.sh codex --auth-with api-key +``` + +Gemini with API key: + +```bash +export GEMINI_API_KEY=... +deva.sh gemini --auth-with api-key +``` + +More auth details live in [Authentication Guide](authentication.md). + +## Useful Modes + +Throwaway container: + +```bash +deva.sh claude --rm +``` + +Bare mode with no config loading or host auth mounts: + +```bash +deva.sh claude -Q +``` + +Isolated auth home: + +```bash +deva.sh claude -c ~/auth-homes/work +``` + +## If Something Looks Wrong + +Use these before you start editing code out of frustration: + +```bash +deva.sh --show-config +deva.sh claude --debug --dry-run +deva.sh shell +``` + +Then read [Troubleshooting](troubleshooting.md). diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 0000000..2fb067a --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,169 @@ +# Troubleshooting + +The goal here is to stop guessing. + +## First Three Commands + +Run these before you change code, files, or your worldview: + +```bash +deva.sh --show-config +deva.sh claude --debug --dry-run +deva.sh shell +``` + +Those tell you: + +- what config was loaded +- what Docker shape deva is building +- what the live container actually sees + +## Docker Is Not Running + +Symptom: + +- container creation fails immediately + +Check: + +```bash +docker ps +``` + +Fix: + +- start Docker +- confirm your user can talk to the Docker daemon + +This one is not mysterious. + +## Wrong Mount Paths After The `/root` -> `/home/deva` Move + +Symptom: + +- files you expected are missing in the container + +Bad: + +```bash +-v ~/.ssh:/root/.ssh:ro +``` + +Good: + +```bash +-v ~/.ssh:/home/deva/.ssh:ro +``` + +Deva warns about `/root/*` mounts, but warnings are easy to ignore when people get overconfident. + +## Auth Looks Wrong + +Symptom: + +- wrong account +- wrong endpoint +- auth falls back to some old session + +Check: + +```bash +deva.sh claude --auth-with api-key --debug --dry-run +``` + +Look for: + +- expected auth env vars present +- unexpected auth env vars absent +- correct credential file mount +- blank overlay on the default credential file when using non-default auth + +If the dry-run shape is correct but the agent still cannot authenticate, the wrapper may be fine and the real problem is the token, endpoint, or upstream CLI behavior. + +## Config Home Is Empty + +Symptom: + +- first run warns that `.claude`, `.codex`, or `.gemini` is empty + +Meaning: + +- you pointed `--config-home` at a new directory, which is fine +- you now need to authenticate into that isolated home + +That is not an error. That is exactly what isolated config homes are for. + +## Proxy Weirdness + +Deva rewrites `localhost` in `HTTP_PROXY` and `HTTPS_PROXY` to `host.docker.internal` for the container path. + +If the agent cannot reach a local proxy: + +- check the proxy actually listens on the host +- inspect the translated value in `--dry-run` +- check `NO_PROXY` + +For Copilot proxy mode, deva also adds `NO_PROXY` and `no_grpc_proxy` entries for the local proxy hostnames. + +## Dry-Run Looks Fine But Runtime Fails + +That is normal in at least three cases: + +- bad token +- wrong remote permissions +- agent CLI rejects the auth type at runtime + +`--dry-run` validates assembly, not end-to-end auth success. + +It also does not start the container. For Copilot mode it now skips starting the local proxy as well, so the output stays a planning tool instead of mutating local state. + +Use a real smoke test: + +```bash +deva.sh claude --auth-with api-key -- -p "reply with ok" +``` + +## Too Much State, Need A Clean Repro + +Use bare mode: + +```bash +deva.sh claude -Q +``` + +Or remove the project container: + +```bash +deva.sh rm +``` + +If the problem disappears in `-Q`, your usual config or mounts are part of the issue. + +## Container Reuse Confuses You + +Persistent is the default. That means the next run may attach to an existing container. + +Check: + +```bash +deva.sh ps +deva.sh status +``` + +If you want a throwaway run: + +```bash +deva.sh claude --rm +``` + +## Still Stuck + +Collect something useful before filing an issue: + +- `deva.sh --show-config` +- `deva.sh --debug --dry-run` +- exact auth mode +- exact config-home path +- whether `docker.sock` or `--host-net` was enabled + +Then open the issue without hand-wavy descriptions like "auth broken somehow". That phrase helps nobody. diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..910f26a --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,38 @@ +site_name: deva.sh +site_description: Docker-based multi-agent launcher for Claude, Codex, and Gemini +site_url: https://deva.sh +repo_url: https://github.com/thevibeworks/deva +repo_name: thevibeworks/deva +edit_uri: "" + +theme: + name: material + features: + - navigation.instant + - navigation.sections + - navigation.top + - search.highlight + - search.suggest + - content.code.copy + palette: + - scheme: default + primary: black + accent: blue grey + +markdown_extensions: + - admonition + - attr_list + - tables + - toc: + permalink: true + +nav: + - Home: index.md + - Quick Start: quick-start.md + - How It Works: how-it-works.md + - Authentication: authentication.md + - Philosophy: philosophy.md + - Advanced Usage: advanced-usage.md + - Troubleshooting: troubleshooting.md + - Research: + - UID/GID Handling: UID-GID-HANDLING-RESEARCH.md From ad546eab685b2607fba3f489e538d9d41b545630 Mon Sep 17 00:00:00 2001 From: Eric Wang Date: Wed, 11 Mar 2026 21:35:29 -0700 Subject: [PATCH 6/6] ci: wire GitHub Pages and nightly image builds - publish docs with docs.deva.sh domain metadata and site links - share tool version resolution across nightly and tagged releases --- .github/workflows/nightly-images.yml | 175 +++++++++++++++++++++++++++ .github/workflows/pages.yml | 3 + .github/workflows/release.yml | 118 ++++++++++++------ CHANGELOG.md | 4 +- DEV-LOGS.md | 6 +- README.md | 14 +++ docs/index.md | 6 + mkdocs.yml | 4 +- scripts/resolve-tool-versions.sh | 36 ++++++ 9 files changed, 326 insertions(+), 40 deletions(-) create mode 100644 .github/workflows/nightly-images.yml create mode 100644 scripts/resolve-tool-versions.sh diff --git a/.github/workflows/nightly-images.yml b/.github/workflows/nightly-images.yml new file mode 100644 index 0000000..2222215 --- /dev/null +++ b/.github/workflows/nightly-images.yml @@ -0,0 +1,175 @@ +name: Nightly Images + +on: + schedule: + - cron: "17 4 * * *" + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: thevibeworks/deva + +permissions: + contents: read + packages: write + +concurrency: + group: nightly-images + cancel-in-progress: true + +jobs: + resolve-versions: + name: Resolve Latest Tool Versions + runs-on: ubuntu-latest + outputs: + stamp: ${{ steps.versions.outputs.stamp }} + 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 }} + atlas_cli_version: ${{ steps.versions.outputs.atlas_cli_version }} + copilot_api_version: ${{ steps.versions.outputs.copilot_api_version }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: "22" + + - name: Resolve versions + id: versions + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: bash ./scripts/resolve-tool-versions.sh + + - name: Summary + run: | + cat <> "$GITHUB_STEP_SUMMARY" + ## Nightly Tool Versions + + - Claude Code: \`${{ steps.versions.outputs.claude_code_version }}\` + - Codex: \`${{ steps.versions.outputs.codex_version }}\` + - Gemini CLI: \`${{ steps.versions.outputs.gemini_cli_version }}\` + - Atlas CLI: \`${{ steps.versions.outputs.atlas_cli_version }}\` + - Copilot API: \`${{ steps.versions.outputs.copilot_api_version }}\` + - Stamp: \`${{ steps.versions.outputs.stamp }}\` + EOF + + build-base: + name: Build Nightly Base Image + needs: resolve-versions + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=nightly + type=raw,value=nightly-${{ needs.resolve-versions.outputs.stamp }} + labels: | + org.opencontainers.image.title=deva-nightly + org.opencontainers.image.description=Nightly deva image with latest upstream CLI versions + + - name: Build and push base image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha,scope=nightly-base + cache-to: type=gha,mode=max,scope=nightly-base + build-args: | + CLAUDE_CODE_VERSION=${{ needs.resolve-versions.outputs.claude_code_version }} + CODEX_VERSION=${{ needs.resolve-versions.outputs.codex_version }} + GEMINI_CLI_VERSION=${{ needs.resolve-versions.outputs.gemini_cli_version }} + ATLAS_CLI_VERSION=${{ needs.resolve-versions.outputs.atlas_cli_version }} + COPILOT_API_VERSION=${{ needs.resolve-versions.outputs.copilot_api_version }} + + build-rust: + name: Build Nightly Rust Image + needs: [resolve-versions, build-base] + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=nightly-rust + type=raw,value=nightly-${{ needs.resolve-versions.outputs.stamp }}-rust + labels: | + org.opencontainers.image.title=deva-nightly-rust + org.opencontainers.image.description=Nightly deva rust image with latest upstream CLI versions + + - name: Build and push rust image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile.rust + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha,scope=nightly-rust + cache-to: type=gha,mode=max,scope=nightly-rust + build-args: | + BASE_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:nightly-${{ needs.resolve-versions.outputs.stamp }} + + summary: + name: Nightly Summary + needs: [resolve-versions, build-base, build-rust] + runs-on: ubuntu-latest + steps: + - name: Publish summary + run: | + cat <> "$GITHUB_STEP_SUMMARY" + ## Published Nightly Images + + - \`ghcr.io/thevibeworks/deva:nightly\` + - \`ghcr.io/thevibeworks/deva:nightly-${{ needs.resolve-versions.outputs.stamp }}\` + - \`ghcr.io/thevibeworks/deva:nightly-rust\` + - \`ghcr.io/thevibeworks/deva:nightly-${{ needs.resolve-versions.outputs.stamp }}-rust\` + + This workflow refreshes nightly image tags only. + Semver releases and GitHub Releases remain manual. + EOF diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 8f8e5ba..9eb407f 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -38,6 +38,9 @@ jobs: - name: Build site run: mkdocs build --strict + - name: Set docs domain marker + run: printf 'docs.deva.sh\n' > site/CNAME + - name: Remove unpublished artifacts run: rm -rf site/devlog diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5e8bf08..17b2a9f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,8 +16,64 @@ env: IMAGE_NAME: thevibeworks/deva jobs: + prepare: + name: Prepare Release Metadata + runs-on: ubuntu-latest + outputs: + release_tag: ${{ steps.release.outputs.release_tag }} + steps: + - name: Resolve release tag + id: release + shell: bash + run: | + set -euo pipefail + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + release_tag="${{ github.event.inputs.tag }}" + else + release_tag="${GITHUB_REF#refs/tags/}" + fi + [ -n "$release_tag" ] || { echo "error: empty release tag" >&2; exit 1; } + echo "release_tag=$release_tag" >> "$GITHUB_OUTPUT" + + resolve-versions: + name: Resolve Tool Versions + runs-on: ubuntu-latest + outputs: + 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 }} + atlas_cli_version: ${{ steps.versions.outputs.atlas_cli_version }} + copilot_api_version: ${{ steps.versions.outputs.copilot_api_version }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: "22" + + - name: Resolve versions + id: versions + env: + GH_TOKEN: ${{ github.token }} + run: bash ./scripts/resolve-tool-versions.sh + + - name: Summary + run: | + cat <> "$GITHUB_STEP_SUMMARY" + ## Release Tool Versions + + - Claude Code: \`${{ steps.versions.outputs.claude_code_version }}\` + - Codex: \`${{ steps.versions.outputs.codex_version }}\` + - Gemini CLI: \`${{ steps.versions.outputs.gemini_cli_version }}\` + - Atlas CLI: \`${{ steps.versions.outputs.atlas_cli_version }}\` + - Copilot API: \`${{ steps.versions.outputs.copilot_api_version }}\` + EOF + build-and-push: name: Build and Push Docker Image + needs: [prepare, resolve-versions] runs-on: ubuntu-latest permissions: contents: read @@ -25,6 +81,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + ref: ${{ needs.prepare.outputs.release_tag }} - name: Set up QEMU uses: docker/setup-qemu-action@v3 @@ -45,8 +103,8 @@ jobs: with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | - type=ref,event=tag - type=raw,value=latest,enable={{is_default_branch}} + type=raw,value=${{ needs.prepare.outputs.release_tag }} + type=raw,value=latest - name: Build and push base image uses: docker/build-push-action@v5 @@ -59,17 +117,25 @@ jobs: labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max + build-args: | + CLAUDE_CODE_VERSION=${{ needs.resolve-versions.outputs.claude_code_version }} + CODEX_VERSION=${{ needs.resolve-versions.outputs.codex_version }} + GEMINI_CLI_VERSION=${{ needs.resolve-versions.outputs.gemini_cli_version }} + ATLAS_CLI_VERSION=${{ needs.resolve-versions.outputs.atlas_cli_version }} + COPILOT_API_VERSION=${{ needs.resolve-versions.outputs.copilot_api_version }} build-and-push-rust: name: Build and Push Rust Profile Image runs-on: ubuntu-latest - needs: build-and-push + needs: [prepare, build-and-push] permissions: contents: read packages: write steps: - name: Checkout uses: actions/checkout@v4 + with: + ref: ${{ needs.prepare.outputs.release_tag }} - name: Set up QEMU uses: docker/setup-qemu-action@v3 @@ -90,8 +156,8 @@ jobs: with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | - type=ref,event=tag,suffix=-rust - type=raw,value=rust,enable={{is_default_branch}} + type=raw,value=${{ needs.prepare.outputs.release_tag }}-rust + type=raw,value=rust - name: Build and push rust image uses: docker/build-push-action@v5 @@ -105,51 +171,33 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max build-args: | - BASE_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} + BASE_IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.prepare.outputs.release_tag }} release: name: Create GitHub Release runs-on: ubuntu-latest - needs: [build-and-push, build-and-push-rust] + needs: [prepare, build-and-push, build-and-push-rust] permissions: contents: write steps: - name: Checkout uses: actions/checkout@v4 with: + ref: ${{ needs.prepare.outputs.release_tag }} fetch-depth: 0 - - name: Get version from tag - id: version - run: | - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - echo "version=${{ github.event.inputs.tag }}" >> $GITHUB_OUTPUT - else - echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT - fi - - - name: Update version in deva.sh - run: | - VERSION="${{ steps.version.outputs.version }}" - # Remove 'v' prefix if present - VERSION=${VERSION#v} - sed -i "s/^VERSION=.*/VERSION=\"$VERSION\"/" deva.sh - git config --local user.email "action@github.com" - git config --local user.name "GitHub Action" - git add deva.sh - git commit -m "Update version to $VERSION" || echo "No changes to commit" - - name: Generate release notes id: release_notes + shell: bash run: | - # Get the previous tag - PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD~1 2>/dev/null || echo "") + set -euo pipefail + RELEASE_TAG="${{ needs.prepare.outputs.release_tag }}" + PREVIOUS_TAG="$(git tag --sort=-version:refname | grep -Fxv "$RELEASE_TAG" | head -1 || true)" - # Generate release notes if [ -n "$PREVIOUS_TAG" ]; then echo "## Changes since $PREVIOUS_TAG" > release_notes.md echo "" >> release_notes.md - git log --pretty=format:"- %s (%h)" $PREVIOUS_TAG..HEAD >> release_notes.md + git log --pretty=format:"- %s (%h)" "$PREVIOUS_TAG..$RELEASE_TAG" >> release_notes.md else echo "## Initial Release" > release_notes.md echo "" >> release_notes.md @@ -160,11 +208,11 @@ jobs: echo "## Docker Images" >> release_notes.md echo "" >> release_notes.md echo "**Base Profile (Python, Node, Go):**" >> release_notes.md - echo "- \`ghcr.io/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}\`" >> release_notes.md + echo "- \`ghcr.io/${{ env.IMAGE_NAME }}:$RELEASE_TAG\`" >> release_notes.md echo "- \`ghcr.io/${{ env.IMAGE_NAME }}:latest\`" >> release_notes.md echo "" >> release_notes.md echo "**Rust Profile (includes Rust toolchain):**" >> release_notes.md - echo "- \`ghcr.io/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}-rust\`" >> release_notes.md + echo "- \`ghcr.io/${{ env.IMAGE_NAME }}:${RELEASE_TAG}-rust\`" >> release_notes.md echo "- \`ghcr.io/${{ env.IMAGE_NAME }}:rust\`" >> release_notes.md echo "" >> release_notes.md echo "## Supported Architectures" >> release_notes.md @@ -175,8 +223,8 @@ jobs: - name: Create Release uses: softprops/action-gh-release@v1 with: - tag_name: ${{ steps.version.outputs.version }} - name: Release ${{ steps.version.outputs.version }} + tag_name: ${{ needs.prepare.outputs.release_tag }} + name: Release ${{ needs.prepare.outputs.release_tag }} body_path: release_notes.md generate_release_notes: true append_body: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 22c941b..0ecb0a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `SECURITY.md` with private vulnerability reporting guidance - `CONTRIBUTING.md` with the repo workflow, local checks, and release rules - `docs/` guide set for quick start, internals, philosophy, authentication, advanced usage, and troubleshooting -- `mkdocs.yml`, `docs/index.md`, and GitHub Pages workflow for publishing the docs site +- `mkdocs.yml`, `docs/index.md`, and GitHub Pages workflow for publishing the docs site at `docs.deva.sh` +- scheduled `nightly-images.yml` workflow that publishes fresh nightly container tags without minting semver releases ### Fixed - Claude `--auth-with api-key` now forwards `ANTHROPIC_AUTH_TOKEN` and `ANTHROPIC_BASE_URL` @@ -22,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Config-home fan-out skips loose credential files, backup files, VCS junk, and `.DS_Store` - Auth-specific persistent containers now include the agent in the name suffix, avoiding cross-agent reuse with the wrong env or mounts - `install.sh` now installs the full current agent set, including Gemini and `shared_auth.sh` +- release and nightly container workflows now resolve tool versions through the same script, and release no longer invents a local commit inside Actions ### Changed - Rewrote `README.md` into a deva.sh front page with a real docs index and sharper OSS positioning diff --git a/DEV-LOGS.md b/DEV-LOGS.md index 83fcfb1..fb64ba9 100644 --- a/DEV-LOGS.md +++ b/DEV-LOGS.md @@ -21,9 +21,11 @@ - revalidated the docs against real `--dry-run` output instead of just `--help` - corrected the docs and CLI help to describe persistent containers as project-scoped shapes, not a naive single-container story - fixed auth-specific persistent naming to include the agent and fixed Copilot `--dry-run` so it no longer starts the proxy - - added MkDocs config, a GitHub Pages deploy workflow, a dedicated docs site home page, and CI docs-build validation + - retargeted the docs site config to `docs.deva.sh`, added a GitHub Pages workflow path for the docs subdomain, and kept CI docs-build validation + - added a nightly image workflow that resolves latest upstream tool versions and publishes `nightly` and dated nightly container tags without creating fake semver releases + - factored version resolution into a shared script so nightly and tagged release images stop drifting, and removed the fake "commit during release workflow" step - aligned `CHANGELOG.md` and contribution guidance with the new docs split -- Result: the repo now has an actual docs spine for onboarding, internals, auth, and advanced workflows, the documented behavior matches the observed runtime shape, and the repo is ready to publish docs through GitHub Pages +- Result: the repo now has an actual docs spine for onboarding, internals, auth, and advanced workflows, the documented behavior matches the observed runtime shape, docs can live on `docs.deva.sh`, and both nightly and tagged image builds use one consistent version-resolution path instead of hand-wavy workflow divergence # [2026-03-11] Dev Log: OSS repo polish and auth mount cleanup - Why: the repo still looked half-finished in public, the installer lagged behind the actual agent set, and recent auth switching work exposed ugly mount behavior diff --git a/README.md b/README.md index 4dc2b31..57b2b8e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # deva.sh [![CI](https://img.shields.io/github/actions/workflow/status/thevibeworks/deva/ci.yml?branch=main&label=ci)](https://github.com/thevibeworks/deva/actions/workflows/ci.yml) +[![Docs](https://img.shields.io/badge/docs-docs.deva.sh-111111)](https://docs.deva.sh) [![Release](https://img.shields.io/github/v/release/thevibeworks/deva?sort=semver)](https://github.com/thevibeworks/deva/releases) [![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) @@ -74,6 +75,7 @@ Project policy and OSS housekeeping: - [Contributing](CONTRIBUTING.md) - [Security Policy](SECURITY.md) - [MIT License](LICENSE) +- [Live Docs](https://docs.deva.sh) Deep research note: @@ -154,6 +156,18 @@ Basic checks: If you changed auth, mounts, or container lifecycle, run the real path. Do not ship "should work". +## Images + +Stable release tags: + +- `ghcr.io/thevibeworks/deva:latest` +- `ghcr.io/thevibeworks/deva:rust` + +Nightly refresh tags: + +- `ghcr.io/thevibeworks/deva:nightly` +- `ghcr.io/thevibeworks/deva:nightly-rust` + ## License MIT. See [LICENSE](LICENSE). diff --git a/docs/index.md b/docs/index.md index 97f7b45..81482e5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -9,6 +9,7 @@ rebuilding the same state every run. ## Start Here +- [Live docs site](https://docs.deva.sh) - [Quick Start](quick-start.md) - [Authentication Guide](authentication.md) - [Troubleshooting](troubleshooting.md) @@ -67,3 +68,8 @@ deva.sh stop - [Contributing](https://github.com/thevibeworks/deva/blob/main/CONTRIBUTING.md) - [Security Policy](https://github.com/thevibeworks/deva/blob/main/SECURITY.md) - [MIT License](https://github.com/thevibeworks/deva/blob/main/LICENSE) + +## Images + +- Stable: `ghcr.io/thevibeworks/deva:latest`, `ghcr.io/thevibeworks/deva:rust` +- Nightly: `ghcr.io/thevibeworks/deva:nightly`, `ghcr.io/thevibeworks/deva:nightly-rust` diff --git a/mkdocs.yml b/mkdocs.yml index 910f26a..9bb1d2f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,6 +1,6 @@ -site_name: deva.sh +site_name: deva.sh docs site_description: Docker-based multi-agent launcher for Claude, Codex, and Gemini -site_url: https://deva.sh +site_url: https://docs.deva.sh repo_url: https://github.com/thevibeworks/deva repo_name: thevibeworks/deva edit_uri: "" diff --git a/scripts/resolve-tool-versions.sh b/scripts/resolve-tool-versions.sh new file mode 100644 index 0000000..ee32380 --- /dev/null +++ b/scripts/resolve-tool-versions.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck disable=SC1091 +source "$SCRIPT_DIR/release-utils.sh" + +emit() { + local key=$1 value=$2 + printf '%s=%s\n' "$key" "$value" + if [[ -n ${GITHUB_OUTPUT:-} ]]; then + printf '%s=%s\n' "$key" "$value" >> "$GITHUB_OUTPUT" + fi +} + +resolve_tool() { + local key=$1 tool=$2 value + value="$(fetch_latest_version "$tool")" + if [[ -z $value ]]; then + echo "error: failed to resolve $tool version" >&2 + exit 1 + fi + emit "$key" "$value" +} + +main() { + emit "stamp" "$(date -u +%Y%m%d)" + resolve_tool "claude_code_version" "claude-code" + resolve_tool "codex_version" "codex" + resolve_tool "gemini_cli_version" "gemini-cli" + resolve_tool "atlas_cli_version" "atlas-cli" + resolve_tool "copilot_api_version" "copilot-api" +} + +main "$@"