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

> repo：`compliance-manager-be`。依據 `design-agent-auth.md` v2。
> **BE 擁有共用契約 + DB migration（A0）**;FE / Agent 依本文件 §1 契約開發。
> 全程 `AGENT_AUTH_MODE=none` 包覆,不破壞現有 demo;全綠才切 `full`。
> 已完成：Phase 1 — `remote_agents` 加 `device_fingerprint`(=device_uuid) + `status`(active/revoked)。
> 📋 **進度回填**：開工/完成/卡住都更新 [TRACKER](2026-06-19-agent-auth-v2-TRACKER.md);動到 §1 契約必在 TRACKER 契約 Log 公告。

---

## §1 共用契約（canonical，FE/Agent 照這個刻）

### 控制面（agent → cloud）
**`POST /api/1.0/agents/register`**（bootstrap：TLS + token,免 mTLS）
```jsonc
// req
{ "token":"<tenant 註冊token明文>", "device_uuid":"<sha256>", "base_url":"http://<agent>:8080",
  "hardware_info":{"hostname":"..","os":"..","ip":".."}, "agent_version":"0.1.0",
  "status":"registering", "csr":"<PEM CSR>" }
// resp
{ "agent_uid":"..", "certificate":"<PEM 簽好的 server cert>", "ca_certificate":"<PEM>",
  "jwt_public_key":"<PEM>", "heartbeat_interval_sec":300 }
```
**`POST /api/1.0/agents/heartbeat`**（mTLS：agent 持註冊時拿到的 cert 當 client cert）
```jsonc
// req { "device_uuid":"..", "status":"online" }   // resp { "status":"ok" }
```

### 資料面（cloud → agent，沿用 /blob，加認證）
- 雲端請求帶 `Authorization: Bearer <JWT>`;JWT(RS256) claims：`iss=compliance-cloud, aud=<agent_uid>, exp≈now+60s, op, tenant_id, bound_fp=<device_uuid>`。
- agent 回應帶 `X-Agent-Fingerprint: <device_uuid>`。
- mTLS：雲端 client cert + 驗 agent server cert（皆對內部 CA）。

### Token 管理（cloud，FE 用）
- `GET  /api/1.0/agents/enroll-token` → `{exists, label, created_at}`（不回明文）
- `POST /api/1.0/agents/enroll-token` → 產生/重新產生 → `{token:"<明文,僅此次>", endpoint}`
- `POST /api/1.0/agents/enroll-token/revoke` → 停用

### 撤銷 / 重新綁定（cloud，FE 用）
- `POST /api/1.0/remote-agents/<uid>/revoke` | `/unrevoke` → 改 status
- `POST /api/1.0/remote-agents/<uid>/rebind` → 更新 device_fingerprint（manager;硬體換新後續同一 agent_uid）

---

## §2 任務

### A0 · migration + 契約凍結（先做,擋 FE/Agent）
- migration `scripts/sql/2026-06-19-fr039-agent-enroll.sql`：
  - `remote_agents` 加 `last_seen_at TIMESTAMP NULL`、`agent_version VARCHAR(50) NULL`、`hardware_info JSONB NULL`。
  - 新表 `compliance.remote_agent_enroll_tokens`：`id, uid, tenant_id, token_hash VARCHAR(64), label VARCHAR(100), enabled BOOLEAN DEFAULT true, created_at/created_user/...`;index(tenant_id);GRANT cm_app + sequence;RLS policy 比照 remote_agents(per-tenant);schema_migrations。
  - model/entity/mapper：`remote_agents` 補三欄;新建 `RemoteAgentEnrollToken` 全套(model/entity/query/mapper/repo,放 `domain|infra/remote_agent/...`)。
- **驗收**：migration 套 dev;契約(§1)三 repo 可照刻。

