# FR-039 Agent 認證 v2 — Agent 端開發計劃（詳盡版）

> repo：`evidence-agent`。依據 `design-agent-auth.md` v2 + 後端計劃 §1 契約。
> 範圍：agent 自我註冊 + 心跳 + 設備指紋 + 資料面認證 + TLS。資料面 `/blob` 既有,沿用。
> **依賴契約**：BE 計劃 §1（register/heartbeat/cert/JWT/X-Agent-Fingerprint）。可對 mock 雲端先開發。
> 📋 **進度回填**：開工/完成/卡住都更新 [TRACKER](2026-06-19-agent-auth-v2-TRACKER.md)。

---

## 契約回顧（agent 視角）
- 開機 → 算 `device_uuid = sha256(product_uuid|machine_id|primary_mac)`。
- `POST <endpoint>/api/1.0/agents/register {token, device_uuid, base_url, hardware_info, agent_version, status, csr}` → 拿 `{agent_uid, certificate, ca_certificate, jwt_public_key, heartbeat_interval_sec}`。
- 每 `heartbeat_interval_sec`：`POST /api/1.0/agents/heartbeat {device_uuid, status}`（mTLS,帶拿到的 cert）。
- 資料面：收 `Authorization: Bearer <JWT>` → 驗;回應加 `X-Agent-Fingerprint`。

---

## 任務

### G1 · device_uuid 模組 + config
- `config`：讀 env `REGISTRATION_TOKEN`、`CLOUD_ENDPOINT`、cert 存放路徑（`AGENT_CERT_DIR`）、host 識別碼路徑(`/host/product_uuid`、`/host/machine-id`、`/host/mac`)。
- `core/fingerprint.py`（新）：
  ```
  product_uuid = read('/host/product_uuid')   # 缺則 ''
  machine_id   = read('/host/machine-id')
  mac          = read('/host/mac')            # 部署腳本寫入
  device_uuid  = sha256(f"{product_uuid}|{machine_id}|{mac}")
  ```
  開機算一次、快取。缺某來源 graceful(記 log,不崩)。
- **驗收**：算出穩定 device_uuid;手動改某來源 → device_uuid 變。**依賴**：BE A0 契約。

### G2 · 自我註冊 + 心跳迴圈
- `core/enroll.py`（新）：
  1. 開機若無本地 cert → 產 keypair + CSR(SAN=base_url host/IP) → `POST /agents/register`(帶 token + device_uuid + csr) → 存回傳 cert/CA/jwt_public_key 到 `AGENT_CERT_DIR`。
  2. 已有 cert → 略過註冊。
  3. 啟動背景 thread：每 `heartbeat_interval_sec` `POST /agents/heartbeat`(mTLS 帶 cert)。
- 註冊失敗(token 錯/雲端不通)→ retry with backoff + 記 log,不崩。
- **app_factory** 開機呼叫 enroll（在 init_db 後）。
- **驗收**：對(mock)雲端 register 成功拿 cert;定期 heartbeat;重啟不重複註冊(已有 cert)。**依賴**：G1 + BE B3/B4。

### G3 · 資料面認證 + TLS
- `before_request`（blob 路由）：mode=full 時驗 `Authorization` JWT —— 用 `jwt_public_key` 驗簽 + `exp` + `aud==agent_uid` + `bound_fp==本地 device_uuid`;任一不過 → 401。
- 回應加 header `X-Agent-Fingerprint: <device_uuid>`（after_request）。
- `GET /agent-info` → `{device_uuid, agent_version, status}`（供雲端/除錯）。
- **TLS / mTLS**：compose 加 **nginx sidecar** 收 mTLS(驗雲端 client cert,proxy→Flask:8000);掛載 CA/agent cert(註冊後產生的)/host 識別碼;`deploy/collect-host-id.sh`(抓 host MAC 寫 `host-id/mac`)。
  - `deploy/docker-compose.yml`：file-agent 掛 `/sys/.../product_uuid:ro`、`/etc/machine-id:ro`、`./host-id/mac:ro`、`AGENT_CERT_DIR` volume;新增 nginx service。
  - `AGENT_AUTH_MODE=none` 時 before_request 略過(維持現 demo)。
- **驗收**：無/錯 JWT 被擋;指紋不符被擋;mTLS 握手成功;mode=none 時現狀不變。**依賴**：G1 + BE B1(JWT/CA)。

---

## 紀律 / 注意
- **不動 jedi-file-upload 套件**（認證在 evidence-agent app + nginx）。
- 憑證/金鑰**不入版控**（agent cert 是執行期產生 + 雲端簽回,存 volume）。
- `AGENT_AUTH_MODE` 包覆,過程不破壞 121 demo;最後切 full。
- MAC：bridge 網路容器讀不到 host MAC → 一律靠 `collect-host-id.sh` 寫檔掛入(見 design §2.2)。
- 封裝(.msi/.deb)延後;現用 Docker + .env。
- **依賴順序**：G1 → G2、G3（G2 需雲端 B3/B4 才能整合測;可先對 mock 開發）。
