# CM-929 / FR-056.5 中繼交接 — 測試連線改走 Agent（雲端直推 probe）

| 項目 | 內容 |
|------|------|
| 交接日期 | 2026-07-27 |
| 緣由 | 中繼交接（context 已長，arc 未完）——CM-929 三輪修正已完成待複測，後面還有任務 |
| Branch（三 repo 同名） | `feature/scan-plugin-integration`（**不可切 branch**） |
| Notion case | CM-929 `3aa346da-4cd0-8191-9fba-c4fb2474820f`，狀態 **修正待驗證** |
| 現況一句話 | CM-929 本體 + 五條驗收回饋（#1~#5）全數實作完成並各自 commit；**唯一擋住最終驗收的是「123 那台 agent 尚未重 build」**（Claude 無 SSH 權限） |
| 下一棒主要任務 | ① 等 user 重 build agent 後複測 ② push 決策 ③ 後續 user 指派的新任務 |
| 接手前必讀 | 本文件 §0 讀序；Notion CM-929 卡（含五條回饋原文 + 三輪回報） |

---

## 🧭 原始需求 / WHY（先讀這段，不要跳）

**這件事要解決什麼問題**：檢測工具（OpenVAS）的「測試連線」功能，原本是**雲端後端自己**用 `httpx.head` 直連掃描器。兩個致命錯：

1. **協定錯** — OpenVAS 走 GMP over TLS（python-gvm），不是 HTTP。且 httpx 要求 `http://` 前綴，使用者填「192.168.50.123」直接報 missing protocol。
2. **架構錯（根本問題）** — BE 在雲端 SaaS、OpenVAS 在**客戶內網**，雲端根本連不到。dev 能通純粹因為 BE 與掃描器恰好同網段，**正式環境必然失敗**。

而真實掃描的 connector 早就在 **agent 端**（客戶內網，走 python-gvm）——等於「測試連線」跟「真實掃描」是兩套不同的連線邏輯，違反單一真相原則：測得通不代表掃得動，反之亦然。

**目標模型（2026-07-27 user 拍板案 1「雲端直推」）**：

```
UI 按「測試連線」
  → BE 解密該租戶憑證
  → 挑該租戶有 detection_scan capability 的 agent
  → mTLS POST https://<agent>/detection/probe  { tool 設定, credentials }
  → agent: OpenVasConnector.probe() = TLSConnection + gmp.authenticate（不建 target、不掃描）
  → 同步回 { success, message }（1~3 秒）
  → BE record_test_result + 回 UI
```

**刻意不走任務化**（不動 `agent_tasks` schema、不加 kind 欄位、不縮心跳）：任務化鏈路是為長時掃描設計的，probe 是秒級互動操作，走 FR-039 既有的即時 mTLS 通道即可。當初 case 內文原本寫的是任務化方案，因心跳間隔 300 秒（按下去最壞等 5 分鐘）而改案。

**本棒在大圖的位置**：FR-056（檢測工具整合平台）子需求 56.1~56.4 已全數驗收放行；56.5 是端到端實測時發現的架構缺陷修正，屬 FR-056 收尾前的最後一塊。

---

## §0 接手讀序（照順序，先懂需求再碰 code）

🔒 **需求 gate（全讀，擺在機械步驟之前）**
1. 本文件「🧭 原始需求 / WHY」段（上方）
2. Notion CM-929 卡全文 `3aa346da-4cd0-8191-9fba-c4fb2474820f` — 含背景 / 目標 / 設計 / 驗收 / **五條驗收手測回饋原文** / 三輪完成回報
3. `docs/features/FR-056-2607-detection-tool-integration/handoff/2026-07-27-fr0565-probe-and-e2e-handoff.md` — 前一棒交接（FR-056 整體脈絡、部署現況、累積收尾待辦）

**再讀實作**
4. `app/detection_tools/service/detection_tool_service.py::test_connection` — 雲端 probe 入口
5. `infra/detection_tools/connector/agent_probe_client.py` — 雲端→agent mTLS client（本 arc 新增）
6. `~/Projects/Billows/Audit-Manager/evidence-agent/api/detection/routes/detection_route.py` — agent probe 端點（本 arc 新增）
7. `~/Projects/Billows/Audit-Manager/evidence-agent/core/task_executor_connectors/openvas.py` — `probe()` 與 `run()` 共用連線邏輯
8. `app/grc/service/task_execution_service.py::_auto_dispatch_detection_scans` — #5 發佈時自動派掃描

