# FR-039 分散式檔案 Agent — 實作計畫 / 詳細 Spec

> **For agentic workers:** 本計畫採分階段。**Phase 0 是 de-risk spike，未過不得往下展開逐行 TDD。** 各 Task 用 checkbox 追蹤。

**Goal：** 把證據檔案的儲存從雲端拉出，落地到客戶端的「檔案 agent」；雲端依 per-tenant STORAGE_CONFIG 把檔案轉發給對應 agent，binary 不存雲端磁碟，並以 SHA-256 防竄改。

**Architecture：** agent = `jedi-file-upload` 包成的獨立極小 Flask 服務（自帶本地 Postgres，存 Local/MinIO）；雲端新增 `RemoteAgentAdapter`（實作既有 `IUploadFileProvider`）依 tenant 呼叫 agent。雲端只留參照 metadata，agent 留真檔。

**Tech Stack：** Python 3.11 / Flask + Flask-RESTful / dependency-injector / SQLAlchemy 2.0 / psycopg3 / jedi-common（session/db）/ jedi-file-upload（檔案邏輯）/ MinIO client / LibreOffice headless / Docker Compose。

**狀態（2026-06-18，v2）：** ✅ Phase 0/1/2/3 + ✅ **file_agent registry 後端**（方案 B，`agent_uid` 指向，無 UI）+ ✅ 部署資產（`evidence-agent/deploy/`）。⏳ **Phase 4**（雙客戶 demo 組裝：121 已部署、dev 已 seed，差 BE 重啟 + 102 上傳驗證）。
> **最新進度與接手** 一律看 `handoff/2026-06-18-FR039-progress.md`（進度表）+ `handoff/2026-06-18-FR039-handoff.md`（交接）+ `handoff/2026-06-18-FR039-next-prompt.md`（prompt）。
> 套件相依：jedi-file-upload mapper 補 `checksum`（未 commit，待發版/path-dep 才讓防竄改 live）。

---

## §0 關鍵設計決策與風險（先讀）

| # | 決策 | 理由 |
|---|------|------|
| D-A | **agent 是全新獨立專案**，不是 fork 主專案砍模組 | 主專案 app factory 拖進 GRC config / JWT 多租戶 / Redis / RLS / DI auto-scan 全部包袱；agent 單租戶用不到 |
| D-B | agent **完整重用 `jedi-file-upload` 套件**（不 fork 檔案/轉檔邏輯）| 維護成本最低（見 design.md §3）|
| D-C | agent storage 設定 **走 env**（用套件原廠 `FileUploadService` + config DTO），不搬 `ManagedFileUploadService`、不要 `system_configs` 表 | agent 單租戶，per-tenant 查表多餘；env 即套件預設行為 |
| D-D | agent DB **只建 `upload_files` 一張表**，本地 Postgres，**不連雲端 DB** | 自包含、隔離（design.md §3）|
| D-E | 雲端路由沿用 **per-tenant STORAGE_CONFIG**（加 `remote_agent` type），**不建新表** | STORAGE_CONFIG 已 per-tenant（`system_configs` 走 RLS）|
| D-F | uid **由 agent 產**（jedi-file-upload 預設），雲端記下當跨邊界 key | 套件已生 uid，避免雙 uid |

### ✅ 風險 R-1（已驗證解除，2026-06-18）：jedi-common session 機制可在 agent 裸跑

**結論：agent 不需 Redis、不需 JWT。** `init_db(database_url)` 後，`@transaction` 即可裸跑。

- 證據（code）：`session_scope()` 內的 `get_user_context()`（`jedi-common/session/auth/auth_context.py:17`）**只讀一個 `ContextVar`**，未設定回 `None` → 走 else 分支只下 `SET LOCAL app.is_super_admin='t'`，不碰 Redis / Flask / JWT。
- 證據（runtime spike）：以 BE 環境對臨時 docker postgres 跑 headless `FileUploadService.upload_file`（無 user_context / 無 Flask app / 無 Redis）→ 成功寫檔 + 寫 `upload_files` row + `get_upload_file` 讀回。PostgreSQL 接受 `SET LOCAL app.*` 自訂 GUC。
- **影響設計**：agent `app_factory` **不需** session 包裝、不需 Redis、不需 JWT；只要啟動時 `init_db(本地 pg URL)`。
- 待驗（非阻擋，Phase 1 docker image 內驗）：`convert_to_pdf` 需 LibreOffice headless（subprocess，不涉 transaction 風險）。

