# AiderHeal 110 主機部署 SOP（ADR-020）

> 解決 2026-05-03 發現的 AiderHeal 100% no-op 根因：110 主機上 `AIDER_REPO_PATH` (`/home/wooo/ewoooc`) **不存在**，所有 `cd` 立刻失敗，`|| true` 吞掉錯誤後整條 pipeline 走完卻 0 次 push。
>
> 本 SOP 設定一次後永久生效，需統帥手動執行（牽涉 SSH key 部署 + Gitea push 權限驗證）。

---

## 前置確認

| # | 檢查項 | 命令 |
|---|--------|------|
| 1 | 110 主機可達 | `ssh wooo@192.168.0.110 hostname` |
| 2 | 110 上是否已有 `~/.ssh/autoheal_id_ed25519` | `ssh wooo@192.168.0.110 'ls -la ~/.ssh/autoheal*'` |
| 3 | 188 容器內 `config/autoheal_id_ed25519` 是否存在 | `ssh ollama@192.168.0.188 'ls -la /home/ollama/momo-pro-system/config/autoheal*'` |
| 4 | Gitea 上該 ssh key 是否已加為 deploy key（write 權限）| Gitea → wooo/ewoooc → Settings → Deploy Keys |

**若 #2 #3 都 OK 且 #4 已加** → 直接跳到「步驟 2 clone repo」  
**若 #2 缺 key** → 走「步驟 1 部署 SSH Key」  
**若 #4 沒加** → 走「步驟 3 加 Gitea Deploy Key」

---

## 步驟 1：部署 SSH Key 到 110 主機

容器內已有的 key 同步到 110，作為 110 push 回 Gitea 的身份。

```bash
# 從 188 取出私鑰（container mount 點）
ssh ollama@192.168.0.188 'cat /home/ollama/momo-pro-system/config/autoheal_id_ed25519' \
  | ssh wooo@192.168.0.110 'umask 077 && cat > ~/.ssh/autoheal_id_ed25519'

# 取公鑰
ssh ollama@192.168.0.188 'cat /home/ollama/momo-pro-system/config/autoheal_id_ed25519.pub' \
  | ssh wooo@192.168.0.110 'cat > ~/.ssh/autoheal_id_ed25519.pub'

# 設權限
ssh wooo@192.168.0.110 'chmod 600 ~/.ssh/autoheal_id_ed25519 && chmod 644 ~/.ssh/autoheal_id_ed25519.pub'

# 在 ~/.ssh/config 加 host alias 讓 git 自動用此 key
ssh wooo@192.168.0.110 'cat >> ~/.ssh/config << "EOF"

Host gitea-autoheal
    HostName 192.168.0.110
    Port 3022
    User git
    IdentityFile ~/.ssh/autoheal_id_ed25519
    IdentitiesOnly yes
EOF
chmod 600 ~/.ssh/config'
```

> **Port 3022 確認**：用 `ssh wooo@192.168.0.110 'docker ps | grep gitea'` 看 Gitea SSH port，預設 3022 但可能不同。

---

## 步驟 2：在 110 上 clone repo 到 `/home/wooo/ewoooc`（直接 SSH clone）

> 注意：**從一開始就用 SSH clone**，避免 HTTP clone 在 private repo 卡帳密 prompt + 跟步驟 1 部署的 key 不關聯。先確認 Gitea SSH port（預設 3022 但可能被改）：
>
> ```bash
> ssh wooo@192.168.0.110 'docker ps --format "{{.Ports}}" | grep gitea'
> ```
>
> 從輸出找到 `0.0.0.0:NNN->22/tcp` 的 NNN 即為 Gitea SSH port。下方用 3022 為例，**請依實況替換**。

```bash
ssh wooo@192.168.0.110 << 'EOF'
set -e
cd ~

# 防呆：如果 ewoooc 已存在但不是 git repo（可能舊垃圾），先備份
if [ -d ewoooc ] && [ ! -d ewoooc/.git ]; then
    mv ewoooc ewoooc.bak.$(date +%s)
fi

# 直接 SSH clone（複用步驟 1 部署的 key + ~/.ssh/config 的 gitea-autoheal alias）
if [ ! -d ewoooc/.git ]; then
    git clone gitea-autoheal:wooo/ewoooc.git ewoooc
fi

cd ewoooc

# 設 git identity 讓 AiderHeal commit 有可識別作者
git config user.name "AiderHeal"
git config user.email "autoheal@wooo.work"

# 確認 remote 走 SSH（gitea-autoheal alias 自帶正確 port + key）
git remote -v
git log --oneline -3
EOF
```

驗證：應印出 `origin gitea-autoheal:wooo/ewoooc.git`（fetch+push 兩行）和最近 3 個 commit。

> **若 clone 失敗報 `Permission denied (publickey)`**：步驟 3 的 Gitea Deploy Key 還沒加或沒勾 write access，先回去處理步驟 3。

---

## 步驟 3：在 Gitea 加 Deploy Key（若 #4 沒加）

1. 取公鑰：
   ```bash
   ssh wooo@192.168.0.110 'cat ~/.ssh/autoheal_id_ed25519.pub'
   ```
