# FR-059 檢測工具 Profile / Content 庫（掃描設定檔後台管理）— 設計文件

> 狀態：設計定稿（P1–P7 + D1–D10 共 17 項全數拍板）｜建立日期：2026-08-01｜FR-058 檢測工具擴充續作
> 討論稿（含流程圖）：[`discussion.html`](./discussion.html)
> **Notion 開卡：尚未進行**——等 FR-058.7 開卡完成後接續（避免 Case No 跳號）；屆時 CM-992 需關聯本 FR

## 變更紀錄

| 日期 | 變更 | 對應 |
|------|------|------|
| 2026-08-01 | 初版設計定稿：P1–P7（討論稿原樣）+ D1–D10 全數拍板。其中 D1 附 RLS 前例事實修正（討論稿「config schema 首例」說法有誤）；D4 為決策者修訂版（收 zip / tar / tar.gz / tgz 四格式 + agent 端統一解壓目錄交付，與討論稿原建議不同） | FR-059 母案 |
| 2026-08-01 | resolver 介面契約定案（058.7 協調者已下達 T-7.1：`(agent_uid, file_uid)` callable + `_FILE_ACCESS_RESOLVERS` 清單）；T-2.3 拒絕碼改為以端點實際落地為準 | D6 |
| 2026-08-01 | §5.2 capability 勘誤（T-1.2 實作 session 發現矛盾、決策者拍板）：寫入 capability 由「參照 compliance-framework 不對稱模式（is_platform=true）」改為**四個全 `is_platform=false` 比照 flow_template 平掛**——原引用適用於純系統資產，profile 庫是租戶可寫雙軌資源，套用會使租戶自助全滅、service guard 成死碼。CM-1013 卡同步更正 | §5.2 |

---

## 1. 案件基本資料

| 項目 | 內容 |
|------|------|
| FR 編號 | FR-059-2608-detection-profile-library |
| 標題 | 檢測工具 Profile / Content 庫（掃描設定檔後台管理） |
| 日期 / 狀態 | 2026-08-01｜設計定稿（決策 P1–P7 + D1–D10 全數拍板） |
| 關聯 | FR-058（母脈絡）／ FR-058.7（agent 取檔通道依賴，該案先行）／ Notion CM-992（範圍③「後台 content 管理機制」由本案承接）／ FR-039（agent 安全基建與不變式） |
| 討論稿 | [`discussion.html`](./discussion.html)（同資料夾） |
| Notion 開卡 | **尚未進行**——等 FR-058.7 開卡完成後接續，避免 Case No 跳號 |

---

## 2. 需求背景與端到端流程

### 2.1 現況：每加一支 profile 的四步人工維運鏈

掃描工具（第一期 CINC Auditor / InSpec 與 GCB）執行時需要 profile / content 檔作為掃描依據。以 GCB（TWGCB 政府組態基準）為例，目前一支新 profile 要走完這條鏈才會出現在使用者的下拉選單裡：

| 步驟 | 動作 | 誰做得到 |
|------|------|---------|
| 1 | profile 原始碼進 BE repo `content/detection-profiles/` 版控 | 開發者（要會 git + InSpec） |
| 2 | 人工 `rsync -a --delete` 到**每一台** agent 主機 `/opt/evidence-agent/deploy/content/` | 有主機 SSH 權限的工程師 |
| 3 | 寫一支 migration 改 `param_schema` 的 options（加一個容器內路徑選項） | 開發者（要懂 param_schema 版本紀律） |
| 4 | migration 依環境異動鐵律套 DEV → 決策者放行後 STG / POC | 開發者＋決策者 |

四步裡沒有任何一步是**一線工程師或客戶端管理員**做得到的。現況投放狀態也印證維運成本：8 支 TWGCB profile 只投放到 DEV agent（192.168.50.123），**STG / POC 的 agent 根本沒有這些檔案**——選了下拉選項也掃不動。

### 2.2 CM-992 記錄的兩個限制（立案理由）

- **限制①：封閉網路無法投放 profile**——封閉網路客戶的 agent 連不了外網，只能用「agent 本機路徑」型 profile；但目前沒有任何機制能把 profile 檔送進 agent container（content 掛載是人工 rsync，客戶做不來）。本案解法：profile 上傳到平台 → agent 透過既有 mTLS 通道拉檔＋cache，檔案遞送不再依賴外網或人工投放。
- **限制②：私有版本庫無憑證路徑**——CINC 原生支援 URL 取 profile，但 `params.profile` 是 `secret:false` 純文字，token 塞進 URL 會洩漏到 FE 執行紀錄與稽核 log，私有 repo 的 profile 無路可走。本案解法：私有 profile 直接打包上傳成租戶自有 profile，不再需要從私有 repo 拉；URL 憑證機制明列不做（P5），需求出現再立案。

### 2.3 目標狀態

agent 只升級一次（學會「拉檔＋cache」），之後 **content 的新增 / 改版 / 停用全部在後台完成**——不再重建 agent image、不再人工 rsync、不再為選項寫 migration。公用版由 root 租戶維護全租戶可見，各租戶也能上傳自己的 profile，模式與流程管理（flow_templates）的雙軌完全一致。

### 2.4 端到端流程（文字版）

```
管理者在「掃描設定檔管理」頁上傳 profile（zip/tar/tar.gz/tgz 或登記 URL）
  → BE 驗證（格式 / 大小 / 結構含 inspec.yml / 防 slip・bomb）→ 計算 sha256 → 存平台儲存 + 落 config.detection_tool_profiles
  → 任務抽屜 profile 下拉動態列出庫內項目（按 detection_tool_id 過濾）
  → 使用者選庫內 profile 發起執行（params.profile = "profile:<uid>"）
  → 派工 _collect_pending_tasks() 偵測前綴、展開 _profile 內部 payload（uid/version/sha256/...）
  → agent 心跳領工 → 查本機 cache /data/content/cache/<uid>/v<version>/
      命中 → 直接用；miss → GET /api/1.0/agents/files/<uid>（FR-058.7 通道）拉檔
      → 重算 sha256 對帳（不符即 fail）→ 安全解壓進 cache 目錄
  → 把解壓後「目錄」餵給 cinc-auditor（四格式一條路徑，D4）→ 掃描 → 報告走既有 blob 回收鏈
```

---

## 3. 決策定案（P1–P7 + D1–D10，共 17 項）

P1–P7 為前期已拍板事項（討論稿 §2 原樣）；D1–D10 於 2026-08-01 全數定案。