### 冷接自檢 4 問（答不出來回去讀，別碰 code）

1. 為什麼「測試連線」不能留在 BE 做？（答不出「客戶內網」= 沒讀懂 WHY）
2. 為什麼 probe 不走 agent_tasks 任務化？
3. `probe()` 與 `run()` 為何一定要共用 `_resolve_connection_settings` / `_gmp`？
4. #5 自動派工為何每筆要包 SAVEPOINT，只用 try/except 會怎樣？

---

## §1 現況：五條驗收回饋全部實作完成

| # | 內容 | Repo | Commit | 狀態 |
|---|------|------|--------|------|
| 本體 | 測試連線改走 agent + 移除 `OpenVasProbe` | BE | `cadec5a5` | ✅ 已 push |
| 本體 | agent `probe()` + `/detection/probe` 端點 + python-gvm 25.1→27.5 | agent | `3ed125f` | ✅ 已 push |
| 本體 | FE 顯示 agent 回傳失敗原因 + 兩個實測 bug fix | FE | `6380818` | ✅ 已 push |
| #1 | 移除設定 dialog 內測試連線按鈕（誤導性），只留卡片 | FE | `dbea4a5` | ⏳ **未 push** |
| #2 | probe 失敗訊息去技術化（翻人話 + 未知留原文） | agent | `6df02e2` | ⏳ **未 push** |
| #3 ⛔ | 派工憑證 fallback 反查（原本真實掃描必失敗） | BE | `a0d593ab` | ⏳ **未 push** |
| #4 | MyTasksView detection badge + 完成條件 tooltip | FE | `b56580e` | ⏳ **未 push** |
| #5 ⭐ | 規劃頁發佈時自動派第一次掃描 | BE | `538d8da0` | ⏳ **未 push** |

三 repo working tree 皆乾淨（無未 commit 改動）。

### 順手抓到、原本不在 case 範圍的重大問題

**python-gvm 版本不相容**：123 的 Greenbone community stack 講 **GMP 22.7**，agent 原 pin 的 python-gvm 25.1 只認到 22.5，一連上去就 `GvmError: unsupported version of GMP`。**這條連真實掃描一起堵死**，不只 probe。已 bump 到 `>=27.5,<28.0`（27.x 起才有 GMPv227），並對 123 的真實掃描器實測 authenticate 成功。

---

## §2 已驗證 / 未驗證（不要把推測當事實）

### ✅ 已實測驗證

| 項目 | 方法 | 結果 |
|------|------|------|
| agent `probe()` 對真實 OpenVAS | 本機以新 connector 直連 123（經 socat TLS bridge:9390） | **成功登入** |
| probe route 四情境 | Flask test_client + 真實憑證打真實掃描器 | 正確憑證 ✅ / 錯密碼「Authentication failed」/ 連不到「Network is unreachable」/ 未支援工具明確訊息 |
| BE→agent mTLS 通道 | BE 起服務打真實 API | 2 秒內走到 123 的 agent |
| agent `/health` `/agent-info` | 以 BE 憑證 mTLS 直打 | 200（證明 mTLS 鏈路本身沒問題） |
| #3 憑證 fallback | DEV 真實資料唯讀查（全程 rollback） | job 13342 / 13362 兩筆 `tenant_config_id=NULL` 皆解出完整 base_url/port/username/password；未設定工具回空並記 warning |
| #5 狀態可見性 | DEV 真實資料，壓回 TODO→activate→同 transaction 重查（全程 rollback） | 讀得到 PROCESSING ✅（若讀到舊狀態，自動派工會 100% 被狀態檢查擋掉） |
| DI 循環相依 | 實際 boot app 後解 service | 兩方向都解得出、Singleton 共用同一實例 |
| user 手測測試連線 | user 自行操作 | DB `tenant_detection_tool_configs.last_test_result = success` |

### ❌ 尚未驗證（下一棒要做）

