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/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 new file mode 100644 index 0000000..9eb407f --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,63 @@ +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: Set docs domain marker + run: printf 'docs.deva.sh\n' > site/CNAME + + - 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/.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/.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 00b0405..0ecb0a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,35 @@ # 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). +## [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 +- `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 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` +- 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` +- 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 +- 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 ### Fixed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..5354235 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,72 @@ +# 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 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 + +## 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..fb64ba9 100644 --- a/DEV-LOGS.md +++ b/DEV-LOGS.md @@ -13,6 +13,31 @@ - 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 + - 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, 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 +- 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/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 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..57b2b8e 100644 --- a/README.md +++ b/README.md @@ -1,350 +1,173 @@ -# deva.sh Multi-Agent Wrapper +# deva.sh -> **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) +[![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) +[![Agents](https://img.shields.io/badge/agents-claude%20%7C%20codex%20%7C%20gemini-222222)](#what-this-is) -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 without pretending the agent's own sandbox is the thing keeping you safe. -**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. Explicit mounts are the contract. Persistent project containers keep the workflow fast instead of rebuilding the same state every run. -**What Changed**: `claude.sh` → `deva.sh` as the primary interface. Your existing `claude-yolo` commands still work via compatibility shims. +This repo is the source of truth for `deva.sh`. -## Quick Start - -```bash -# install the CLI wrapper -curl -fsSL https://raw.githubusercontent.com/thevibeworks/claude-code-yolo/main/install.sh | bash - -# run from a project directory -cd ~/wrk/my-project - -deva.sh # Claude by default +## What This Is -deva.sh codex -- --help # Codex CLI passthrough -``` - -`claude-yolo` simply calls `deva.sh claude` for backwards compatibility. - -> **Note** -> The older `claude.sh` / `claudeb.sh` wrappers are deprecated and now print a reminder before forwarding to `deva.sh`. +- 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 -## Multi-Agent Capabilities +Primary entry point: -**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` +- `deva.sh` -**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` +Compatibility wrappers still exist: -**Shared Infrastructure**: -- Project-scoped containers (`deva---`) -- Unified config system (`.deva*` files) -- Container management (`--ps`, `--inspect`, `shell`) +- `claude.sh` +- `claude-yolo` -## Safety Checklist +## What This Is Not -- 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. +- 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. -## Advanced Directory Access (Beyond `--add-dir`) - -**Claude's `--add-dir` vs deva.sh's Military-Grade Control**: +## Quick Start ```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 -``` +curl -fsSL https://raw.githubusercontent.com/thevibeworks/deva/main/install.sh | bash -**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 - -# 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 - -# 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! +cd ~/work/my-project +deva.sh claude ``` -**XDG Config Home (Per-Agent)**: +Then inspect the container if you want: + ```bash -$XDG_CONFIG_HOME defaults to ~/.config - -# Default layout (each agent directory under ~/.config/deva mounts into /home/deva): - -~/.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 - -# Mount destinations inside container: -# - claude/* → /home/deva/* -# - codex/* → /home/deva/* - -# 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 +deva.sh shell +deva.sh ps +deva.sh stop ``` -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. +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. -Migration from legacy dotfiles: -```bash -# 1) Create the new XDG tree -mkdir -p ~/.config/deva/{claude,codex} +## Docs -# 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 +Start here if you want the short path: -# 3) Move Codex OAuth directory -if [ -d ~/.codex ]; then mv ~/.codex ~/.config/deva/codex/.codex; fi +- [Quick Start](docs/quick-start.md) +- [Authentication Guide](docs/authentication.md) +- [Troubleshooting](docs/troubleshooting.md) -# 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 +Read these if you want to understand the machinery instead of cargo-culting commands: -# 5) Verify -tree -a ~/.config/deva | sed -n '1,200p' +- [How It Works](docs/how-it-works.md) +- [Philosophy](docs/philosophy.md) +- [Advanced Usage](docs/advanced-usage.md) +- [Docs Home](docs/index.md) -# Done. Now just run -deva.sh claude -deva.sh codex -``` +Project policy and OSS housekeeping: -**Symlink Setup** +- [Contributing](CONTRIBUTING.md) +- [Security Policy](SECURITY.md) +- [MIT License](LICENSE) +- [Live Docs](https://docs.deva.sh) -Prefer links if you don’t want to move files. Docker resolves symlinks at start and binds the target. +Deep research note: -```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 -``` - -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 -``` - -## Multi-Account Auth Architecture +- [UID/GID Handling Research](docs/UID-GID-HANDLING-RESEARCH.md) -**Config Home Structure** (`--config-home DIR` / `-c DIR`): +## How It Feels -deva.sh mounts entire auth directories into `/home/deva`, enabling **isolated authentication contexts** for different accounts, organizations, or projects. - -```bash -# Organize by account type -~/auth-homes/ -├── personal/ # Personal accounts -│ ├── .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 +```text +host workspace + auth home + | + v + deva.sh + | + v + docker run / docker exec + | + v + persistent project container + /home/deva + chosen agent ``` -**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. +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. -## Container Management +## Common Commands -- `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 naming pattern: `deva---`. +```bash +# Default agent is Claude +deva.sh -## Config Files +# Same container, different agents +deva.sh codex +deva.sh gemini -We load configuration in this order (later wins): +# Throwaway run +deva.sh claude --rm -1. `$XDG_CONFIG_HOME/deva/.deva` -2. `$HOME/.deva` -3. `.deva` -4. `.deva.local` -5. Legacy `.claude-yolo*` files (still honoured for compatibility) +# Inspect what deva would run +deva.sh claude --debug --dry-run -Example `.deva` file: +# Open a shell in the project container +deva.sh shell -```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 +# Read resolved config state +deva.sh --show-config ``` -Supported keys: `VOLUME`, `ENV`, `CONFIG_HOME`, `DEFAULT_AGENT`, `HOST_NET`, plus any valid shell variable names you want exported. +## Sharp Edges -## Flexible Authentication Matrix +- `--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. -**All Authentication Methods Supported**: +## Why This Exists -| 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` | +Agent CLIs are useful. Their native permission theater is often not. -**Multi-Org Support Examples**: -```bash -# Personal dev work -deva.sh claude -c ~/auth-homes/personal +deva moves the line: -# Corporate Bedrock account -deva.sh claude -c ~/auth-homes/corp-aws --auth-with bedrock +- 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 -# 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" -``` +That is a better trade if you are working in a trusted repo and you actually want to get work done. -## Image Contents +## Development -- 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. - -## Developer Notes +Basic checks: ```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 --help +./deva.sh --version +./claude-yolo --help +./scripts/version-check.sh ``` -See `CHANGELOG.md` and `DEV-LOGS.md` in this directory for history and daily notes. +If you changed auth, mounts, or container lifecycle, run the real path. Do not ship "should work". -## Image Profiles and Local Dockerfiles +## Images -Select a development image profile (deva flags can appear before or after the agent): +Stable release tags: -```bash -# Use base image (default) -deva.sh claude +- `ghcr.io/thevibeworks/deva:latest` +- `ghcr.io/thevibeworks/deva:rust` -# Use rust toolchain image (tries pull, then local Dockerfile.rust if present) -deva.sh -p rust claude -deva.sh claude -p rust # equivalent +Nightly refresh tags: -# 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 . -``` +- `ghcr.io/thevibeworks/deva:nightly` +- `ghcr.io/thevibeworks/deva:nightly-rust` -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/agents/claude.sh b/agents/claude.sh index d2b73ce..ccd07a8 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,19 +93,26 @@ 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) 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%% *}" @@ -122,6 +133,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/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/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/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 e2e2ee6..1398193 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}" @@ -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,22 +73,25 @@ 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) + --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: @@ -98,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) @@ -612,19 +616,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 @@ -764,28 +785,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 @@ -918,25 +939,171 @@ 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 } +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() { + 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_claude_json() { + local config_base + config_base=$(resolve_config_base) + + # .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 + +} + +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") +} + +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() { local base="$1" [ -d "$base" ] || return 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 } @@ -1418,6 +1585,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 +2044,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 @@ -1888,6 +2074,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" @@ -1933,16 +2120,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 +2192,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 +2215,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 +2248,24 @@ 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}" + auth_suffix="${auth_suffix}-${ACTIVE_AGENT}" + + # 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,147 +2295,61 @@ 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 +# Back up .claude.json before mounting, without touching live credential files. +if [ "$QUICK_MODE" != true ] && [ "$DRY_RUN" != true ]; then + backup_claude_json +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 +# 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") # 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 +2372,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*) + 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,10 +2401,10 @@ 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 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..81482e5 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,75 @@ +# 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 + +- [Live docs site](https://docs.deva.sh) +- [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) + +## 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/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/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/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..9bb1d2f --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,38 @@ +site_name: deva.sh docs +site_description: Docker-based multi-agent launcher for Claude, Codex, and Gemini +site_url: https://docs.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 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 "$@" 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 +```