| # | 主題 | 定案內容 | 被排除方案與理由 |
|---|------|---------|-----------------|
| **P1** | 範圍 | 第一期只做**形態 A（agent 本機讀取型）**，涵蓋 `inspec`＋`gcb` 兩工具。資料模型帶 `detection_tool_id` 保持 tool-agnostic——未來擴充其他形態只加下發策略、不改表 | **OpenSCAP 排除**（content 在目標主機、客戶用 apt/dnf 自裝、有上游 SSG 版本節奏問題）；**ZAP 排除**（scan policy 放在客戶自己的 ZAP daemon 裡，平台碰不到） |
| **P2** | 取檔通道 | 復用 FR-058.7 建立的 `GET /api/1.0/agents/files/<uid>`（純 mTLS＋任務綁定驗證＋串流不落地，該案 D25），**不重建**；本案增量＝授權 resolver 多一種 `profile_ref` claim（見 D6） | 自建第二條通道——重工且違反 FR-058「不做抽象層」定案 |
| **P3** | 存量搬遷 | 既有 8 支 TWGCB profile 搬入庫成 **SYSTEM 公用版 seed**——新機制上線第一天下拉內容與今天完全一致（零回歸）；搬完退役「migration seed 選項＋人工 rsync」舊路（時程見 D5） | 不搬遷——上線即造成下拉回歸，且兩套維運方式並存無收斂點 |
| **P4** | 版本模型 | 照抄 `detection_tool_param_schemas`：`version`＋`is_current` 單現行版；版更＝**INSERT 新版＋舊版 `is_current=FALSE`**，不原地 UPDATE | 「任務綁舊版」的 group＋version 模型——任務永遠用現行版即可，歷史版本留表內供回溯，不引入綁版複雜度 |
| **P5** | 型態雙軌 | `source_type = file | url`：`file`＝上傳打包檔存平台儲存、agent 走通道拉＋cache；`url`＝只登記 URL 字串、agent 原樣交給 cinc-auditor、平台完全不經手檔案。**URL 的憑證 / 認證 / 權限一概不管**——打不打得通是客戶自己的網路與授權問題，平台不驗證、不代管憑證；此界線明寫，避免日後被當缺陷回報，user 有反應再立案 | URL 憑證代管——CM-992 限制②已由「私有 profile 改打包上傳」解掉，代管憑證的複雜度沒有對應需求 |
| **P6** | 手填保留 | 欄位型別維持 `select_or_text`——下拉改由 profile 庫動態餵，使用者仍可現場手打 URL 或容器路徑（不進庫、單次執行用）。取值來源三層並存：**庫內 file 型 / 庫內 url 型 / 現場手填**；既有任務存的手填值升級後照常可用（相容性零破壞） | 改純 select——破壞既有手填相容，且封殺臨時性 ad-hoc 掃描的彈性 |
| **P7** | 工具隔離 | CINC 與 GCB 的 profile 池**分開**，按 `detection_tool_id` 綁定與過濾：上傳時必選所屬工具；GCB 任務抽屜只看得到 gcb 的 profile，CINC 同理。同一份檔兩工具都要用就各登記一筆，提供「複製到另一工具」動作 | 跨工具共用池——會造成「GCB 池混進通用 baseline」的語意污染 |
| **D1** | 表落點與命名 | **`config.detection_tool_profiles`**——語意上 profile 庫是 detection_tools 家族的延伸，放 `config` 最自然；RLS policy 是表級設定與 schema 無關。**事實修正**：討論稿稱「本表帶 RLS 是 config schema 首例」**有誤**——`config.tenant_detection_tool_configs` 本來就有 RLS（policy `tdtc_tenant_isolation`，見 `scripts/sql/2026-07-26-fr056-1-detection-tools-config-schema.sql:65`），config schema 早有前例，此顧慮不成立 | `compliance` schema——跨 schema 拆家族反而讓 detection_tools 的讀者找不到；「RLS 慣例一致」的動機在前例修正後也不存在 |
| **D2** | 庫參照下發格式 | **`profile:<uid>` 顯式前綴字串**存在 `params.profile`，派工時展開。與手填值零歧義（手填不可能以 `profile:` 開頭）；單欄位不動 FE 主參數顯示邏輯（`_PRIMARY_PARAM_KEYS`）；派工展開時前綴判斷一行搞定 | **UUID 裸值格式判別**——手填值理論上可以長得像 UUID（低機率但非零），靠格式猜測是隱性契約；**雙欄位（另開 `profile_ref`）**——要處理互斥驗證與 FE 顯示分流，成本高於前綴 |
| **D3** | FE 管理頁入口 | **系統管理下獨立新頁「掃描設定檔管理」**——tenant 管理員與 root 用同一頁，靠 `scope` 區分能力（公版對非 root 唯讀）。雙軌管理語意與流程管理一致（流程管理也是獨立頁） | 工具管理頁（tool-plugin-manage）內嵌 tab——工具管理頁是純平台目錄頁（root 專用、受眾不同），內嵌 tab 會把租戶級功能塞進平台級頁面，權限模型混濁 |
| **D4** | 上傳格式與大小（**決策者修訂**） | 收 **zip / tar / tar.gz / tgz 四種常用打包格式**、大小上限 **50MB**。設計含意兩點：① BE 驗證層用 `tarfile`＋`zipfile` 雙支援做結構檢查（找 `inspec.yml`）與安全防護——path traversal / bomb **兩種格式都要防**，streaming 檢查不落地解壓；② **agent 端交付統一化**：不按格式分支餵 cinc-auditor，agent 拉檔後一律安全解壓到 cache 目錄、把**目錄**餵給 cinc-auditor（CINC 原生吃目錄），四種格式一條路徑、行為一致好除錯。cache 目錄結構 `/data/content/cache/<uid>/v<version>/`（存解壓後內容） | 討論稿原建議「僅收 .tar.gz / .tgz」——決策者裁示擴大到常用打包格式、降低使用門檻；「按格式分支處理（tarball 直接餵、zip 才解壓）」——多格式多路徑行為不一致、難除錯 |
| **D5** | 舊路退役時程 | **過渡一版**——新機制上線驗收通過後，**下一版**拿掉 agent 的 `/data/content` bind mount 與 param_schema seed 的靜態 options。過渡期兩路並存（舊容器路徑選項照常可用），確認庫路徑穩定再收。**手填容器路徑能力因 P6 永遠保留**——舊路徑字串日後仍可手填使用，只是平台不再維護該目錄內容 | 立即退役——庫路徑未經線上驗證即斷舊路，風險不對稱 |
| **D6** | 058.7 通道增量（跨案協調項） | **通道端點共用、授權 resolver 按檔案用途分流**——`source_file`（058.7：一次性輸入檔）與 `profile_ref`（本案：任務 `params` 中引用的 profile 檔，cache miss 拉檔也發生在任務執行中）兩種 claim 各自驗證；**本案負責 `profile_ref` resolver 的實作**。🔴 **跨案依賴提醒：058.7 實作時須把授權驗證做成可插拔的 resolver 介面**（而非硬編 source_file 查詢），本案接入成本才最低——**此協調應在 058.7 開工前談定，寫進其 T-7.1 規格** | 另開專用端點——通道重複建設，違反 P2 |
| **D7** | URL 型公版搬遷 | dev-sec **linux-baseline / windows-baseline 兩條公開 tarball URL 搬成 SYSTEM url 型公版**，與 P3 的 TWGCB 搬遷同批 seed——上線第一天 CINC 下拉也零回歸，順帶成為 url 型的首批示範資料（驗證 url 型下發路徑） | 不搬——CINC 下拉回歸，且 url 型上線時無示範資料可驗證 |
| **D8** | 上傳驗證深度 | **只驗「結構＋檔案安全」**——壓縮檔可開、頂層或一層內有 `inspec.yml`、大小 / entry 數 / 解壓比防 bomb、entry 路徑防 slip（streaming 檢查不落地解壓）。**profile 正確性由掃描執行結果反映**（壞 profile 掃了會 fail，錯誤訊息可回溯）——此界線在文件明寫，不做「上傳即保證能掃」的承諾。BE 裝 CINC 做 pre-check 列未來選項 | 上傳即跑 `cinc-auditor check`——BE 端沒裝 CINC，第一期不引入該執行環境 |
| **D9** | agent cache 管理 | 第一期**無上限＋手動清理指引**——單支 profile 幾百 KB～幾 MB，量級離磁碟壓力很遠；cache key 含 `version`，**版本更新自然失效**（舊版目錄不再被引用）；agent 重啟不清（磁碟 volume 保留，避免每次重啟全量重拉）。LRU 上限（如 2GB）列 follow-up | 第一期即做 LRU 上限——量級尚遠，過早最佳化 |
| **D10** | 三環境節奏（提醒項） | 環境異動鐵律的提醒，非新決策——**DEV 先行驗收，STG / POC 一律等決策者明示放行**才套 migration 與 seed；搬遷 seed（P3 / D7）與 param_schema 升版都適用。附帶效益：STG / POC 的 agent 目前沒有 profile 檔（§4），新機制上線後它們**第一次**有了可用的 GCB 掃描能力——放行時程可跟這個效益一起評估 | （無替代方案——鐵律） |