---

## §1 整體架構與兩邊職責

```
雲端（主專案 compliance-manager-be）          客戶端（新專案 guidant-file-agent）
────────────────────────────────────         ──────────────────────────────────
業務碼（不動）                                  Flask /blob API（新，薄）
  └ IUploadFileProvider（不動）                   └ FileUploadService（jedi-file-upload 原廠）
      └ RemoteAgentAdapter（新）  ──HTTP──►          └ Local / MinIO adapter（套件既有）
          └ 寫雲端 upload_files 參照                      └ 寫 agent 本地 upload_files
          └ 算 SHA-256 錨點                              └ binary 落地 + convert_to_pdf
STORAGE_CONFIG（per-tenant，加 remote_agent）   本地 Postgres（只有 upload_files）
```

---

## §2 主專案（雲端）要有的東西

### 2.1 `RemoteAgentAdapter`（新）— 實作 `IUploadFileProvider`

**Files：** Create `infra/upload_file/remote_agent_adapter.py`（或 `app/upload_file/adapter/`，依專案放 adapter 慣例）

實作介面（簽名對齊 `jedi_file_upload/ports/upload_file_provider/upload_file_provider.py`）：

| method | 行為 |
|--------|------|
| `save_file(file, user_name) -> UploadFileEntity` | ① 串流 file → HTTP `POST {base_url}/blob`；② 邊串流邊算 SHA-256（錨點）；③ agent 回 `{uid, size, mimetype}`；④ 寫**雲端** `upload_files` 參照（`storage_type=REMOTE_AGENT`, `save_file_name=uid`, `sha256=錨點`, `tenant_id`）；⑤ 回 entity |
| `get_file(uid) -> BytesIO` | HTTP `GET {base_url}/blob/{uid}` 串流回 BytesIO；**邊收邊算 SHA-256 → 比對雲端錨點**，不符 raise + audit（見 2.5）|
| `delete_file(uid) -> bool` | HTTP `DELETE {base_url}/blob/{uid}` + 刪雲端參照 row |
| `convert_to_pdf(file_uid, output_folder, output_filename=None) -> BytesIO` | HTTP `GET {base_url}/blob/{uid}/pdf` 串流回（agent 已轉好 + 快取）|

- `base_url` 由建構時注入（來自 STORAGE_CONFIG，見 2.2）。
- 寫雲端參照 row：重用 `UploadFileRepoImpl`（`jedi_file_upload/infra/repository/upload_file_repo_impl.py`）。
- HTTP client：用 `httpx`（串流上傳/下載）。

### 2.2 `ManagedFileUploadService` 加 `remote_agent` 分支

**Files：** Modify `app/upload_file/service/managed_file_upload_service.py:33-56`（`_load_config`）+ adapter 建構處

- `_load_config`：`storage_type == 'remote_agent'` → 回新的 `RemoteAgentConfigDTO(base_url=v['base_url'])`。
- adapter 建構：`remote_agent` 時 build `RemoteAgentAdapter`（**不走** `UploadProviderFactory`，因該 factory 在套件內、不該認識雲端專屬 adapter）。在 service 層直接 instantiate。
- **per-tenant 已自動生效**：`file_upload_service` 是 `providers.Factory`（`di_containers/upload_file/upload_file_containers.py:31`），每 request 重讀 STORAGE_CONFIG，RLS 回本 tenant 那筆。

### 2.3 `RemoteAgentConfigDTO`（新）