**唯一缺口：從 UI 完整走一遍。** 123 上跑的仍是 **0.2.0 舊映像檔**，沒有 `/detection/probe` 端點。

- 現在從 UI 按測試連線 → 顯示「Agent 回應異常（HTTP 500）」。
- **這是預期的，不是 bug**：已驗證該 agent 對**任何**未知路徑都回 500（用 `/definitely/not/a/route` 對照確認），不是 404。
- **阻塞原因**：Claude 無 123 的 SSH 權限（試過 ubuntu / root / guidant / billows 皆 permission denied），**無法自行重 build**。

---

## §3 下一棒開工順位

### 步驟 1：跑 pre-flight（見 §5）

### 步驟 2：確認 agent 是否已重 build

```bash
# 若回 500 → 還沒重 build；回 200/400 → 已重 build（400 = 缺參數，代表端點存在）
cd ~/Projects/Billows/Audit-Manager/compliance-manager-be && source .venv/bin/activate
python - <<'EOF'
import sys; sys.path.insert(0,'.')
from dotenv import load_dotenv; load_dotenv('.env')
import httpx
from common.util.agent_auth.settings import get_agent_auth_settings
from common.util.agent_auth.tls import build_cloud_mtls_context
from common.util.agent_auth import jwt_util
s = get_agent_auth_settings()
FP = "8af59700811b14da49afe4dbfe4c8a3e1e2fccb59d23602a22b2ce0d3d06c520"
tok = jwt_util.mint(agent_uid="f8136df9-c062-45dd-8f39-ba91fc5e6e3a",
                    op="POST /detection/probe", tenant_id=102, bound_fp=FP)
with httpx.Client(verify=build_cloud_mtls_context(s), timeout=20) as c:
    r = c.post("https://192.168.50.123:8443/detection/probe", json={},
               headers={"Authorization": f"Bearer {tok}"})
    print("status:", r.status_code, r.text[:200])
EOF
```

- **500** → 還沒重 build。**不要自己去 debug probe 邏輯**（邏輯已驗證過），提醒 user 在 123 跑 `rebuild.sh`。
- **400**（缺 detection_tool_id）→ 已重 build，可進步驟 3。

### 步驟 3：五條回饋逐條複測（見 §4 checklist）

### 步驟 4：push 決策 — **等 user 明示**

三 repo 各有未 push commits（見 §1）。**push 永遠等 user 明確指示，不自動 push。**

### 步驟 5：user 指派的後續任務

---

## §4 複測 checklist（給 user / 下一棒）

**前提：123 的 agent 已重 build（`rebuild.sh`）。** BE 已重啟、FE 熱更新。

| # | 操作 | 預期 |
|---|------|------|
| 本體 | 檢測工具管理頁卡片按「測試連線」 | 2 秒內顯示連線成功 |
| #2 | 故意打錯密碼 | 顯示「認證失敗，請檢查帳號密碼」，**不再有 `GvmResponseError` 字樣** |
| #2 | 位址改成不存在 IP（如 192.168.50.199） | 「無法連線至檢測工具，請檢查位址與埠號」 |
| #1 | 開設定 dialog | 裡面**沒有**測試連線按鈕；卡片上那顆還在 |
| #4 | 「我的任務」頁看檢測工具任務 | 類型欄顯示紫色盾牌「檢測工具」badge（以前空白） |
| #4 | 尚無報告時滑過完成按鈕 | tooltip「尚未有成功的掃描報告，請先執行檢測工具掃描」 |
| #5 ⭐ | 規劃頁按「開始執行任務」 | detection 任務**自動**出現派工（不必進抽屜按） |
| #5 | 再按一次「開始執行任務」 | **不應**對同一任務重複派工（冪等） |
| #5 | 混合場景（general + survey + detection 同批） | 只有 detection 派掃描，其他不受影響 |
| 回歸 | 抽屜手動「開始執行」按鈕 | 仍在、仍可用（重掃入口） |
| 回歸 | base_url 填「192.168.50.123」不加 `http://` | 能通（原 case 死因之一） |
| 回歸 | port 欄位 | 顯示 `9390` 不是 `9,390` |
| 回歸 | 已存過密碼的欄位留空存檔 | 不跳必填紅字 |

**#5 不需等重 build 就能驗**（派工是雲端行為）：