---

## 4. 現況接入點盤點

相關 repo 皆已實查（附檔案座標）。結論：**雙軌資料模型、檔案儲存、agent 安全通道全有現成拍板模式可抄**；真正的新造點集中在「profile 庫本體」「派工展開」「agent cache」「上傳安全驗證」四處。

| 元件 | 現況 | 本案動作 |
|------|------|---------|
| **profile 實體** | BE repo `content/detection-profiles/`（8 支 TWGCB＋gcb-demo-win）；產生器 `content/detection-profiles/tools/twgcb2inspec.py`（讀 NICS 官方 .docx 產出 InSpec profile）。`content/detection-profiles/README.md` 有完整說明，含「為什麼不放 agent repo」：agent Dockerfile 最後一行 `COPY . .`，放進去就違反 FR-058 D7「content 不可打包進 agent image」 | 8 支打包上傳成 SYSTEM seed（§5.6）；此目錄過渡一版後不再是投放通道（D5），保留作 profile 原始碼版控與產生器工作區 |
| **投放** | 人工 `rsync -a --delete` 到 agent 主機 `/opt/evidence-agent/deploy/content/`，靠 `evidence-agent/deploy/docker-compose.yml:82` 的唯讀 bind mount `- ./content:/data/content:ro` 讓容器看到。**只投放 DEV agent（192.168.50.123），STG / POC 未投放** | 由 agent 拉檔＋cache 取代（§5.4）；bind mount 過渡一版後拿掉（D5） |
| **下拉選項** | `config.detection_tool_param_schemas` 的 `param_schema.profile` 欄位 type=`select_or_text`。GCB 的 8 個選項是容器內路徑（`/data/content/<dir>`），由 migration `scripts/sql/2026-07-31-fr058-18-twgcb-os-profiles-8-options.sql:83-91` 硬 seed；CINC 的 2 個選項是公開 tarball URL（dev-sec linux / windows-baseline）。**options 是建議值非白名單**（手填任何字串都會被原樣下發） | 加 `options_source: "profile_library"` 宣告＋param_schema 升版拿掉靜態 options（§5.5） |
| **執行** | agent connector `evidence-agent/core/task_executor_connectors/inspec.py` 把 `params.profile` **原樣塞進 `cinc-auditor exec` argv**。CINC 原生支援三種 content 來源：網址 / agent 本機路徑 / Supermarket 名稱 | 接 `_profile` 展開：file 型改餵 cache 解壓目錄（§5.4、D4）；url 型 / 手填現行為不變 |
| **架構前提** | CINC Auditor 裝在 **agent 本機**、由 agent 連出去掃目標（SSH / WinRM），profile 由 agent 本機讀取後交給 cinc-auditor，**不送到目標主機**——與 OpenSCAP（引擎與 content 都在目標主機）方向相反（FR-058 design.md:363-370） | 約束：content 遞送設計不可用 OpenSCAP 心智模型 |
| **派工鏈路** | 使用者按執行 → `detection_orchestration_service.start_execution()` 建 `agent_tasks`(pending) → agent 心跳 `POST /api/1.0/agents/heartbeat`（mTLS）回應夾 `pending_tasks`（每筆 {uid, detection_tool_id, detection_tool_code, params, credentials}）→ agent ack → connector run → 報告走 blob 上傳回收。心跳間隔預設 300 秒 | 可複用：在 `_collect_pending_tasks()`（`app/remote_agent/service/agent_enrollment_service.py:241-249`）組 payload 階段加庫參照展開（§5.4） |
| **agent 面控制端點** | `api/remote_agent/__init__.py:34-38` 僅四支：register / heartbeat / tasks/&lt;uid&gt;/ack / tasks/&lt;uid&gt;/result，純 mTLS 不掛 jwt。**agent 從 BE 拉檔的通道目前不存在**——由 FR-058.7 D25 新建 `GET /api/1.0/agents/files/<uid>`（純 mTLS、任務綁定驗證、串流回傳 BE 不落地；agent 端加 `_get_stream()`） | 復用（P2）；本案新增 `profile_ref` 授權 resolver（D6、§5.4） |
| **既有安全基建** | `infra/upload_file/remote_agent_adapter.py` 一整套：mTLS context（`common/util/agent_auth/tls.py:build_cloud_mtls_context`）、短效綁定 JWT（`common/util/agent_auth/jwt_util.mint`）、回應指紋比對、**SHA-256 雙邊對帳**（雲端自算為信任根、下載重算比對、不符 → audit `FILE_INTEGRITY_TAMPER_DETECTED`） | 直接沿用（sha256 對帳模式照搬到 agent 拉 profile 檔路徑） |
| **FR-039 不變式** | `docs/features/FR-039-2606-distributed-file-agent/design.md:284-291`：雲端不落地 binary（串流）、agent 不連雲端 DB、完整性信任根＝雲端 sha256 | 約束：本案所有設計不得破壞（參照由心跳 payload 下發、agent 拉檔後重算對帳） |
| **公用 / 租戶雙軌模式** | `docs/analysis/2026-06-29-tenant-scope-data-model.md` Category C 已定案：`scope`（'SYSTEM'\|'TENANT'）＋`tenant_id NOT NULL`（SYSTEM 掛 ROOT(1)；**tenant_id IS NULL 永遠是壞資料**）；Model 繼承 `BaseModel, TenantScopedMixinModel`；RLS 四段 policy 範本 `scripts/sql/2026-06-29-fr042-tenant-scope-module-frames-flow-templates.sql:41-72`（module_frames 版）；程式層三層防禦抄 flow_templates（route capability 參照 `2026-06-30-compliance-framework-platform-capability.sql` 的「read is_platform=false、create/update/delete=true」不對稱模式；service 層 guard 抄 `app/flow_engine/service/flow_template_app_service.py:187-197` `_guard_builtin_writable`）；root 判定 canonical＝`common/authz/platform.py` `viewer_is_platform_admin()`；唯一索引形狀抄 `2026-05-12-flow-template-management-schema.sql:70-73` | 照抄不重議（§5.1、§5.2）。**RLS 前例事實（D1 修正）**：config schema 已有 RLS 前例（`config.tenant_detection_tool_configs` 的 `tdtc_tenant_isolation` policy），本表帶 RLS 並非首例 |
| **檔案儲存** | canonical 上傳 service `app/upload_file/service/managed_file_upload_service.py`——TENANT scope 用 `upload_files_for_tenant()`（:281）、SYSTEM 公版用 `upload_system_file()`（:298，storage_scope=system 存 root tenant 儲存）。後端三種：local / minio / remote_agent（無 S3）。metadata 表 `public.upload_files` 單表（uid / file_name / save_file_name / size / sha256 / storage_type / storage_scope，**無 tenant_id 欄**——隔離靠存進哪個租戶的後端）；業務表存 `upload_files.id` soft-ref（範本：`infra/flow_engine/models/job_evidence.py:41`） | 直接複用，不另造儲存抽象 |
| **使用者面下載** | 既有 `GET /file/download/<uid>`＋signed token（TTL 120 秒）；**agent 拿不到 signed token**（換 token 端點掛 `@jwt_required`） | 不適用 agent——agent 必須走 P2 的專用通道 |
| **上傳驗證現況** | 各 service 手寫「副檔名＋seek 大小」三步（如 `app/grc/service/ap_docx_import_app_service.py:74-79`），**無 magic number 檢查、無 zip-slip / bomb 防護（全 repo 零處理）** | **本案新造點**：四格式結構驗證（內含 `inspec.yml`）與解壓安全防護（§5.3） |