**Files：** Create `app/upload_file/dto/remote_agent_config_dto.py`
```python
@dataclass
class RemoteAgentConfigDTO(UploadConfigDTO):  # 繼承 jedi_file_upload ports UploadConfigDTO
    base_url: str = ""
    # demo 後續：api_key、timeout、verify_tls
```

### 2.4 `upload_files` schema 變更（migration）

**Files：** Create `scripts/sql/2026-06-17-fr039-upload-files-remote-agent.sql`（遵守 SQL migration 鐵則：日期註解 / schema_migrations / cmmgr 套用）

- `ALTER TABLE public.upload_files ADD COLUMN sha256 VARCHAR(64)`（防竄改錨點，nullable，舊資料無值）
- `ALTER TABLE public.upload_files ADD COLUMN tenant_id ...`（參照表隔離；型別對齊專案 tenant_id 慣例）
- `storage_type` 新值 `REMOTE_AGENT`（VARCHAR(20) 容得下，無需改欄）
- **後續**（非本期）：upload_files 補 RLS policy
- `sha256` 同步加進 `jedi_file_upload/infra/models/upload_file.py`（套件異動，兩邊共用此 model；走 path-dependency dev 流程，發版時機見 CLAUDE.md）

### 2.5 完整性驗證 + 稽核事件

**Files：** Modify `api/uploadfile/routes/uploadfile_route.py`（download/preview route）或在 `RemoteAgentAdapter.get_file`

- 下載 relay 時對收到 bytes 重算 SHA-256 → 比對雲端 `upload_files.sha256` 錨點。
- 不符 → raise（擋下回傳）+ 寫稽核事件：`logger.info(..., extra={'event_code': <新碼>})`（沿用 `common/util/audit_log.py` pattern，event_code 參考 FR-033 規範新增一個「疑似竄改」碼）。

### 2.6 STORAGE_CONFIG 設定（每個用 agent 的 tenant 一筆）

`system_config` group=`STORAGE_CONFIG` key=`CONFIG`：
```json
{ "storage_type": "remote_agent", "base_url": "https://<該客戶 agent>" }
```

---

## §3 Agent 要有的東西（新專案 `evidence-agent`）

> 專案已建立：`~/Projects/Billows/Audit-Manager/evidence-agent`（poetry 2.1.1、PEP 621 `[project]`、git main、已有 BE 同款 `.gitignore` + seed `CLAUDE.md`）。⚠️ 現為 `requires-python ^3.13`，jedi-* baseline 是 3.11 → 動工前確認相容或對齊 3.11。

### 3.0 開發規範立場：「縮小版主專案」，不堆空殼層

agent 自己**沒有 domain 邏輯**（上傳/下載/轉檔/寫 DB 全在 `jedi-file-upload` 套件內），所以：

- **照主專案可辨識的骨架**：`core/`（app factory）、`api/<mod>/__init__` + Flask-RESTful route 風格、`di_containers/` + dependency-injector、`config/` config class + ENV 選擇、jedi-common error envelope/handler、**poetry + Nexus 來源**。
- **不照會變空殼的層**：**不建 `domain/` `infra/` `app/` 三層**（邏輯在套件內，硬建是 cargo-cult）。
- **保留分層精神**：route 呼叫注入的 `FileUploadService`（= 套件的 app service 層），剛好就是「route 不碰 DB」的正確 pattern，不需再多包 pass-through。
- 未來 agent 真長出自有邏輯，再補層。

### 3.1 專案結構（縮小版主專案形狀）