2. Gitea Web UI：
   - 開 `http://192.168.0.110:3001/wooo/ewoooc/settings/keys`
   - Add Deploy Key
   - Title: `AiderHeal 110 host`
   - Key: 貼上 #1 的公鑰
   - **勾選 `Allow write access`**（必要！否則只能 fetch 不能 push）
   - Add Key

---

## 步驟 4：端到端驗證

### 4a. 110 上手動測試 push 鏈

```bash
ssh wooo@192.168.0.110 << 'EOF'
cd ~/ewoooc
git fetch origin main
git status
EOF
```

預期：`fetch` 不報錯，`status` 顯示 `Your branch is up to date`。

### 4b. 從 188 容器測試 SSH 鏈（模擬 AiderHeal preflight）

> 早期版本曾用 `docker exec ... bash -c "ssh ... \"...\""` 三層引號，內層雙引號會被中層吃掉，導致 `&& echo PREFLIGHT_OK` 變成本地 echo 而非 remote echo —— **永遠 false positive**。改用 heredoc + 單引號嵌套保護：

```bash
ssh ollama@192.168.0.188 << 'OUTER'
docker exec momo-pro-system bash -c '
  ssh -i /app/config/autoheal_id_ed25519 \
      -o StrictHostKeyChecking=no \
      wooo@192.168.0.110 "test -d /home/wooo/ewoooc/.git && echo PREFLIGHT_OK"
'
OUTER
```

預期輸出：`PREFLIGHT_OK`（**從遠端 110 印出**，非本地）。

驗證真假：故意把 path 寫錯一個字母，應該 **0 輸出**（不該印 PREFLIGHT_OK）。

### 4c. 觸發 AiderHeal pipeline 觀察

任意推一個會被 Hermes 找到 finding 的 commit（或統帥 push 一個下次自然有 finding 的 commit），等 2 分鐘後查：

```bash
# 看是否有 AiderHeal 簽名的新 commit
git fetch origin main && git log --pretty='%h | %an | %s' origin/main -5
```

預期：看到 author 是 `AiderHeal` 或 commit message 開頭 `fix(autoheal):` 的新 commit。

### 4d. 看容器 log

```bash
ssh ollama@192.168.0.188 'docker logs momo-pro-system --since 10m 2>&1 | grep -E "event=(heal_start|aider_exec|push_ok|preflight_failed|setup_failed)"'
```

預期：`event=heal_start` → `event=aider_exec`（停 10–60s）→ `event=push_ok` 連貫出現，**不應**看到 `event=preflight_failed`。

---

## 故障排除

| 症狀 | 可能原因 | 排查 |
|------|---------|------|
| `event=preflight_failed` | 110 上 `~/ewoooc` 不存在 / 不是 git repo | 重跑步驟 2 |
| `event=setup_failed` 顯示 `Permission denied (publickey)` | Gitea deploy key 未加 / write 權限沒勾 | 檢查步驟 3 |
| `event=push_failed` 顯示 `remote: hook declined` | Gitea 設 protected branch | 在 Gitea 把 main 從 protected 移除（或加 deploy key 為例外）|
| `event=no_diff` 但 aider 確實看到問題 | aider 模型品質不佳（qwen2.5-coder:7b 太小）| 改 `AIDER_MODEL` env，例如 `ollama/deepseek-coder-v2:16b`；需 110 上有對應 model |
| `event=diff_too_large` 連續發生 | finding 牽涉檔案 > 50 行修改 | 調 `AIDER_MAX_DIFF_LINES` env，但建議保留 50 作 ADR-020 安全網 |

---

## 安全護欄回顧（ADR-020）

| L | 機制 | 觸發點 |
|---|------|-------|
| L0 | preflight 路徑檢查 | `aider_heal_executor.py:execute_code_fix` 開頭 |
| L1 | 檔案白名單 `^(services\|routes\|database)/[a-zA-Z0-9_]+\.py$` | `ALLOWED_FILE_PATTERN` |
| L2 | diff > 50 行拒絕 push | `AIDER_MAX_DIFF_LINES` |
| L3 | 每小時最多 5 次 CODE_FIX | `_enforce_rate_limit` |
| L4 | health check 失敗自動 git revert | `_revert_last_commit` |
| L5 | Telegram 通知（成功/失敗/回滾）| `_notify_telegram` → EventRouter |

主開關：`CODE_REVIEW_AUTO_FIX_ENABLED=false`（docker-compose env）即時切斷整條鏈。

---

## 完成後更新

- [ ] 110 上 `~/ewoooc` 存在且 `git remote -v` 顯示走 SSH push
- [ ] Gitea deploy key 已加，write access 勾選
- [ ] 步驟 4b 印出 `PREFLIGHT_OK`
- [ ] 至少一次自然觸發 AiderHeal 後看到 `fix(autoheal):` commit
- [ ] 通知 Claude 把 memory `feedback_code_review_autoheal.md` 的「待觀察」段刪掉，標記 AiderHeal 執行層也驗證完成