---

## 5. 詳細設計

### 5.1 資料模型

新表 **`config.detection_tool_profiles`**（D1），Category C 雙軌形狀＋P4 版本模型：

| 欄位 | 型別 / 約束 | 說明 |
|------|------------|------|
| `id` / `uid` | BIGSERIAL / UUID | 標準主鍵＋對外識別 |
| `scope` | VARCHAR(20) NOT NULL DEFAULT 'TENANT' | 'SYSTEM'（公用版，root 維護）\| 'TENANT'（租戶自有）——Category C 拍板形狀 |
| `tenant_id` / `org_unit_id` | BIGINT NOT NULL / BIGINT | `TenantScopedMixinModel` 自動填；SYSTEM 列掛 ROOT(1)。**tenant_id IS NULL 永遠是壞資料** |
| `detection_tool_id` | BIGINT NOT NULL | soft-ref `config.detection_tools`；inspec 與 gcb 各自綁定（P7） |
| `name` / `description` | VARCHAR / TEXT | 顯示名稱與說明 |
| `platform_hint` | VARCHAR NULL | 適用平台標記（如 windows / linux），純顯示用不參與過濾邏輯 |
| `source_type` | VARCHAR(10) NOT NULL | 'file' \| 'url'（P5 雙軌） |
| `file_id` | BIGINT NULL | soft-ref `upload_files.id`；`source_type=file` 時必填 |
| `url` | TEXT NULL | `source_type=url` 時必填；平台不驗證可達性（P5 界線） |
| `sha256` | VARCHAR(64) NULL | file 型上傳時計算（對原始壓縮檔 bytes），為 agent 對帳的信任根（FR-039 不變式） |
| `version` / `is_current` | INTEGER NOT NULL DEFAULT 1 / BOOLEAN NOT NULL DEFAULT TRUE | P4 版本模型：版更＝INSERT 新版＋舊版 `is_current=FALSE` |
| `is_active` | BOOLEAN NOT NULL DEFAULT TRUE | 軟刪（停用後下拉不出現，歷史執行紀錄參照仍可回溯） |
| 稽核欄位 | created_user / updated_user / 時間戳 | API 回傳時依專案規範 enrich `*_user_name`（nickname，見 §5.2） |

**CHECK 約束**（source_type 與來源欄位互斥成對）：