```
evidence-agent/
├── pyproject.toml                 # poetry 管理，Nexus 為 primary source（§3.2）
├── poetry.lock
├── .env.example                   # storage 設定 + DB 連線
├── main_app.py                    # 進入點（對齊主專案命名）：create_app + 註冊 blob blueprint + run
├── core/
│   ├── app_factory.py             # create_app（對齊主專案 core/app_factory.py，但極小，§3.3）
│   └── extensions.py              # 需要的單例（可能只有 logger）
├── config/
│   ├── config.py                  # Config class（對齊主專案）
│   └── config_util.py             # ENV 選 config + 組 storage DTO / DB URL（§3.5）
├── api/
│   └── blob/
│       ├── __init__.py            # create_module() → blueprint（對齊主專案 api/<mod>/__init__）
│       └── routes/
│           └── blob_route.py      # Flask-RESTful Resources，/blob 五支（§3.4）
├── di_containers/
│   └── containers.py              # dependency-injector：FileUploadService + UploadProviderFactory
├── common/
│   └── code/
│       └── agent_error_code.py    # 若有自有錯誤碼（最小；多數錯誤由套件拋）
├── Dockerfile                     # 含 LibreOffice（§3.7）
└── docker-compose.yml             # file-agent + agent-db(postgres) + volume（§3.7）
```

> 注意：**沒有 `domain/` `infra/` `app/`**（見 §3.0）。不搬主專案的業務模組、GRC config、JWT 多租戶、RLS、APScheduler。

### 3.2 套件依賴（pyproject.toml，poetry）

用 **poetry**（2.1.1）管理，**PEP 621 `[project]` 格式**（專案現況），Nexus 設為 primary source（`[[tool.poetry.source]]`）。對齊 `jedi-file-upload` 的 runtime 依賴（其 pyproject 已列），裝套件本身即帶入大部分：

```toml
[project]
name = "evidence-agent"
requires-python = ">=3.11,<4"   # ⚠️ 現況 ^3.13，建議對齊 jedi-* baseline 3.11
dependencies = [
  "jedi-file-upload>=0.0.16",   # 檔案邏輯（連帶 jedi-common、minio、SQLAlchemy、psycopg）
  "jedi-common>=0.0.23",        # 顯式 pin：session_scope / init_db / BaseModel / @transaction
  "flask",
  "flask-restful",
  "dependency-injector",
]
```
- 套件異動期（加 `sha256`）走 **poetry path-dependency**（同主專案 dev 流程，CLAUDE.md），完成才發版改回 pin。
- 安裝 `poetry install`、加套件 `poetry add`、更新 `poetry update`（不用 `poetry lock`）。
- LibreOffice **非 poetry 套件** → Dockerfile 裝（§3.7）。

### 3.3 `app_factory.py`（極小）

職責（只做檔案服務跑得起來的最小集合）：
1. `Flask(__name__)`
2. `jedi_common` `init_db(database_url)`（agent 本地 Postgres URL）→ 讓 `@transaction` / `get_session` 可運作
3. `register_error_handlers(app)`（jedi-common，例外轉 HTTP envelope）
4. wire DI（§3.4 routes）
5. **不要**：JWT（demo 無 auth）、Redis（待 Phase 0 確認可省）、SocketIO、Babel、APScheduler、RLS user-context 注入

### 3.4 `/blob` routes（薄，對齊 design.md §7）

| Method | Path | 呼叫 | 回傳 |
|--------|------|------|------|
| POST | `/blob` | `file_upload_service.upload_file(file, user_name='cloud', save_dir=...)` | `{uid, save_file_name, size, mimetype, sha256}` |
| GET | `/blob/{uid}` | `file_upload_service.get_upload_file(uid)` | `send_file(stream)` |
| GET | `/blob/{uid}/pdf` | `file_upload_service.convert_to_pdf(uid, output_folder, f'{uid}.pdf')` | `send_file(pdf, mimetype='application/pdf')` |
| DELETE | `/blob/{uid}` | `file_upload_service.delete_file(uid)` | 200 |
| GET | `/health` | — | 200 |

- 直接用套件原廠 `FileUploadService`（不是 `ManagedFileUploadService`）。
- ~~**不掛 JWT**（demo）；cloud↔agent 認證列後續。~~
  → **已完成（auth v2，2026-06-19）**：正式一律 mTLS + JWT + 指紋（`AGENT_AUTH_MODE=full`），
  設計見 `design-agent-auth.md`、部署見 `evidence-agent/deploy/README.md`。`none` 僅測試用。

### 3.5 `config.py`（env → config DTO）

