diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 9d079ed..2cb8271 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -28,7 +28,7 @@ "biomejs.biome" ], "settings": { - "terminal.integrated.defaultProfile.linux": "zsh", + "terminal.integrated.defaultProfile.linux": "bash", "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.tabSize": 2, diff --git a/.github/workflows/image-build-check.yml b/.github/workflows/image-build-check.yml new file mode 100644 index 0000000..59ab4e2 --- /dev/null +++ b/.github/workflows/image-build-check.yml @@ -0,0 +1,62 @@ +# PR validation for the Prebuilt Image definition. NOT part of the release mechanism +# (that is build-image.yml, contract: docs/adr/0001-image-release-contract.md). +# This never pushes: it only proves `devcontainer build` succeeds and the image +# satisfies the agent-first baseline (ADR 0002) before a change can merge. +name: Image Build Check + +on: + pull_request: + paths: + - 'devimage-build/**' + - '.github/workflows/image-build-check.yml' + +jobs: + build-check: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Build image (single-arch, no push) + run: | + npm install -g @devcontainers/cli + devcontainer build \ + --workspace-folder ./devimage-build \ + --image-name image-build-check:pr + + - name: Verify agent-first baseline (ADR 0002) + run: | + docker run --rm --user vscode image-build-check:pr bash -c ' + set -e + + echo "--- 必备工具(agent 与 Human Supervisor 双方所需)---" + for tool in bash git jq rg fd node npm python3 cargo gh; do + command -v "$tool" >/dev/null || { echo "::error::missing tool: $tool"; exit 1; } + echo "ok: $tool" + done + + echo "--- 工效层不得存在(zsh / oh-my-zsh / tmux / 装饰性 CLI)---" + # htop / tree 不在此列:它们由 common-utils Feature 作为基础包安装, + # 且是 Human Supervisor debug 实际会用的工具(ADR 0002 的 keep 侧)。 + for tool in zsh tmux bat batcat eza xclip; do + if command -v "$tool" >/dev/null; then echo "::error::ergonomics layer leaked in: $tool"; exit 1; fi + echo "absent: $tool" + done + [ ! -e "$HOME/.oh-my-zsh" ] || { echo "::error::oh-my-zsh present"; exit 1; } + + echo "--- 默认 shell 与用户 ---" + [ "$(getent passwd vscode | cut -d: -f7)" = "/bin/bash" ] || { echo "::error::vscode login shell is not /bin/bash"; exit 1; } + sudo -n true 2>/dev/null || { echo "::error::vscode has no passwordless sudo"; exit 1; } + + echo "--- dev-cache 符号链接 ---" + for link in "$HOME/.npm" "$HOME/.local/share/pnpm" "$HOME/.cache/pip" "$HOME/.cache/pypoetry" "$HOME/.vscode-server/extensions"; do + [ -L "$link" ] || { echo "::error::missing cache symlink: $link"; exit 1; } + echo "ok: $link -> $(readlink "$link")" + done + + echo "--- git 全局配置写在 vscode 用户下 ---" + [ "$(git config --global core.autocrlf)" = "input" ] || { echo "::error::git core.autocrlf not configured for vscode user"; exit 1; } + + echo "✅ agent-first baseline holds" + ' diff --git a/agent-compose/.env.example b/agent-compose/.env.example new file mode 100644 index 0000000..9fe59a0 --- /dev/null +++ b/agent-compose/.env.example @@ -0,0 +1,18 @@ +# 复制为 .env 后按需修改;全部变量都有默认值,不建 .env 也能直接用。 + +# 要让 agent 操作的项目目录(宿主机路径;默认挂本仓库根目录) +# WORKSPACE_DIR=F:/zsp/Projects/my-app + +# 镜像来源(fork 者改成自己的 registry;与 .devcontainer/.env 语义一致) +# DOCKERHUB_USERNAME=xiao806852034 +# IMAGE_NAME=ai-dev-container + +# pin 到具体版本(默认 latest;可复现场景设为 vX.Y.Z) +# IMAGE_TAG=v2.0.0 + +# Agent Home volume(默认与 devcontainer 共享 dev-home,登录一次两边可用) +# WSL_HOME=dev-home + +# 通过环境变量传 API key 时,先在 compose.yaml 里取消 environment 段注释 +# ANTHROPIC_API_KEY= +# OPENAI_API_KEY= diff --git a/agent-compose/README.md b/agent-compose/README.md new file mode 100644 index 0000000..93ffad9 --- /dev/null +++ b/agent-compose/README.md @@ -0,0 +1,57 @@ +# agent-compose —— 不用 devcontainer 跑 coding agent + +用纯 Docker Compose 在 Prebuilt Image 里运行 claude / codex / gemini 这类 coding agent,**不依赖 VS Code devcontainer**。适合: + +- 只想在终端里跑 agent,不开 VS Code +- 在服务器 / CI 等没有 devcontainer 工具链的环境复用同一套镜像 +- 同时给多个项目各起一个 agent 容器 + +复用与 `.devcontainer/` 相同的镜像和同名 volume(`dev-home` / `dev-cache`),因此 **agent 登录态与缓存两边共享**——在 devcontainer 里登录过的 claude,这里直接可用,反之亦然。 + +## 使用 + +```bash +cd agent-compose + +# (可选)配置目标项目目录等 +cp .env.example .env + +docker compose up -d # 启动并完成 Startup Install +docker compose exec agent claude # 或 codex / gemini / bash +docker compose down # 用完关掉 +``` + +首次 `up` 时引导脚本(`bootstrap.sh`)会安装缺失的 agent CLI(Startup Install,镜像刻意不烘焙,见 [ADR 0001](../docs/adr/0001-image-release-contract.md));agent 配置目录(`~/.claude` 等)链接到 `dev-home` volume,重建容器不丢登录态。 + +## 换一个项目 + +编辑 `.env` 的 `WORKSPACE_DIR` 指向目标项目,然后 `docker compose up -d --force-recreate`。 + +要同时跑多个项目,用 `-p` 起独立实例: + +```bash +WORKSPACE_DIR=/path/to/app docker compose -p agent-app up -d +docker compose -p agent-app exec agent claude +``` + +## 更新镜像 + +这里没有 devcontainer 的 `initializeCommand` 自动拉取(那是给 VS Code rebuild 用的,见 [ADR 0001](../docs/adr/0001-image-release-contract.md)),手动拉: + +```bash +docker compose pull && docker compose up -d --force-recreate +``` + +pin 版本:`.env` 里设 `IMAGE_TAG=vX.Y.Z`。 + +## 与 `.devcontainer/` 的关系 + +| | `.devcontainer/` | `agent-compose/` | +|---|---|---| +| 入口 | VS Code Reopen in Container | `docker compose up -d` | +| 镜像 | 同一个 Prebuilt Image | 同一个 Prebuilt Image | +| Agent 登录态 | `dev-home` volume | 同一个 `dev-home` volume | +| 缓存 | `dev-cache` volume | 同一个 `dev-cache` volume | +| Startup Install | `post-create.sh`(含 HAPI Local Hub) | `bootstrap.sh`(仅 agent CLI) | + +> HAPI Local Hub 属于 devcontainer 工作流,本目录不拉起;需要时进容器手动 `npm i -g @twsxtd/hapi` 使用。 diff --git a/agent-compose/bootstrap.sh b/agent-compose/bootstrap.sh new file mode 100644 index 0000000..5bb598e --- /dev/null +++ b/agent-compose/bootstrap.sh @@ -0,0 +1,43 @@ +#!/bin/bash +# ============================================ +# agent-compose 容器引导脚本 +# ============================================ +# 由 compose.yaml 作为容器 command 运行(对应 devcontainer 流程的 post-create.sh): +# 1. 链接 Agent Home(dev-home volume)里的 agent 配置到 ~ +# 2. Startup Install:安装缺失的 coding agent CLI(镜像刻意不烘焙,见 ADR 0001) +# 3. sleep infinity 常驻,等待 `docker compose exec agent ` 进入 +set -e + +AGENT_HOME=/home/vscode/wsl-home + +# ============================================ +# 链接 AI agent 配置(与 devcontainer 共享同一 dev-home volume) +# ============================================ +for dir in .claude .codex .gemini; do + mkdir -p "$AGENT_HOME/$dir" + ln -sfn "$AGENT_HOME/$dir" "$HOME/$dir" + echo "📦 Linked $dir -> Agent Home" +done + +# ============================================ +# 安装 AI Agents(Startup Install,幂等) +# ============================================ +if ! command -v claude &> /dev/null; then + echo "📦 Installing Claude Code..." + npm install -g @anthropic-ai/claude-code +fi + +if ! command -v codex &> /dev/null; then + echo "📦 Installing OpenAI Codex..." + npm install -g @openai/codex +fi + +if ! command -v gemini &> /dev/null; then + echo "📦 Installing Gemini CLI..." + npm install -g @google/gemini-cli +fi + +echo "✅ Agent 环境就绪。进入方式:docker compose exec agent claude(或 codex / gemini / bash)" + +# 常驻(PID 1 由 compose 的 init: true 托管,可正常响应 stop 信号) +exec sleep infinity diff --git a/agent-compose/compose.yaml b/agent-compose/compose.yaml new file mode 100644 index 0000000..605ce14 --- /dev/null +++ b/agent-compose/compose.yaml @@ -0,0 +1,49 @@ +# 纯 Docker Compose 的 coding agent 运行环境 —— 不依赖 VS Code devcontainer。 +# 复用同一个 Prebuilt Image 与同名 volume(dev-home / dev-cache), +# 因此 agent 登录态、包管理器缓存与 devcontainer 完全共享。 +# 发布契约见 docs/adr/0001-image-release-contract.md(默认消费 :latest)。 +services: + agent: + # 与 .devcontainer/compose.yaml 相同的参数化引用;fork 者在本目录 .env 覆盖。 + image: ${DOCKERHUB_USERNAME:-xiao806852034}/${IMAGE_NAME:-ai-dev-container}:${IMAGE_TAG:-latest} + + volumes: + # 要让 agent 操作的项目目录;默认挂本仓库根,改 .env 的 WORKSPACE_DIR 指向任意项目 + - ${WORKSPACE_DIR:-..}:/home/vscode/workspace + + # Agent Home(.claude / .codex / .gemini 登录态与配置),与 devcontainer 共享 + - ${WSL_HOME:-dev-home}:/home/vscode/wsl-home:cached + + # 统一缓存目录(npm / pnpm / pip / poetry),与 devcontainer 共享 + - dev-cache:/home/vscode/.cache-volumes + + # 容器内引导脚本:链接 Agent Home、Startup Install agent CLI,然后常驻 + - ./bootstrap.sh:/usr/local/bin/agent-bootstrap:ro + + working_dir: /home/vscode/workspace + + # 引导完成后 sleep infinity 常驻;用 `docker compose exec agent claude` 进入 + command: bash /usr/local/bin/agent-bootstrap + + init: true + + # 需要通过环境变量传 API key 时在此打开(值来自本目录 .env): + # environment: + # ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} + # OPENAI_API_KEY: ${OPENAI_API_KEY:-} + + # 资源限制(与 devcontainer 一致) + deploy: + resources: + limits: + memory: 8G + reservations: + memory: 4G + +volumes: + # AI agent 配置存储(与 .devcontainer/compose.yaml 同名 → 同一个 volume) + dev-home: + name: dev-home + # 统一缓存存储 + dev-cache: + name: dev-cache diff --git a/devimage-build/.devcontainer/Dockerfile b/devimage-build/.devcontainer/Dockerfile index 747e43e..2d38135 100644 --- a/devimage-build/.devcontainer/Dockerfile +++ b/devimage-build/.devcontainer/Dockerfile @@ -1,55 +1,48 @@ -FROM mcr.microsoft.com/devcontainers/base:ubuntu +# 裸 Debian 基线(ADR 0002 agent-first):不用 devcontainers/base:*——那一族预烘焙了 +# common-utils 默认配置,自带 zsh / oh-my-zsh / htop / tree 等交互工效层。 +# devcontainer 必需品(sudo、用户配置等)由 common-utils Feature 在此镜像之上补齐。 +FROM debian:trixie-slim # 避免交互式提示 ENV DEBIAN_FRONTEND=noninteractive -# 安装系统依赖(Features 未覆盖的工具) +# 安装系统依赖:agent 与 Human Supervisor 双方都需要的工具, +# 以及 Features 安装脚本运行所需的基础组件(ca-certificates / curl) +# fd-find 的二进制名为 fdfind;符号链接为 fd,便于 agent 与 Human Supervisor 直接调用 RUN apt-get update && apt-get install -y \ + ca-certificates \ + curl \ + git \ jq \ ripgrep \ fd-find \ - bat \ - eza \ - htop \ - tree \ - tmux \ - xclip \ - && rm -rf /var/lib/apt/lists/* + && rm -rf /var/lib/apt/lists/* \ + && ln -sf "$(command -v fdfind)" /usr/local/bin/fd -# 配置 Git(避免 Windows 换行符问题) -RUN git config --global core.autocrlf input \ - && git config --global init.defaultBranch main \ - && git config --global pull.rebase false +# 裸基础镜像没有非 root 用户;先建 vscode(uid/gid 1000), +# common-utils Feature 检测到同名用户已存在时会原样采用并补上 sudo 配置 +RUN groupadd --gid 1000 vscode \ + && useradd --uid 1000 --gid 1000 --create-home --shell /bin/bash vscode # 切换到 vscode 用户 USER vscode WORKDIR /home/vscode -# 创建缓存目录和符号链接 +# 配置 Git(避免 Windows 换行符问题);须在 USER vscode 之后执行, +# 否则写入的是 /root/.gitconfig,对实际运行用户无效 +RUN git config --global core.autocrlf input \ + && git config --global init.defaultBranch main \ + && git config --global pull.rebase false + +# 创建缓存目录和符号链接(包管理器缓存持久化到 dev-cache volume) RUN mkdir -p ~/.cache-volumes/npm \ && mkdir -p ~/.cache-volumes/pnpm \ && mkdir -p ~/.cache-volumes/pip \ && mkdir -p ~/.cache-volumes/poetry \ && mkdir -p ~/.cache-volumes/vscode-extensions \ - && mkdir -p ~/.cache-volumes/zsh \ - && mkdir -p ~/.cache-volumes/tmux/plugins \ && mkdir -p ~/.local/share ~/.cache ~/.vscode-server \ && ln -sf ~/.cache-volumes/npm ~/.npm \ && ln -sf ~/.cache-volumes/pnpm ~/.local/share/pnpm \ && ln -sf ~/.cache-volumes/pip ~/.cache/pip \ && ln -sf ~/.cache-volumes/poetry ~/.cache/pypoetry \ - && ln -sf ~/.cache-volumes/vscode-extensions ~/.vscode-server/extensions \ - && ln -sf ~/.cache-volumes/zsh ~/.zsh \ - && ln -sf ~/.cache-volumes/tmux ~/.tmux - -# 配置 tmux(持久化到 dev-cache) -RUN git clone https://github.com/tmux-plugins/tpm ~/.cache-volumes/tmux/plugins/tpm -COPY --chown=vscode:vscode configs/tmux.conf /home/vscode/.cache-volumes/tmux/.tmux.conf -RUN ln -sf ~/.cache-volumes/tmux/.tmux.conf ~/.tmux.conf - -# 配置 zsh(持久化到 dev-cache) -RUN git clone https://github.com/zsh-users/zsh-autosuggestions ~/.cache-volumes/zsh/zsh-autosuggestions \ - && git clone https://github.com/zsh-users/zsh-syntax-highlighting ~/.cache-volumes/zsh/zsh-syntax-highlighting -COPY --chown=vscode:vscode configs/zshrc-addon /tmp/zshrc-addon -RUN cat /tmp/zshrc-addon >> ~/.zshrc \ - && rm /tmp/zshrc-addon + && ln -sf ~/.cache-volumes/vscode-extensions ~/.vscode-server/extensions diff --git a/devimage-build/.devcontainer/configs/tmux.conf b/devimage-build/.devcontainer/configs/tmux.conf deleted file mode 100644 index a3617b9..0000000 --- a/devimage-build/.devcontainer/configs/tmux.conf +++ /dev/null @@ -1,20 +0,0 @@ -# tmux 基础配置 -set -g @plugin 'tmux-plugins/tpm' -set -g @plugin 'tmux-plugins/tmux-sensible' - -# 启用鼠标 -set -g mouse on - -# 修改前缀键为 Ctrl-a -set -g prefix C-a -bind C-a send-prefix - -# 分屏快捷键 -bind | split-window -h -bind - split-window -v - -# 窗口导航 -bind h select-pane -L -bind j select-pane -D -bind k select-pane -U -bind l select-pane -R diff --git a/devimage-build/.devcontainer/configs/zshrc-addon b/devimage-build/.devcontainer/configs/zshrc-addon deleted file mode 100644 index 11df35f..0000000 --- a/devimage-build/.devcontainer/configs/zshrc-addon +++ /dev/null @@ -1,18 +0,0 @@ -# zsh 主题(随机) -ZSH_THEME="random" - -# zsh 插件 -plugins=(git z sudo zsh-autosuggestions zsh-syntax-highlighting) - -# 实用别名 -alias ll="eza -la" -alias bat="batcat" -alias cat="bat" -alias find="fd" -alias grep="rg" - -# tmux 快捷键 -alias ta='tmux attach -t' -alias tn='tmux new -s' -alias tl='tmux ls' -alias tk='tmux kill-session -t' diff --git a/devimage-build/.devcontainer/devcontainer.json b/devimage-build/.devcontainer/devcontainer.json index cc874ce..69655b2 100644 --- a/devimage-build/.devcontainer/devcontainer.json +++ b/devimage-build/.devcontainer/devcontainer.json @@ -4,10 +4,10 @@ }, "features": { "ghcr.io/devcontainers/features/common-utils:2": { - "installZsh": true, - "configureZshAsDefaultShell": true, - "installOhMyZsh": true, - "upgradePackages": true + "installZsh": false, + "installOhMyZsh": false, + "upgradePackages": true, + "username": "vscode" }, "ghcr.io/devcontainers/features/node:2.0.0": { "version": "lts" diff --git a/devimage-build/README.md b/devimage-build/README.md index 831a652..f905f29 100644 --- a/devimage-build/README.md +++ b/devimage-build/README.md @@ -9,46 +9,39 @@ devimage-build/ └── .devcontainer/ ├── Dockerfile # 镜像定义 ├── devcontainer.json # Features 配置 - ├── devcontainer-lock.json # Features 锁定文件 - └── configs/ # 配置文件 - ├── tmux.conf # tmux 配置 - └── zshrc-addon # zsh 配置追加内容 + └── devcontainer-lock.json # Features 锁定文件 ``` ## 镜像信息 - **镜像名称**:`xiao806852034/ai-dev-container:latest` -- **基础镜像**:`mcr.microsoft.com/devcontainers/base:ubuntu` +- **基础镜像**:`debian:trixie-slim` - **支持架构**:`linux/amd64`, `linux/arm64` ## 预装内容 ### Features(通过 devcontainer.json) -- Node.js LTS + pnpm -- Python 3.12 + Poetry +- Node.js LTS + pnpm / yarn +- Python 3.13(os-provided)+ pipx - Rust - GitHub CLI - Docker-in-Docker ### 系统工具(通过 Dockerfile) +镜像走 **agent-first** 基线(见 [ADR 0002](../docs/adr/0002-agent-first-baseline.md)):只烘焙 AI Agent Toolchain 与 Human Supervisor debug 双方都需要的工具。基础镜像用**裸 `debian:trixie-slim`** 而非 `devcontainers/base:*`——后者预烘焙了 common-utils 默认配置,自带 zsh / oh-my-zsh 等交互工效层。devcontainer 必需品(非 root 用户、sudo、locale 等)由 Dockerfile 自建 `vscode` 用户 + `common-utils` Feature(`installZsh: false`)补齐;`tmux` / `bat` / `eza` / `xclip`、zsh 插件与 tmux 配置均不安装(`htop` / `tree` 随 common-utils 基础包进入,算 debug 工具的 keep 侧,不视为泄漏)。基线由 CI 把关:`.github/workflows/image-build-check.yml` 在 PR 上构建镜像并断言上述工具的存在/缺席。 + - jq - ripgrep (rg) -- fd-find -- bat -- eza -- htop -- tree -- tmux -- xclip +- fd-find(以 `fd` 暴露:`fdfind` 在 `PATH` 上符号链接为 `fd`) ### 用户配置(通过 Dockerfile) - Git 配置(避免 Windows 换行符问题) -- tmux 配置 + TPM 插件管理器 -- zsh 配置 + 插件(autosuggestions, syntax-highlighting) -- 缓存目录符号链接 +- 包管理器缓存目录符号链接(npm / pnpm / pip / poetry / vscode-extensions) + +> 默认登录 shell 为 `bash`(Dockerfile `useradd --shell /bin/bash` 创建 `vscode` 用户):`common-utils` Feature 设 `installZsh: false` / `installOhMyZsh: false` 且不 `configureZshAsDefaultShell`。Human Supervisor 进容器即得到可用的 `bash`;模板侧终端 profile(`.devcontainer/devcontainer.json`)同样默认 `bash`。 ## 本地构建 @@ -134,58 +127,6 @@ RUN apt-get update && apt-get install -y \ } ``` -### 修改 tmux 配置 - -编辑 `configs/tmux.conf`: - -```bash -# tmux 基础配置 -set -g @plugin 'tmux-plugins/tpm' -set -g @plugin 'tmux-plugins/tmux-sensible' - -# 启用鼠标 -set -g mouse on - -# 修改前缀键为 Ctrl-a -set -g prefix C-a -bind C-a send-prefix - -# 分屏快捷键 -bind | split-window -h -bind - split-window -v - -# 窗口导航 -bind h select-pane -L -bind j select-pane -D -bind k select-pane -U -bind l select-pane -R -``` - -### 修改 zsh 配置 - -编辑 `configs/zshrc-addon`: - -```bash -# zsh 主题(随机) -ZSH_THEME="random" - -# zsh 插件 -plugins=(git z sudo zsh-autosuggestions zsh-syntax-highlighting) - -# 实用别名 -alias ll="eza -la" -alias bat="batcat" -alias cat="bat" -alias find="fd" -alias grep="rg" - -# tmux 快捷键 -alias ta='tmux attach -t' -alias tn='tmux new -s' -alias tl='tmux ls' -alias tk='tmux kill-session -t' -``` - ## 缓存持久化策略 ### 符号链接映射 @@ -198,16 +139,14 @@ alias tk='tmux kill-session -t' ├── pnpm/ # → ~/.local/share/pnpm ├── pip/ # → ~/.cache/pip ├── poetry/ # → ~/.cache/pypoetry -├── vscode-extensions/ # → ~/.vscode-server/extensions -├── tmux/ # → ~/.tmux -└── zsh/ # → ~/.zsh +└── vscode-extensions/ # → ~/.vscode-server/extensions ``` ### 实现方式 ```dockerfile # 创建缓存目录 -RUN mkdir -p ~/.cache-volumes/{npm,pnpm,pip,poetry,vscode-extensions,tmux/plugins,zsh} +RUN mkdir -p ~/.cache-volumes/{npm,pnpm,pip,poetry,vscode-extensions} # 创建符号链接 RUN ln -sf ~/.cache-volumes/npm ~/.npm \ @@ -282,9 +221,11 @@ QEMU + Docker Buildx 提供多架构能力,实际构建与推送由 devcontain 修改 `Dockerfile` 第一行: ```dockerfile -FROM mcr.microsoft.com/devcontainers/base:ubuntu-22.04 +FROM debian:trixie-slim ``` +> 注意保持裸发行版镜像:换成 `devcontainers/base:*` 会重新引入预烘焙的 zsh / oh-my-zsh 等工效层,违背 ADR 0002 的 agent-first 基线。 + ### Q: 如何添加新的缓存目录? 1. 在 Dockerfile 中创建目录: