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:重用 UploadFileRepoImpljedi_file_upload/infra/repository/upload_file_repo_impl.py)。
  • HTTP client:用 httpx(串流上傳/下載)。

2.2 ManagedFileUploadServiceremote_agent 分支

Files: Modify app/upload_file/service/managed_file_upload_service.py:33-56_load_config)+ adapter 建構處

  • _load_configstorage_type == 'remote_agent' → 回新的 RemoteAgentConfigDTO(base_url=v['base_url'])
  • adapter 建構:remote_agent 時 build RemoteAgentAdapter不走 UploadProviderFactory,因該 factory 在套件內、不該認識雲端專屬 adapter)。在 service 層直接 instantiate。
  • per-tenant 已自動生效file_upload_serviceproviders.Factorydi_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

@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

{ "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 已列),裝套件本身即帶入大部分:

[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.mdnone 僅測試用。

3.5 config.py(env → config DTO)

# 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

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)

  • Gate:✅ 通過 → 可展開 Phase 1 逐行實作。

Phase 1 — agent 服務骨架(進行中,2026-06-18)

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)

  • 註:httpx 已可用(transitive 0.28.1);正式化前在 pyproject 顯式宣告。

Phase 3 — 完整性驗證(核心完成,2026-06-18)

  • ⚠️ 套件相依(啟用條件)upload_files.checksum 原本 write-only —— jedi-file-uploadUploadFileMapper.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 — 收尾


§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)。