- `source_type='file'` → `file_id IS NOT NULL AND url IS NULL`
- `source_type='url'` → `url IS NOT NULL AND file_id IS NULL`

**唯一索引三組**（形狀抄 `2026-05-12-flow-template-management-schema.sql:70-73`）：

1. `(detection_tool_id, tenant_id, name, version)` unique——租戶內同工具同名同版唯一
2. 公版名稱全域唯一 partial index（`WHERE scope='SYSTEM' AND is_active`）
3. 同名 `is_current=TRUE` 僅一筆的 partial unique——版本模型完整性

**RLS 四段 policy**：直接抄 `scripts/sql/2026-06-29-fr042-tenant-scope-module-frames-flow-templates.sql:41-72`（module_frames 版）——SELECT 的 `scope='SYSTEM'` 分支獨立放最前（公版人人可讀）＋super_admin＋path 子樹；INSERT WITH CHECK 堵 SYSTEM 偽造；UPDATE 要 USING＋WITH CHECK 兩段（防租戶把自己的列升級成公版）；DELETE 非 super 不能刪 SYSTEM。helper `app_tenant_allowed_for_session()` 已存在直接呼叫。

**Migration 鐵則**：新表必加 `GRANT ... TO cm_app`＋sequence 權限；語句加日期註解＋檔頭 `-- Date:`；收尾 `INSERT public.schema_migrations`；套用節奏依 D10（DEV 先行）。

**ORM Model**：繼承 `BaseModel, TenantScopedMixinModel`（jedi_common before_flush listener 自動填 tenant_id；SYSTEM 列由 root session 建立自然掛 ROOT）；Repo 繼承 `BaseRepositoryImpl`（session lazy，禁止 `__init__` 內 `get_session()`）。

### 5.2 API 設計

管理頁六個動作（實際 route 命名與 schema 於 plan 階段依 `docs/claude/api-patterns.md` 定稿）：

| 動作 | 方法（草案） | 行為 |
|------|-------------|------|
| 列表 | POST `/detection-tool-profiles/list`（分頁，`RequestMetaSchema`＋`EnvelopeSchema`） | 公版＋自有合併查詢 `or_(tenant_id==ctx.tenant_id, scope=='SYSTEM')`，公版排前面；欄位含 scope / 工具 / source_type / 版本 / 狀態；DTO 回 `scope` 讓 FE 決定編輯鈕（公版對非 root 唯讀） |
| 下拉 menu | GET `/detection-tool-profiles/menu?detection_tool_id=`（`List[MenuDto]` 形狀，不包 PageDataDto） | 任務抽屜下拉用：只回 `is_current=TRUE AND is_active=TRUE`，帶 `detection_tool_id` 過濾（P7）；回 uid / name / source_type / platform_hint |
| 上傳 / 登記 | POST（multipart 或 JSON） | 選工具（P7 必選）→ 選 source_type → file 走 §5.3 驗證管線後存儲（SYSTEM 公版走 `upload_system_file()`、TENANT 走 `upload_files_for_tenant()`）；url 只登記字串（P5） |
| 版更 | POST `/<uid>/new-version` | 同名 INSERT 新 `version`＋舊版 `is_current=FALSE`（P4 紀律）；file 型換新檔重算 sha256 |
| fork ＋ 複製到另一工具 | POST `/<uid>/fork`／POST `/<uid>/copy-to-tool` | fork＝公版複製成租戶自有可編輯副本（硬寫 `scope='TENANT', tenant_id=ctx.tenant_id`）；複製到另一工具＝同內容在另一 `detection_tool_id` 池各登記一筆（P7） |
| 停用 | PUT `/<uid>/deactivate` | 軟刪 `is_active=FALSE`，下拉即不出現；歷史執行紀錄參照可回溯 |

**三層防禦守門**（抄 flow_templates 模式，§4 盤點）：

1. **Route capability**：讀寫**四個 capability 全部 `is_platform=false`**（比照 flow_template 平掛）——租戶管理員可建 / 改 / 停用自己的 TENANT profile 與 fork 公版，這正是雙軌功能的核心；「公版只有 root 能動」交給第 2 層 service guard 與第 3 層 RLS，不在 capability 層鎖（實際 capability 命名與角色矩陣掛法 plan 階段定；守門一律走 `common/authz/`，禁止另立 helper）。**勘誤（2026-08-01，實作 session 發現）**：初稿此處誤引 `2026-06-30-compliance-framework-platform-capability.sql` 的「寫入 is_platform=true」不對稱模式——那適用於「整個資源寫入天生 root-only」的純系統資產（框架目錄）；profile 庫是租戶可寫的雙軌資源，套該模式會讓租戶自助全滅、service guard 淪為死碼。
2. **Service 層 guard**：抄 `app/flow_template_app_service.py:187-197` `_guard_builtin_writable` 模式，改判 `scope=='SYSTEM' and not viewer_is_platform_admin()` → `ForbiddenError`（root 判定 canonical＝`common/authz/platform.py` `viewer_is_platform_admin()`，不硬編 id）。
3. **DB RLS 兜底**（§5.1 四段 policy）。

**審計欄位 enrich**：response 有 `created_user` / `updated_user`（login_name）時必同時回 `created_user_name` / `updated_user_name`（nickname）——在 app service 層批次查 `User.login_name → User.nickname`，不在 infra 層 JOIN（pattern 見 `app/oscal/service/ssp_docx_import_app_service.py:_enrich_party_user_org_names()`）。

**Error code**：遵循 CLAUDE.md「`<模組前綴>_<HTTP狀態碼><序號>`」命名規則，落在 detection_tools 家族檔 `common/code/detection_tools_error_code.py`（前綴 `DETECTION_TOOLS_`，序號接續既有）。需新增（草列，plan 階段定稿序號）：

| 情境 | HTTP | 例外類別 |
|------|------|---------|
| profile 不存在 | 404 | `NotFound` |
| 同工具同名已存在 | 409 | `ConflictError` |
| 上傳格式不在四格式白名單（副檔名 / magic number 不符） | 400 | `BadRequestError` |
| 超過 50MB 大小上限 | 400 | `BadRequestError` |
| 壓縮檔不安全（path traversal / bomb / entry 數超限） | 400 | `BadRequestError` |
| 結構驗證失敗（找不到 `inspec.yml`） | 400 | `BadRequestError` |
| `source_type` 與 file / url 欄位互斥違反 | 400 | `BadRequestError` |
| 非 root 對 SYSTEM 公版寫入 | 403 | `ForbiddenError` |