```python
# storage：依 STORAGE_TYPE 組 DTO
STORAGE_TYPE = os.getenv("STORAGE_TYPE", "local")  # local | minio
# local → LocalFileUploadConfigDTO(base_dir=os.getenv("BASE_DIR"))
# minio → MinioUploadConfigDTO(endpoint, access_key, secret_key, bucket_name, secure)
# DB：agent 本地 postgres
DATABASE_URL = os.getenv("DATABASE_URL")  # postgresql+psycopg://...@agent-db:5432/fileagent
```

### 3.6 DB schema（agent 本地）

- 只建 `upload_files`（含新增的 `sha256`）。
- 建表方式：用 `jedi_file_upload.infra.models.upload_file.UploadFile.metadata.create_all(engine)`（agent 啟動時跑一次），或附一支簡單 SQL。**不需要** `system_configs`、不需要 RLS。

### 3.7 Dockerfile + docker-compose

**Dockerfile** 要點：
- base `python:3.11-slim`
- `apt-get install libreoffice`（headless 轉檔；可裝 `libreoffice-core` + 需要的 filter 以縮 image）
- 裝 poetry → `poetry install --no-root`（Nexus 源；或 `poetry export` 成 requirements 再裝，二擇一）
- CMD `python main_app.py`

**docker-compose.yml**：
```yaml
services:
  file-agent:
    build: .
    environment:
      STORAGE_TYPE: local            # 或 minio
      BASE_DIR: /data/upload
      DATABASE_URL: postgresql+psycopg://fileagent:***@agent-db:5432/fileagent
    volumes: [ "./data:/data/upload" ]
    ports: [ "8080:8000" ]
    depends_on: [ agent-db ]
  agent-db:
    image: postgres:16
    environment: { POSTGRES_DB: fileagent, POSTGRES_USER: fileagent, POSTGRES_PASSWORD: *** }
    volumes: [ "agent-db-data:/var/lib/postgresql/data" ]
volumes: { agent-db-data: {} }
```
（密碼一律走部署文件 / `.env`，不入版控。）

---

## §4 雲端 ↔ agent REST 合約（demo）

| 動作 | Request | Response |
|------|---------|----------|
| 上傳 | `POST /blob` multipart `file` | `200 {uid, save_file_name, size, mimetype, sha256}` |
| 下載 | `GET /blob/{uid}` | `200` binary stream（`Content-Type`, `Content-Disposition`）/ `404` |
| 預覽 | `GET /blob/{uid}/pdf` | `200` PDF stream（必要時即時轉、快取）/ `404` / `415` 不支援格式 |
| 刪除 | `DELETE /blob/{uid}` | `200` / `404` |
| 健康 | `GET /health` | `200` |

- ~~demo 無認證；正式化加 `Authorization`（per-tenant key）+ TLS。~~
  → **已完成（auth v2）**：資料面走 mTLS（nginx:8443）+ 短效 JWT（RS256）+ 設備指紋；
  正式一律啟用，見 `design-agent-auth.md` / `evidence-agent/deploy/README.md`。

---

## §5 資料流（對齊 design.md §5 / §5.4）

見 design.md 流程圖：上傳（雲端寫參照 + 算錨點 → agent 存 + 回 uid）、下載（雲端 relay + 重算驗錨點）、預覽（agent 轉 PDF + 快取）。

---

## §6 分階段任務

### Phase 0 — de-risk spike（R-1）✅ 已完成（2026-06-18）
- [x] Task 0.1：headless script `init_db(臨時 docker pg)` + `FileUploadService.upload_file`（local config，**無 Flask/JWT/Redis**）→ `@transaction` 完成、`upload_files` 寫入成功、`get_upload_file` 讀回。**PASS**。
- [x] Task 0.2：`get_upload_file` 裸跑 PASS；`convert_to_pdf`（LibreOffice）改於 Phase 1 docker image 內驗（非阻擋）。
- [x] Task 0.3：結論寫回 §0 R-1 → **Redis/JWT 皆可省，不需 fallback session 包裝**。
- **Gate：✅ 通過** → 可展開 Phase 1 逐行實作。