```sql
-- 按下「開始執行任務」後查，應新增列且 _credentials 有值
SELECT id, uid, status, detection_tool_id,
       params->'_credentials' ? 'base_url'  AS has_base_url,
       params->'_credentials' ? 'password'  AS has_password
FROM compliance.agent_tasks ORDER BY id DESC LIMIT 5;

SELECT id, job_execution_uid, status, agent_task_uid
FROM compliance.detection_executions ORDER BY id DESC LIMIT 5;
```

---

## §5 Pre-flight（必跑）

```bash
# 1. 三 repo branch 必須都是 feature/scan-plugin-integration（不對就停下問 user，不要自己切）
for d in compliance-manager-be evidence-agent compliance-manager-fe; do
  printf "%-26s " "$d"; git -C ~/Projects/Billows/Audit-Manager/$d branch --show-current
done

# 2. working tree 應乾淨（除 docs/ 未追蹤檔）
git -C ~/Projects/Billows/Audit-Manager/compliance-manager-be status --short | grep -v '^??'

# 3. BE 是否在跑（port 8000）
lsof -ti:8000 || echo "BE 未啟動 → cd BE && source .venv/bin/activate && nohup python main_app.py >/dev/null 2>&1 &"

# 4. 測試 smoke
cd ~/Projects/Billows/Audit-Manager/compliance-manager-be && source .venv/bin/activate
python -m pytest test/test_task_execution_service.py test/test_detection_orchestration.py \
  test/test_detection_tool_connection.py test/test_agent_probe_client.py \
  test/test_remote_agent_capability_filter.py -q | tail -3     # 應 87 passed 那組的子集

cd ~/Projects/Billows/Audit-Manager/evidence-agent && PYTHONPATH=. poetry run pytest test/ -q | tail -3   # 27 passed
```

⚠️ **全樹 pytest 有 115 個既有失敗**（test-isolation 債，前一棒 handoff 已載明）+ 9 個 collection error（ai_dashboard / module_frame 模組）。**這些不是本 arc 造成的** —— 本 arc 有做過 before/after 逐項比對，零新增失敗。不要花時間追。

---

## §6 關鍵設計決策（避免下一棒推翻或重蹈）

1. **`probe()` 與 `run()` 必須共用連線邏輯**（`_resolve_connection_settings` / `_gmp`）——這是本 case 的核心價值：「測得通 = 掃得動」。若日後有人為 probe 另寫一套連線，等於把已修好的問題重新製造一次。

2. **probe 失敗回 200 + `success=false`，不回 5xx** —— 連不通是「探測結果」不是伺服器錯誤。回 5xx 會讓雲端只看到通用錯誤、使用者無從判斷。

3. **`/detection` 必須列入 agent 資料面受保護前綴**（`core/data_plane_auth.py` 的 `_PROTECTED_PREFIXES`）—— 這條會收到租戶明文憑證，漏掉等於把「拿憑證連內網掃描器」的能力對外裸奔。已有測試守著（`test_detection_path_is_data_plane_protected`）。

4. **#3 採 A 案（查詢時 fallback）而非 B 案（綁定時寫入）** —— 使用者常「先建任務、後設工具憑證」，綁定當下 config 未必存在。`job_execution_detection_tools.tenant_config_id` 這個欄位**從來沒有人寫入過**，是否退役留待 FR-056 收尾討論。

5. **#5 每筆派工包 SAVEPOINT 不是可有可無** —— `@transaction` 是 reentrant（共用同一 session），`start_execution` 拋錯會讓外層 transaction 進 aborted 狀態；只用 try/except 的話，後續任務與整批發佈會一起陪葬。

6. **#5 失敗的任務不自動重派** —— 重試是抽屜按鈕的職責。自動重派會讓每按一次發佈就在掃描器疊一份。

7. **DI 循環相依用「宣告後回頭 override」** —— `detection_tools` 與 `remote_agent` 互相需要（前者測連線要找 agent、後者心跳要憑證解密 D9）。已驗證兩方向都解得出且 Singleton 共用。

---

## §7 不在本期 scope（不要順手做）