**DDD 分層**：route 不碰 session；app service public method 一律 `@transaction`；DB 操作走 domain service → repository interface → infra 實作；DI 於 `di_containers/` 對應 container wiring。

### 5.3 上傳驗證管線（本案新造點）

現況全 repo 無壓縮檔安全處理先例（§4），本管線為第一個實作，設計如下：

**格式偵測**：副檔名白名單（`.zip` / `.tar` / `.tar.gz` / `.tgz`）＋ magic number 雙重確認（zip=`PK\x03\x04`、gzip=`\x1f\x8b`、tar=offset 257 的 `ustar`），不符即拒收。

**檢查順序**（streaming，全程不落地解壓；D4 / D8）：

1. **大小上限**——Content-Length 預檢＋實讀累計雙保險，超過 50MB 即斷。
2. **開檔**——依格式用 `tarfile` 或 `zipfile` 開啟（兩者皆標準庫；zip 需 seekable 來源，以記憶體 / spooled buffer 承接）。
3. **逐 entry 安全檢查**：
   - **防 path traversal（slip）**：entry 路徑正規化後不得為絕對路徑、不得含 `..` 逸出；tar 的 symlink / hardlink entry 一律拒收。
   - **防 bomb**：entry 數上限＋宣告解壓總量上限＋壓縮比上限（實際數值 plan 階段定，量級參考：現有 TWGCB 單支數百 KB）。
4. **結構驗證**——頂層或一層目錄內須有 `inspec.yml`（D8：只驗結構，不驗 profile 語意正確性；正確性由掃描執行結果反映，界線明寫）。
5. **sha256 計算**——對原始壓縮檔 bytes 計算，落 `detection_tool_profiles.sha256` 作 agent 對帳信任根。
6. **存儲**——SYSTEM 公版走 `upload_system_file()`、TENANT 走 `upload_files_for_tenant()`（§4 canonical service，不另造）。

**兩種格式共防**：tar 系與 zip 的 slip / bomb 檢查邏輯共用同一套 entry 檢查器（僅迭代介面不同），避免 zip 路徑漏防。

### 5.4 下發機制（核心）

派工時 `_collect_pending_tasks()`（`app/remote_agent/service/agent_enrollment_service.py:241-249`）組 payload 階段，偵測 `params.profile` 的值並分流：

| params.profile 值 | BE 派工行為 | agent 執行行為 |
|-------------------|------------|---------------|
| **庫內參照・file 型**（`profile:<uid>` 前綴，D2） | 展開成 `_profile` 內部 key：`{uid, version, sha256, source_type, file_name}`（`_` 前綴不落 FE 紀錄，沿用 `_credentials` 慣例） | 查本機 cache `/data/content/cache/<uid>/v<version>/` → 命中直接用；miss 則打 `GET /api/1.0/agents/files/<uid>`（P2 通道）拉壓縮檔 → **重算 sha256 對帳（不符即 fail，FR-039 不變式）** → **安全解壓進 cache 目錄** → 把**解壓後目錄**餵給 cinc-auditor（D4：四格式統一走此一條路徑） |
| **庫內參照・url 型** | 展開成 `_profile`：`{uid, source_type, url}` | 把 url 原樣交給 cinc-auditor（現行為；打不通是客戶網路問題，P5 界線） |
| **手填值**（不以 `profile:` 開頭） | 原樣下發，不展開 | 原樣塞進 `cinc-auditor exec` argv（現行為，P6 相容） |

**agent cache 細節**（D4 / D9）：

- 目錄結構 `/data/content/cache/<uid>/v<version>/`，存**解壓後內容**；解壓完成後寫入完成標記（含來源壓縮檔 sha256），命中判斷＝目錄存在＋標記 sha256 與 `_profile.sha256` 一致。
- agent 端解壓同樣做 slip / bomb 防護（BE 驗過不代表 agent 可信任傳輸後內容——對帳通過後解壓仍走安全解壓器）。
- cache key 含 version → 版本更新自然失效（舊版目錄不再被引用）；agent 重啟不清；第一期無上限＋手動清理指引，LRU 列 follow-up（D9）。

**取檔授權（`profile_ref` resolver，D6）**：復用 058.7 端點、授權 resolver 分流——`profile_ref` claim 的驗證語意＝「該 uid 是該 agent 名下 pending / running 任務 `params` 中引用的 profile 檔」（cache miss 拉檔發生在任務執行中，任務綁定天然成立）。本案實作此 resolver；**前置條件＝058.7 把授權驗證做成可插拔 resolver 介面（須在該案開工前談定、寫進其 T-7.1 規格）**。

具體介面契約（兩案凍結契約）：resolver 是一個 callable，收 `(agent_uid, file_uid)`，回傳「認不認這個 uid」（bool 或含理由的結果物件）；端點授權段逐一詢問模組層 resolver 清單（058.7 落地 `_FILE_ACCESS_RESOLVERS = [source_file_resolver]`），任一認可即放行、全部不認才拒絕。本案 T-2.3 的動作＝實作 `profile_ref_resolver` 並 append 進該清單，不動端點本體。**此契約 2026-08-01 已由 058.7 協調者下達其 T-7.1 執行者，D6 前置條件已滿足**（058.7 側並會留註解標明「FR-059 將 append profile_ref resolver，resolver 介面即兩案契約，不要在重構中收掉」）。

### 5.5 下拉動態化

`param_schema` 的 profile 欄位加宣告 `"options_source": "profile_library"`（type 維持 `select_or_text`，P6）——FE 看到此宣告，改呼叫 profile menu API（帶 `detection_tool_id` 過濾，P7）動態組 options，不再讀死的 options 陣列。

- **機制 tool-agnostic**：未來任何工具任何欄位要接庫，seed 宣告即可，FE 零改（§5.7 checklist 的基礎）。
- **param_schema 升版動作**：inspec 與 gcb 各 INSERT 新 version（舊版 `is_current=FALSE`，照該表既有版本紀律），新版 profile 欄位帶 `options_source` 宣告並**拿掉靜態 options**——8 支 TWGCB 容器路徑選項與 dev-sec 2 條 URL 選項由搬遷 seed（§5.6）轉為庫資料承接。升版 migration 套用節奏依 D10。

### 5.6 搬遷 seed（P3 / D7）