### Phase 1 — agent 服務骨架（進行中，2026-06-18）
- [x] scaffold `evidence-agent`：`core/app_factory` + `config/`（env→DTO）+ `di_containers/` + `api/blob/`（五支）+ 建表（`UploadFile.__table__.create`）+ `main_app.py` + Dockerfile + compose。pyproject 對齊 3.11 + Nexus，`poetry install` 成功。
- [x] **本機 smoke PASS**（agent 連 docker postgres，local 模式）：`/health` ok、`POST /blob` 回 uid + binary 落地 `BASE_DIR`、`GET /blob/<uid>` 內容一致、`DELETE` 後再下載回 jedi envelope `FILE_UPLOAD_404001`（證 `register_error_handlers` 與 flask-restful 並存）。
- [x] **docker compose up（含 LibreOffice）PASS**：用 **BuildKit secret** 帶 Nexus 帳密（gitignored `.secrets/nexus.env`，只在 build install 階段掛入、不進 image 層、不入版控）→ image build 成功、兩容器起來、health ok。
- [x] **預覽轉檔 PASS**：.rtf → `GET /blob/<uid>/pdf` → HTTP 200 / `application/pdf` / 檔頭 `%PDF-` → LibreOffice headless 在 image 內轉檔成功。
- [x] **MinIO 模式煙測 PASS**（連真實 MinIO `192.168.50.171:9002` bucket `guidant-ai-agent-01`）：上傳/下載 round-trip 一致、刪除 OK、刪後下載回 404 envelope；測試物件已清。
- [x] **修 bug（MinIO 煙測逼出）**：`DEBUG=false`（正式模式）下 flask-restful 把 jedi 非 HTTPException 例外吞成 500 → app_factory 加 `app.config["PROPAGATE_EXCEPTIONS"]=True`，讓 jedi error handler 接手回正確 envelope（404 等）。local smoke 因 `DEBUG=true` 沒踩到。

**Phase 1 ✅ 完成**：agent 本機 dev / docker compose(含 Nexus BuildKit secret) / 預覽轉檔 / local + minio 兩種後端 / 正式模式錯誤 envelope 全綠。

> **Nexus 憑證策略（已採用）**：BuildKit secret。`.secrets/` 與 `data/` 已加 evidence-agent `.gitignore`。客戶出貨：image 在我們這邊 build 成自包含後 `docker save` 出貨（客戶端碰不到 Nexus），客戶端 compose 改用 `image:` 不 build。
- 註：單檔上傳 DTO `checksum` 回 null（jedi DTO 映射細節）；sha256 於 Phase 3 補。

### Phase 2 — 雲端 RemoteAgentAdapter（核心完成，2026-06-18）
- [x] **新增（按層）**：`common/code/file_storage_type.py`（`REMOTE_AGENT` 常數，雲端專屬不污染套件）/ `infra/upload_file/remote_agent_config_dto.py`（`RemoteAgentConfigDTO`）/ `infra/upload_file/remote_agent_adapter.py`（`RemoteAgentAdapter`，mirror LocalAdapter：`IUploadFileProvider+BaseRepository`、走 `UploadFileRepoImpl` 寫雲端參照 row、httpx 轉發、不在 __init__ 開 session）。
- [x] **app 層**：`ManagedFileUploadService` 加 `_load_config` remote_agent 分支 + override `_build_adapter`（remote_agent 直接 instantiate，不走套件 factory）。
- [x] **route 層**：download / preview 加 `REMOTE_AGENT` 分支（只呼叫 service，不碰 DB）。
- [x] **error code**：`FILE_AGENT_500001/500002`（照 `<模組>_<HTTP><序號>`）。
- [x] **e2e 驗證（對真實 agent + cloud pg）**：save（cloud uid≠agent uid、save_file_name=agent uid、storage_type=remote_agent）/ get（內容一致）/ delete（cloud 參照 row 移除）/ 預覽（adapter→agent LibreOffice→`%PDF-`）+ service wiring（STORAGE_CONFIG→RemoteAgentAdapter）全通。**binary 從未落雲端**。
- [ ] **剩**：`upload_files` 加 `tenant_id`（參照表隔離；migration 待寫+套用，非路由必要）；full-BE-HTTP e2e（經真實 BE route，待 Phase 4 demo 用真 tenant STORAGE_CONFIG）。
- 註：httpx 已可用（transitive 0.28.1）；正式化前在 pyproject 顯式宣告。