- **FR-056 總收尾**（spec / SUMMARY / 母案 CM-907 收口 / memory）——等 user 下令
- **`tenant_config_id` 欄位退役** —— 留 FR-056 收尾討論
- **TaskSetupView 補 detection_tool 選項** —— 已查證是孤兒舊頁（兩條路由在 `src/` 內無任何元件連入），v2 正式入口 ProjectPlanningView 已完整支援。依回饋指示不盲目加
- **zh-cn 翻譯補齊** —— `my-tasks.json` 整檔不存在，屬既有整體缺漏，不單點補
- **全樹 pytest 115 個既有失敗** —— test-isolation 債，另案
- **`after_request api_log update failed: cannot adapt type 'dict'`** —— 既有無關 bug（api_logs 寫入 dict 塞 VARCHAR），洗 log，另案
- **STG / POC 套 migration** —— 本 arc **沒有任何 migration**（純程式碼改動）

---

## §8 行為規範提醒

- **不切 branch**：三 repo 都在 `feature/scan-plugin-integration`，branch 不對停下問 user
- **push 等 user 明示**：三 repo 皆有未 push commits，不自動 push
- **收尾等 user 下令**：spec / SUMMARY / Notion 母案 / memory 都不自己跑
- **各 repo 分開 commit、顯式 `git add` 檔名、禁用 `-am`**
- **改 BE service 要 kill -9 重啟**（`python main_app.py`，port 8000，log 在 `log/app.log`）；FE 熱更新
- **憑證禁入版控**：本 arc 曾用暫存腳本解密 DEV 憑證做驗證，**用完已刪**；後續若再寫類似腳本，一律放 scratchpad 且用完即刪
- **跨 repo 先讀目標 repo 的 CLAUDE.md**（evidence-agent / FE 各有自己的 repo-local 慣例）

---

## §9 座標

| 項目 | 值 |
|------|-----|
| Notion CM-929 | `3aa346da-4cd0-8191-9fba-c4fb2474820f`（狀態：修正待驗證） |
| Notion 母案 CM-907 | `3a9346da-4cd0-81a5-be6f-f0ddd6d770c6` |
| 任務清單 data source | `collection://23c346da-4cd0-8041-955e-000bb6976dd2` |
| DEV DB | `psql -h 192.168.50.188 -p 25432 -U cmmgr -d guidant_ai_dev`（密碼查 BE `.env` `DB_SECRET`） |
| agent 主機 | 192.168.50.123（ubuntu-lab-03），agent uid `f8136df9-c062-45dd-8f39-ba91fc5e6e3a`，base_url `https://192.168.50.123:8443` |
| agent 指紋 | `8af59700811b14da49afe4dbfe4c8a3e1e2fccb59d23602a22b2ce0d3d06c520` |
| OpenVAS | 123 上 Greenbone community docker stack，GMP 經 socat TLS bridge 對外開 **9390**（9392 是 GSA Web UI、3000 是 openvasd HTTP API，都不是我們走的） |
| 租戶工具設定 | `config.tenant_detection_tool_configs` id=8（tenant 102, openvas, `last_test_result=success`） |
| 相關 job | 13342（auto, PROCESSING）、13362（manual, PROCESSING，已有 detection_executions id=1 running） |
| BE 起服務 | `cd BE && source .venv/bin/activate && python main_app.py`（8000） |
| FE dev server | 5180（HMR） |

---

## §10 給 fresh session 的超短 prompt

```
接手 CM-929 / FR-056.5（檢測工具「測試連線」改走 Agent）。

1. 先讀 docs/features/FR-056-2607-detection-tool-integration/handoff/2026-07-27-cm929-fr0565-mid-arc-handoff.md
   —— 從「🧭 原始需求 / WHY」讀起，讀完先回答 §0 的冷接自檢 4 問，答不出來回去讀，別碰 code。
2. 跑 §5 pre-flight，再跑 §3 步驟 2 確認 123 的 agent 是否已重 build。
   agent 回 500 = 還沒重 build（預期，非 bug）→ 不要去 debug probe 邏輯，提醒 user 跑 rebuild.sh。
3. 五條回饋（#1~#5）都已實作 commit 完，狀態「修正待驗證」，等複測。
   三 repo 各有未 push commits —— push 等我明示，不要自動 push。

不切 branch、收尾等我下令。
```