### B1 · 憑證 / JWT 基礎 util
- `common/util/agent_auth/ca.py`：載入內部 CA(cert+key 路徑來自 config/secret);`sign_csr(csr_pem, san) -> cert_pem`。
- `common/util/agent_auth/jwt_util.py`：`mint(agent_uid, op, tenant_id, bound_fp) -> jwt`（RS256,雲端私鑰,exp 60s）;`(驗證在 agent 端)`。
- config：`AGENT_AUTH_MODE`(none|full)、`AGENT_CA_CERT/KEY`、`AGENT_JWT_PRIVATE_KEY`、`AGENT_CLOUD_CLIENT_CERT/KEY` 路徑(走 env/secret,**不入版控**)。
- **驗收**：簽一張測試 cert + mint JWT,單測過。**依賴**：A0。

### B2 · 註冊 token 管理
- service：`generate(tenant)`(亂數 token→存 hash→回明文一次)、`get_current(tenant)`、`verify(token)->tenant_id`、`revoke`。
- endpoints：§1 token 管理三支。token 存 sha256;明文只在 generate 回傳。
- **先一組 active UX**：generate 時把該 tenant 既有 enabled token 設 false(等於替換);表結構支援多組(未來放寬)。
- **驗收**：產/重生/撤銷;verify 正確回 tenant、停用的 token 驗不過。**依賴**：A0。

### B3 · `/agents/register`（enrollment）
- 流程：`verify(token)`→tenant → 算/收 device_uuid → **upsert remote_agents by (tenant_id, device_fingerprint)**(寫 base_url/hardware_info/agent_version/last_seen_at=now/status=active) → `sign_csr`(B1)簽 server cert → 回 §1 resp。
- before_flush 自動補 tenant_id(已驗過機制)。
- **驗收**：帶有效 token register → DB upsert + 回合法 cert;無效 token → 401。**依賴**：A0+B1+B2。

### B4 · `/agents/heartbeat` + online/offline
- heartbeat：mTLS 驗 agent cert → 更新 `last_seen_at` + status。
- online/offline helper：`is_online(last_seen_at, interval) = now - last_seen < interval * 3`(或固定 30 分);清單 / detail 回傳時附 `online` 布林。
- **驗收**：heartbeat 更新 last_seen;清單算得出 online/offline。**依賴**：A0。

### B5 · 資料面認證（RemoteAgentAdapter）
- mode=full 時：httpx `cert=(client_cert,key)` + `verify=ca` + `Authorization: Bearer mint(...)` + 讀回應 `X-Agent-Fingerprint` 比對該 agent device_fingerprint(不符→稽核+擋) + 呼叫前 `status != revoked` 檢查。
- mode=none：完全維持現狀。base_url 走 https(mode=full)。
- **關鍵檔**：`infra/upload_file/remote_agent_adapter.py`。
- **驗收**：mode=full 對測試 agent 上傳/下載全鏈通;mode=none 不變。**依賴**：A0+B1。

### B6 · 撤銷 / 重新綁定
- `revoke/unrevoke`(status)、`rebind`(更新 device_fingerprint,manager 權限)。
- **驗收**：撤銷後雲端所有呼叫拒;rebind 更新指紋。**依賴**：A0。

---

## §3 紀律 / 測試
- DDD：route→app service(@transaction)→domain→repo;不在 route 查 DB。
- 權限：token 管理 / revoke / rebind 需 manager;register/heartbeat 是 agent 來的(走 token/mTLS,不走 JWT user)。
- error code：`common/code/error_code.py` 既有 `FILE_AGENT_*` 沿用 + 新增 enroll/token 相關(`<前綴>_<HTTP><序>`)。
- 憑證/金鑰**不入版控**;連線資訊文件只寫路徑。
- 測試在 compliance-manager-test;BE 單測(JWT/CA util、token verify)可加 `test/`。
- **依賴順序**：A0 →(B1∥B2)→ B3;B4/B5/B6 待 A0(+B1 for B5)。