### Phase 3 — 完整性驗證（核心完成，2026-06-18）
- [x] `RemoteAgentAdapter.save_file`：雲端自算 SHA-256 錨點（**不信 agent 回報值**），存 `upload_files.checksum`（重用既有欄，demo 免 migration；remote_agent 檔案的 checksum = SHA-256，local/minio 維持 MD5）。
- [x] `RemoteAgentAdapter.get_file`：下載對實際 bytes 重算 SHA-256，比對錨點；不符 → `audit(FILE_INTEGRITY_TAMPER_DETECTED=6080)` + `raise InternalServerError(FILE_AGENT_500003)`。錨點不存在（舊資料）則 fail-open 略過。
- [x] **竄改 e2e PASS**：上傳→正常下載過→直接改 agent 磁碟上的檔→再下載被擋（`FILE_AGENT_500003`）+ 記稽核。
- [x] 新增 `EventCode.FILE_INTEGRITY_TAMPER_DETECTED=6080`、`ErrorCode.FILE_AGENT_INTEGRITY_MISMATCH`。
- **⚠️ 套件相依（啟用條件）**：`upload_files.checksum` 原本 write-only —— `jedi-file-upload` 的 `UploadFileMapper.to_entity` 漏 map `checksum`。已補一行（套件 source `jedi_file_upload/infra/mapper/upload_file_mapper.py`），**免 migration**（欄位本就存在）。但 BE 目前 pin 0.0.16，此修正要**發版或 dev path-dep** 才在 BE 生效；未生效時驗證 fail-open（讀不到錨點 → 略過，不會誤擋）。e2e 是臨時同步 venv copy 驗證、已還原。
- 設計差異：design.md §5.4 原訂「加 `sha256` 欄」；demo 改重用 `checksum` 欄避 migration，正式版再立獨立 `sha256` 欄（與 tenant_id/RLS 同一 migration）。

### Phase 4 — 收尾
- [ ] 雙客戶 demo（A=local、B=minio）各一份 compose；STORAGE_CONFIG 各指自己 agent；驗 RLS 分流。
- [ ] changelog / design.md §11 sync（收尾時，等指令）。

---

## §7 測試策略

- **agent**：對 `/blob` 五支寫 API 測試（local 模式用 tmp dir；minio 用 testcontainers，套件已依賴 `testcontainers`）。
- **雲端**：`RemoteAgentAdapter` 對 mock agent（httpx mock / responses）測 save/get/convert + SHA-256 比對；測試檔加 logger patch autouse fixture（見專案慣例）。
- **整合**：docker-compose 起 agent，雲端跑 e2e 上傳→下載→預覽→竄改偵測。
- 測試專案 e2e 在 `compliance-manager-test`（不在主專案內加）。

---

## §8 Open / Deferred（demo 後）

- **agent DB 持久化/備份（重要）**：`upload_files`（uid→落點）是關鍵狀態，掉了 binary 全孤兒。compose 已改 **host bind-mount `./pgdata`**（survive `down -v`）；正式部署要補**備份策略**（整包備 agent 目錄 / pg_dump 排程）。安全網：`save_file_name` 內嵌 uid → DB 全毀可從檔名重建（災難復原）。
- 加密（傳輸落地）、cloud↔agent 認證 key、upload_files RLS、agent 健康檢查 / dangling reference 對帳、排程完整性稽核。
- D1 共用 `tenant_agents` registry：等第二種 agent 出現再抽（initial 沿用 STORAGE_CONFIG）。