| 批次 | 內容 | 做法 |
|------|------|------|
| **8 支 TWGCB（file 型）** | `content/detection-profiles/` 下 8 支 TWGCB profile 目錄 | **各自打包成 tar.gz、走與使用者相同的上傳管線**（§5.3 驗證＋sha256＋`upload_system_file()`）落成 SYSTEM 公用版——不繞道直插 DB，確保 seed 資料與使用者上傳資料形狀完全一致（有 upload_files 實體與 sha256）。實作形式＝python seed script（呼叫 app service），SQL migration 只負責表結構 |
| **dev-sec 2 條（url 型）** | linux-baseline / windows-baseline 公開 tarball URL | 登記成 SYSTEM url 型公版（純資料列，無檔案實體），與 TWGCB 同批執行；兼作 url 型首批示範資料（D7） |

- 顯示名稱沿用現行 param_schema options 的 label——確保下拉零回歸（P3 驗收條件）。
- 所屬工具：TWGCB 8 支綁 `gcb`、dev-sec 2 條綁 `inspec`（P7 分池）。
- 執行節奏：DEV 先行驗收，STG / POC 等決策者明示放行（D10）。

### 5.7 新工具接入 checklist（後續接工具照表操課，不用考古）

#### ⑴ 同形態新工具（形態 A：content 在 agent 本機讀取）＝零開發接入，三步驟

前提：該工具的 connector 已存在、且其掃描引擎吃「agent 本機 content」（同 CINC / GCB 形態）。

| 步驟 | 動作 | 動到什麼 |
|------|------|---------|
| 1 | registry 加工具：`config.detection_tools` seed 一筆（照 FR-056 / FR-058 既有 seed 慣例） | 一支 SQL migration |
| 2 | param_schema 欄位宣告：該工具的 profile 類欄位 type=`select_or_text`＋`"options_source": "profile_library"` | 同一支 migration 的 param_schema seed |
| 3 | 後台上傳 profile（上傳時選該工具）→ 任務抽屜下拉即可用 | 純後台操作 |

**BE / FE / agent 程式碼零改**——profile 表帶 `detection_tool_id` 天然分池（P7）、FE 靠 `options_source` 宣告驅動（§5.5）、agent 的 `_profile` 處理與 cache 是 tool-agnostic 通用路徑（§5.4）。

#### ⑵ 異形態工具（如 OpenSCAP content 在目標主機、ZAP policy 在客戶 server）

- **profile 庫本體全形態共用**：儲存（file / url 雙軌）、版本模型、雙軌 scope、管理頁與全部管理 API **一概不動、直接沿用**；**表結構不改**（P1 tool-agnostic 設計的意義所在）。
- **只需為該形態新增「下發策略」段**——即派工展開 `_profile` 之後「檔案最後一哩怎麼到位」的策略。例：OpenSCAP 型＝agent 拉檔後經 SSH 推送至目標主機再執行；ZAP 型＝透過客戶 ZAP daemon 的 API 匯入 policy。接入時要寫的東西＝該形態的下發策略設計（agent 端行為）＋connector 對接；**不要動 profile 庫表與管理 API**。
- 接入時機的判斷素材：該工具的 content 由誰讀（agent / 目標主機 / 第三方 daemon）——是形態 A 就走 ⑴，不是就寫下發策略走 ⑵。

---

## 6. 拆分（4 子需求 × 12 子任務）

依賴鏈：**FR-059.1（BE 地基）→ FR-059.2（下發整合）∥ FR-059.4（FE，管理頁部分）→ FR-059.3（Agent 端，硬依賴 058.7 通道）**。

**並行契約凍結項**（FR-058 arc 既有教訓：並行前先凍結契約）：

1. 庫參照格式 `profile:<uid>`（D2）
2. `_profile` payload 形狀（file 型 `{uid, version, sha256, source_type, file_name}`；url 型 `{uid, source_type, url}`）
3. cache 目錄結構 `/data/content/cache/<uid>/v<version>/`（存解壓後內容＋完成標記）
4. 取檔端點的 `profile_ref` 授權 resolver 介面（D6，**已於 2026-08-01 談定**：callable 收 `(agent_uid, file_uid)` 回傳認可與否，端點逐一詢問 `_FILE_ACCESS_RESOLVERS` 清單、任一認可放行；詳見 §5.4）

### FR-059.1 BE 地基 — 依賴：無（決策已全數定案）

| # | 子任務 | 驗收 | 依賴 | Repo |
|---|--------|------|------|------|
| T-1.1 | migration＋model：`config.detection_tool_profiles` 表（Category C 欄位＋CHECK 約束＋三組唯一索引＋RLS 四段 policy＋GRANT/sequence 鐵則）；ORM model（`BaseModel, TenantScopedMixinModel`）＋repo（`BaseRepositoryImpl`） | DEV 可套；RLS 手測三件套通過：租戶 A 看不到租戶 B 的 TENANT 列、SYSTEM 列人人可讀、非 root 寫 SYSTEM 被 RLS 擋 | — | BE |
| T-1.2 | 管理 API 六動作：列表（分頁合併查詢公版前置）/ menu / 上傳登記（file・url 雙軌）/ 版更 / fork＋複製到另一工具 / 停用；三層防禦守門；審計欄位 enrich；error code 新增 | Swagger 全端點可操作；非 root 對公版寫入回 403（service guard）；DTO 回 scope；版更後舊版 `is_current=FALSE` 且僅一筆 current；menu 只回 current＋active 並按工具過濾 | T-1.1 | BE |
| T-1.3 | 上傳驗證管線：四格式（zip / tar / tar.gz / tgz）偵測（副檔名＋magic number）、`tarfile`＋`zipfile` streaming 安全檢查（slip / bomb / entry 數 / 解壓比）、`inspec.yml` 結構驗證、50MB 上限、sha256 計算 | 惡意樣本測試全數被擋且錯誤訊息可辨識（traversal 樣本 / bomb 樣本 / 無 inspec.yml / 超大小 / 假副檔名）；四格式正常樣本全部通過；不落地解壓（過程無暫存解壓目錄） | T-1.1 | BE |
| T-1.4 | 搬遷 seed：8 支 TWGCB 打包經上傳管線落 SYSTEM file 型＋dev-sec 2 條落 SYSTEM url 型（python seed script）；名稱對齊現行 options label | DEV 庫內 10 筆 SYSTEM 公版；menu API 回傳集合與現行 param_schema 靜態 options 一致（零回歸前置驗證）；TWGCB 綁 gcb、dev-sec 綁 inspec | T-1.2、T-1.3 | BE |

### FR-059.2 下發整合 — 依賴：FR-059.1；058.7 resolver 介面談定（D6）

| # | 子任務 | 驗收 | 依賴 | Repo |
|---|--------|------|------|------|
| T-2.1 | param_schema 動態化：inspec / gcb 兩工具 profile 欄位加 `options_source: "profile_library"` 宣告＋升版（INSERT 新 version 拿掉靜態 options，舊版留存） | 新版 param_schema 生效（is_current 切換正確）；舊版可回溯；宣告形狀與 FE 契約一致 | T-1.4 | BE |
| T-2.2 | 派工展開：`_collect_pending_tasks()` 偵測 `profile:<uid>` 前綴 → 依 source_type 展開 `_profile` payload；手填值原樣下發 | 三種取值來源（庫內 file / 庫內 url / 手填）payload 形狀各自正確；`_profile` 不落 FE 執行紀錄與稽核 log（比照 `_credentials`）；參照失效（uid 不存在 / 已停用）時明確報錯不靜默 | T-1.2 | BE |
| T-2.3 | `profile_ref` 授權 resolver：接 058.7 通道的可插拔 resolver 介面，實作「uid 是該 agent 名下 pending / running 任務 params 引用的 profile 檔」驗證 | 合法任務引用可取檔（串流）；非引用 uid / 無任務綁定被拒（拒絕碼以 058.7 端點實際落地為準，其 T-7.1 原卡為 404 不區分情況）；單元測試綠 | T-2.2；**058.7 通道與 resolver 介面已存在（硬依賴）** | BE |

### FR-059.3 Agent 端 — 依賴：**058.7 通道先存在（硬依賴）**；FR-059.2 契約凍結

| # | 子任務 | 驗收 | 依賴 | Repo |
|---|--------|------|------|------|
| T-3.1 | cache＋拉檔：cache 目錄 `/data/content/cache/<uid>/v<version>/`＋完成標記；`_get_stream()` 拉檔；sha256 對帳（不符即 fail）；四格式統一安全解壓器（agent 端同防 slip / bomb） | cache miss → 拉檔 → 對帳 → 解壓全鏈通；cache 命中不重拉（log 佐證）；sha256 竄改樣本 fail 且錯誤可辨識；四格式各一樣本解壓正確 | 契約凍結；058.7 通道 | evidence-agent |
| T-3.2 | connector 目錄交付：`inspec.py` 接 `_profile`——file 型把 cache 解壓目錄餵 `cinc-auditor exec`；url 型 / 手填維持現行為 | file 型 profile 掃描成功、報告回收；url 型與手填路徑迴歸綠（現行為零變化）；`_profile` 缺漏或形狀錯誤時明確報錯 | T-3.1 | evidence-agent |
| T-3.3 | 封閉網路情境驗證：agent 無外網環境（模擬）下，僅靠平台通道取得 file 型 profile 完成端到端掃描 | 斷外網的 agent 完成 GCB 掃描全鏈（CM-992 限制①的直接驗證）；過程無任何外網請求 | T-3.2 | evidence-agent |

### FR-059.4 FE — 依賴：FR-059.1 API 落 DEV（管理頁）；FR-059.2（下拉）

| # | 子任務 | 驗收 | 依賴 | Repo |
|---|--------|------|------|------|
| T-4.1 | 管理頁：系統管理下新頁「掃描設定檔管理」——列表 / 上傳（雙軌表單）/ 版更 / fork / 複製到另一工具 / 停用；靠 scope 區分能力（公版對非 root 唯讀） | tenant 管理員與 root 各自能力正確（root 可維護公版、租戶只能動自有＋fork 公版）；上傳表單依 source_type 切換欄位；驗證失敗錯誤訊息可讀 | T-1.2 | FE |
| T-4.2 | 任務抽屜下拉動態化：偵測 `options_source: "profile_library"` 宣告改呼叫 menu API（帶 detection_tool_id 過濾）動態組 options；手填能力保留 | GCB 抽屜只見 gcb 池、CINC 只見 inspec 池（P7）；手填照常（P6）；下拉內容與搬遷前一致（零回歸）；無宣告的欄位維持靜態 options 行為 | T-2.1 | FE |

> **後續版（非本案第一版範圍，D5）**：新機制上線驗收通過後的下一版，拿掉 agent `/data/content` bind mount 與 param_schema 靜態 options 舊路（手填容器路徑能力因 P6 永遠保留）。

---

## 7. 驗收（端到端）

1. **上傳四格式**：管理頁分別上傳 zip / tar / tar.gz / tgz 四種格式的 profile → 全部通過驗證入庫；惡意樣本（path traversal / 解壓 bomb / 無 `inspec.yml` / 超過 50MB / 假副檔名）全數被擋且錯誤訊息明確可辨識。
2. **下拉即選**：上傳後任務抽屜下拉立即可選（不經任何 migration / 部署動作）；GCB 抽屜只見 gcb 池、CINC 只見 inspec 池（P7）。
3. **派工取檔全鏈**：選庫內 file 型 profile 發起執行 → agent cache miss → 走 058.7 通道拉檔 → sha256 對帳通過 → 安全解壓 → 解壓目錄餵 cinc-auditor → 掃描成功、報告回收進證據池。
4. **cache 命中**：同 profile 再次派工 → cache 命中不重拉（agent log 佐證）；版更後派工 → 因 cache key 含 version 自然 miss、拉新版。
5. **對帳防線**：sha256 不符（竄改模擬）→ 掃描 fail 且錯誤可辨識，不靜默使用髒檔（FR-039 不變式）。
6. **三層取值相容**：庫內 url 型（dev-sec 搬遷公版）與現場手填值照現行為執行成功（P6 零破壞）。
7. **雙軌守門**：公版 fork 成租戶自有副本可編輯；非 root 對公版寫入被擋（403）；租戶 A 看不到租戶 B 的自有 profile（RLS）。
8. **封閉網路情境模擬**：agent 無外網、僅靠平台通道取得 profile 完成掃描——CM-992 限制①的直接驗證。
9. **下拉零回歸**：搬遷 seed（P3 / D7）＋param_schema 升版後，下拉內容與搬遷前完全一致（8 支 TWGCB＋2 條 dev-sec 名稱不變）。
10. **三環境節奏**：以上驗收全數在 DEV 完成；STG / POC 的 migration / seed / agent 部署一律等決策者明示放行（D10 環境異動鐵律）。
