---
title: E4 檢查結果：設定與插件殼——分類設定 CRUD／模型碼表／plugin 契約／守門殼（jedi-evidence-classification）
---

# E4 檢查結果：設定與插件殼——分類設定 CRUD／模型碼表／plugin 契約／守門殼（jedi-evidence-classification）

> 檢查日期 2026-09-19｜對應卡片 CM-1950｜檢查範圍 16 個檔案 1,850 行

## 🔴 一句話結論

**這一棒範圍內沒有找到任何資安漏洞——工具提的唯一一條被三位檢查員一致駁回，我自己把卡片列的七個重點逐項開檔核對後也同意：守門、租戶隔離、插件接線這三道該擋的都擋住了。**

白話講這棒在查什麼：**「AI 分類設定」就是客戶用來告訴 AI「你是誰、怎麼判、什麼算證據」的那一份設定**，它管的是整個框架版本底下所有專案的 AI 行為。所以要問三件事——**誰能改它**（不能是任何人）、**看得到多少**（別的客戶的人設與提示詞是客戶的稽核方法論，不能外流）、**套件掛進系統時零件缺了會怎樣**（該拒絕啟動，不能靜默放行）。三件都查了，都沒問題。

**唯一值得決策者知道的一件事，但它不是漏洞**：查「這個租戶有哪幾家 AI 可以選」的那支端點，確實沒有像它隔壁四支一樣要求管理員身分——任何登入者都打得到。三位檢查員一致判定這不算漏洞，理由是**它回的只有「anthropic」「openai」這種廠商名字加上一張寫死在程式裡的公開型號表，沒有金鑰、沒有網址、也拿不到別的客戶的資料**；而且**前端的分類操作畫面本來就會呼叫它來填下拉選單**，那些畫面的使用者是專案成員而不是管理員，真的加上管理員檢查會把正常功能擋掉。我開檔核對過這三點都屬實，同意這個判定。詳見下方「逐項查證②」。

## 這一棒在檢查什麼

證據自動分類要用 AI，而「用哪一家的 AI、哪個型號、要它想多深、要它扮演什麼角色、怎麼判斷算不算證據」這些，客戶可以自己設定。這一棒看的就是**這份設定的管理後台那一半，加上整個套件怎麼掛進系統的那層骨架**。

十六個檔案分成三組：

- **設定的建改刪查**（7 檔）：畫面上「新增一份設定／改它／刪掉它／列出來看」背後的那一整條路，從網址進來到寫進資料庫，加上建表腳本。
- **型號清單**（1 檔）：一張寫死在程式裡的表，記著「有哪些 AI 型號、哪個看得懂圖片、哪個是預設」。
- **插件骨架**（8 檔）：這個套件是以「插件」形式掛進主系統的——它自己不知道「誰是管理員」「檔案存哪裡」，這些都要主系統提供。骨架這一層負責**檢查主系統有沒有把該給的零件都給齊**，沒給齊就拒絕掛載。所有端點的登入檢查也是在這一層統一套上去的。

**選它當第四棒的理由**：設定管所有專案的 AI 行為，一旦被任意登入者改掉，等於可以左右整個系統的合規判定結果；而插件骨架是所有端點認證的唯一來源，那層漏一個洞就是一整排端點變成公開的。

掃描目標 `~/Projects/Jedicogy/module/jedi-python-package/jedi-evidence-classification`，branch `feature/review`，版本 `955e409`，mode `scan`，effort `low`，focus `attack-surface`。

範圍十六個檔案：

| 檔案 | 行數 | 角色 |
|------|-----:|------|
| `app/service/classification_profile_service.py` | 311 | 設定的建改刪查主邏輯、守門、四層解析鏈 |
| `migrations/005-classification-profiles.sql` | 175 | 設定表的建表腳本（含權限與租戶隔離規則） |
| `api/routing.py` | 190 | 全部 25 支端點的網址對照表，登入檢查在這裡套上 |
| `domain/ports.py` | 283 | 套件對主系統要求的「零件規格書」（介面定義） |
| `plugin/contract.py` | 144 | 插件對外的契約型別與必填零件清單 |
| `plugin/__init__.py` | 125 | 插件註冊入口 |
| `plugin/assembly.py` | 114 | 接線檢查、登入檢查的套法、blueprint 組裝 |
| `domain/llm_models.py` | 95 | AI 型號碼表（純資料） |
| `api/routes/classification_profile_route.py` | 85 | 五支設定端點的進出口 |
| `api/schemas/classification_profile_schema.py` | 84 | 進出欄位定義 |
| `infra/repository/classification_profile_repository_impl.py` | 59 | 資料庫查詢實作 |
| `domain/service/classification_profile_domain_service.py` | 57 | 領域層薄封裝 |
| `plugin/runtime.py` | 54 | 執行期組件取用 |
| `api/guards.py` | 33 | 執行期組件的取用入口（**實際上沒有守門邏輯**，檔名容易誤會） |
| `domain/entity/classification_profile_query_entity.py` | 21 | 查詢條件的資料結構 |
| `plugin/migrations.py` | 20 | 建表腳本的程式化出口 |

**這支套件的這一塊從來沒有被掃過。** 設定功能是 FR-107 T-5.x／CM-1864（建表與四層解析）、CM-1867（守門從平台管理員放寬到租戶管理員）、CM-1885（型號碼表換代與思考深度三檔）、CM-1888（型號反查廠商）連續做出來的，全部都在 `955e409` 這個版本裡。

## Coverage

十六個檔案全部讀完，就是範圍給的全部。

**不在本棒範圍、但這份報告的結論依賴它們**（我開檔查證時讀了，屬於對照不屬於審查）：
- 主系統側的 `core/plugins/evidence_classification.py`（守門怎麼實作、金鑰怎麼取）
- 主系統側的 `app/system_config/service/ai_provider_key_resolver.py`（金鑰解析鏈本體，199 行）
- 套件的 `evidence_batch_service.py`（設定怎麼被分類流程消費）
- 前端的 `AutoClassifyBatch.vue`／`ClassificationProfilePanel.vue`（誰在呼叫那支端點）

**金鑰解析鏈的本體不在本棒**——本棒只答得了「套件這一半有沒有把金鑰或『誰有金鑰』露出去」，答不了「解析鏈本身守不守得住」。那是主系統接線那一棒的事，FR README 已註明兩支要同一棒做。

低強度跑法不做元件盤點、不做威脅建模、不跑額外廣度掃描，`completenessCheckOutcome` 是 `not-applicable`（指定範圍的掃描本就不適用）。派兩位研究員、兩位都回報；兩個候選去重成一個，**該候選 0:3 被駁回**，沒有候選遺失、沒有候選未被審、沒有被上限砍掉。

**工具沒有執行任何程式碼**：沒有打過任何端點、沒有跑測試、沒有連過資料庫。下面所有判斷都是讀原始碼推出來的。

**工具的偏食**：兩位研究員都只盯「守門有沒有漏」這一類，而且兩位提的是同一條。卡片列的七個重點裡，**工具只碰到①②的一小角，③④⑤⑥⑦完全沒報**，全部由我逐項開檔補查（見下方「卡片重點逐項查證」，每項都標明是工具報的還是人工查的）。

## 掃到什麼（總覽）

**範圍內淨新增零條。** 沒有發現需要開修正卡的資安問題。

工具提出、檢查員駁回的那一條記錄如下（不計入發現數）：

| 候選 | 工具評的嚴重度 | 檢查員判定 | 為什麼駁回 |
|---|---|---|---|
| 查可用型號的端點沒有管理員檢查 | 低 | **3 票全數駁回** | 回的是廠商名字加一張公開型號表，沒有金鑰也沒有別人的資料；而且前端的分類操作畫面本來就要呼叫它，加了檢查會擋掉正常功能 |

我另外記下**兩件不是資安問題、但下一任接手的人應該知道的事**（都不開卡）：

| # | 是什麼 | 為什麼不是資安問題 | 記在哪 |
|---|---|---|---|
| A | 設定表的「AI 人設」與「判斷指引」兩個欄位沒有長度上限，而它們會被寫進給 AI 的指令檔 | 寫得進去的只有租戶管理員（自己人），寫進去的內容只影響自己租戶自己的分類結果；這是「管理員可以把自己的 AI 設定寫得很爛」不是「外人可以動手腳」 | 下方逐項查證③ |
| B | 設定裡的「預設廠商／預設型號」兩個欄位存得進去、畫面填得出來，但分類真的跑起來時**沒有人去讀它們**（只有「預設思考深度」被讀） | 是功能沒接完，不是資安缺口——使用者設了沒作用，症狀是「我明明設了 OpenAI 怎麼還是跑 Anthropic」 | 下方逐項查證⑦ |

## Findings

無。

## 卡片重點逐項查證

卡片列了七個重點。工具不照清單走，所以每一項我都回頭核了一次，下面標明哪些是工具碰過的、哪些是我人工開檔查的。

### ① 守門與放寬後的邊界（人工查證，工具只碰到一角）

**問的是**：設定的建改刪查這五支，到底誰打得到？

**查到的**：`classification_profile_service.py` 的四支寫入與讀取方法——列清單（`:154`）、新增（`:168`）、修改（`:199`）、刪除（`:216`）——**每一支的第一行都是 `self._require_settings_manager()`**，四支無漏。

`_require_settings_manager`（`:221-228`）問的是主系統提供的守門零件 `can_manage_settings()`，回 false 就拋 403。主系統那一半（`core/plugins/evidence_classification.py:123-147`）判的是**租戶管理員**，用的是「儲存設定」那顆既有的能力點（`storage-config.update`），而且它委派給 `common.authz`（FR-048 統一守門），沒有自己另抄一份查詢。

**🔴 這裡有一個做對了、值得記下來的地方**：守門零件沒接上時，程式是**當場拋錯拒絕服務**（`_guard()` `:230-241`），不是「沒接就放行」。這一點在 E3 的舊 Drive 線正好是反例——那條線的查詢端點只驗登入。這裡做對了。

**「租戶管理員改不到原廠設定」也確認成立**：`_require_own_profile`（`:243-253`）比對這筆設定的租戶編號與呼叫者的租戶編號，不一樣就拋 403（而不是假裝不存在）。原廠那一列的租戶編號是 1，一般客戶的不是，所以改不動。

**結論：這一項沒問題。**

### ② 「誰有鑰匙」會不會露出去（工具唯一報的一條，已駁回；我另外核過）

**問的是**：查「這個租戶有哪幾家 AI 可以選」的那支端點（`available_models`，`:119-138`），回的東西裡有沒有洩漏什麼。

**工具說**：這支確實沒有 `_require_settings_manager()`，跟隔壁四支不一樣，所以任何登入者都打得到。

**三位檢查員一致駁回，我開檔核對三個駁回理由，全部屬實**：

1. **回的內容裡沒有金鑰也沒有網址。** 我追到主系統的 `ai_provider_key_resolver.py:184-199`：`configured_providers()` 這支迴圈跑過每一家，只做一件事——解得出金鑰就把**廠商的名字**（字串，例如 `"anthropic"`）加進清單。金鑰本身、網址、任何設定值都沒有進到回傳值裡。套件這一邊（`:129-137`）拿這串名字去跟寫死在程式裡的型號表（`llm_models.py:49-54`，公開資訊）取交集，回出去的是 `provider／model_id／display_name／supports_vision／default` 五個欄位——**沒有 `base_url`、沒有 `is_set` 以外的任何設定狀態**。卡片擔心的「回應裡會不會夾帶 base_url」，答案是不會。

2. **拿不到別的租戶的東西。** 租戶編號來自呼叫者自己的登入脈絡（`:125` 呼叫 `_require_identity()`，`:303-311` 從 `get_user_context()` 取），**不是從網址或請求內容來的**，所以沒有「改個數字看別人的」這種玩法。

3. **前端的正常功能依賴它。** 我在前端 grep 過，呼叫這支的有四個地方：`ClassificationProfilePanel.vue:43`（設定畫面，管理員用）、**`AutoClassifyBatch.vue:315`**、**`AutoClassifyList.vue:265`**、`autoClassifyShared.js:79`。後面三個是分類操作畫面，使用者是專案成員不是租戶管理員——加上管理員檢查會讓他們的型號下拉選單變空白或直接 403。

**所以洩漏的極限是：一個已經登入的自家人，知道「我們公司有設 OpenAI 的鑰匙」這件事。** 不是金鑰、不是別人的資料、不是跨租戶。**我同意檢查員的判定，不開卡。**

**另外確認一件卡片特別點名的事**：四層解析鏈落到原廠那一層時，**分類確實會用原廠的鑰匙跑**（`resolve` `:84-101` 會回原廠那筆設定）。這是 CM-1867 裁示 D13 的刻意設計——**「鎖原廠」鎖的是「不准改」，不是「不准用」**。記在這裡是為了讓下一個看到這段程式的人不要誤判成漏洞。

### ③ 寫入驗證（人工查證，工具未報）

**問的是**：新增／修改設定時，送進來的內容有沒有被檢查？

**查到的**（`_validated` `:265-301`）：

| 欄位 | 有沒有檢查 | 檢查什麼 |
|---|---|---|
| `framework_version_id` | ✅ | 強制轉成整數，轉不了就拋錯 |
| `enable` | ✅ | 強制轉成布林 |
| `evidence_hints` | ✅ | 必須是字典，不是就拋 `EC_PROFILE_HINTS_INVALID` |
| `confidence_threshold_default` | ✅ | 必須是數字且落在 0.0～1.0 之間 |
| `name`／`persona` | ⚠️ 只檢查「新增時不可空白」 | **沒有長度上限** |
| `guidance` | ❌ 無檢查 | **沒有長度上限** |
| `provider_default`／`model_default` | ❌ 無檢查 | **沒有比對型號碼表** |
| `effort_default` | ❌ 在這裡無檢查 | 但下游有（見下） |

**白名單本身是做對的**：`_validated` 只把列在清單裡的欄位挑出來，沒列的一律丟掉——所以送額外欄位（例如想偷偷改 `tenant_id`）是沒用的。

**兩個沒檢查的地方，我判斷都不構成資安問題**：

- **長度沒上限（記為觀察 A）**：`persona`／`guidance` 會被寫進給 AI 的指令檔（`evidence_batch_service.py:1376-1393` → `prompt.json`，`:1108`）。但**寫得進去的只有租戶管理員**，而且影響範圍只有他自己租戶的分類結果。這是「管理員可以把自己的設定寫壞」，不是「外人可以動手腳」。資料庫那兩欄是 `TEXT` 型別（`005:36-37`），沒有硬上限。真要說風險，是有人貼一份超大文字進去讓自己的分類變慢或變貴——那是自傷。**不開卡。**

- **廠商／型號沒比對碼表**：這兩欄存進去的是什麼字串都收。**但真正要跑分類的時候有第二道**：`_resolve_run_params`（`evidence_batch_service.py:1317-1320`）會用 `spec_for()` 去碼表查，查不到就拋 `EC_INVALID_MODEL`、整批擋下。所以存得進髒字串，但跑不起來——**症狀是功能錯誤不是資安問題**。思考深度那欄同理，`_resolve_effort`（`:1330-1343`）每一層都先驗合不合法，不合法就往下一層退。

**卡片特別問的 `apply=False`**：我 grep 過整個套件，**0 命中**，確認沒有這個模式。

**還有一件事值得記**：`classification_profile_schema.py` 裡定義了一個 `ClassificationProfileWriteRequestSchema`（`:11-29`，還帶了 `effort_default` 的 `OneOf` 檢查），但我 grep 全套件，**這個 class 除了自己那行定義之外沒有任何地方引用它**。也就是說**它是死的，真正在把關的是 service 層的 `_validated`**。這不是漏洞（`_validated` 有在做事），但下一個人看到這支 schema 可能會以為進來的東西被 marshmallow 驗過了——實際上沒有。

### ④ 租戶隔離與原廠放行（人工查證，工具未報）

**問的是**：建表腳本裡的租戶隔離規則（RLS，就是「每個客戶只能看自己資料」的資料庫層機制）形狀對不對？原廠那一列被放行到什麼程度？

**查到的**（`005-classification-profiles.sql:105-156`），四條規則：

| 動作 | 規則內容 | 原廠那列（租戶 1）有沒有放行 |
|---|---|---|
| **讀** (`:132-135`) | 超級管理員 or 自己租戶 or **`tenant_id = 1`** | ✅ **有放行** |
| **新增** (`:139-141`) | 超級管理員 or 自己租戶 | ❌ 沒放行 |
| **修改** (`:145-147`) | 超級管理員 or 自己租戶 | ❌ 沒放行 |
| **刪除** (`:151-153`) | 超級管理員 or 自己租戶 | ❌ 沒放行 |

**這正是卡片問的「寫入三條有沒有跟著放行（不該）」——答案是沒有跟著放行，形狀正確。** 讀的時候放行原廠那列是必要的（四層解析鏈的第二層與第四層要讀得到原廠設定，擋掉的話所有租戶都只會拿到程式內建的最陽春版本）；寫入三條不放行，所以業務租戶改不動原廠設定。**資料庫這一層與程式那一層（`_require_own_profile`）是兩道獨立的鎖，都鎖住了。**

**原廠那一列的內容會被所有租戶讀到**——包含它的人設全文。我開檔看了那一列的實際內容（`005:166-171` 的 seed）：是一段通用的英文「你是合規評估專家，把證據對到評估項目」，**刻意不含任何框架名**（註解寫明理由：寫上 CMMC 的話 ISO 客戶的 AI 會一開口就談錯標準）。**沒有客戶資料，沒有敏感內容。** 卡片擔心的「客戶的稽核方法論外流」不成立——外流的是原廠自己寫的通用範本。

**查詢那一側也核過**（`classification_profile_repository_impl.py:47-58`）：`list_for_tenants` 拿到的租戶清單是 service 層算的（`:156`），內容是 `[自己] 或 [自己, 1]`——**不會出現第三個租戶編號**。而且前面有 `if not tenant_ids: return []`，空清單直接回空而不是回全表（這正是 E5 那一棒要查的「空條件回全表」陷阱，這一支沒有踩到）。

`find_generic`（`:30-45`）用的是 `.is_(None)` 而不是 `== None`——註解寫明了理由（SQL 的 `= NULL` 恆為 unknown，會永遠查無而且不報錯）。這是對的寫法。

### ⑤ 插件殼：缺零件會拒絕啟動還是靜默放行（人工查證，工具未報）

**這一項是本棒最重要的查證**，因為插件殼是所有端點認證的唯一來源。

**問的是**：主系統掛這個套件時如果漏接某個零件，會發生什麼？

**查到的**，分成三類：

**第一類——缺了就拒絕掛載（`plugin/assembly.py:19-33`）。** 必填清單是四個（`contract.py:29`）：登入檢查、專案目錄、專案角色守門、證據來源。缺任何一個，`_assert_wiring` 直接拋 `RuntimeError`，**服務起不來**。註解寫明了為什麼不能改成「有就用、沒有就跳過」：跳過的話觸發分類／改結果／歸檔這些端點會全部變成公開端點，而**服務照常起得來、健康檢查照樣綠燈**。**這是做對的。**

**第二類——缺了會拋錯但不擋啟動。** 管設定的那個守門零件（`platform_admin_guard`）**不在必填清單裡**。我核過缺了會怎樣：`_guard()`（`:230-241`）在被呼叫時拋 `RuntimeError`，訊息明講「拒絕以無守門狀態服務」。所以**設定的五支端點會 500，不是放行**——這是安全的失敗方式。決策上把它放在選填是合理的：不需要設定功能的 consumer 可以不接，接了才會用到。

**第三類——缺了會降級成空回應。** `ai_provider_config` 缺了的話 `available_models` 回空清單（`:127-128`），前端顯示「尚未設定任何供應商金鑰」。這是刻意的（註解寫明「比列出一堆選了會 401 的模型好」），也不涉及安全。

**登入檢查怎麼套上去的**（`_guarded` `:36-46`）：用動態產生子類別的方式，把 `get／post／put／delete／patch` 五個動詞各自包一層。**註解特別說明不直接改原本類別的屬性**——因為那會污染跨 app 的共用類別（同一套件掛到兩個 app 時互相影響）。這個作法是對的。

**🔴 卡片要我逐支核對「每一支 `add_resource` 是不是都經過 `R()` 包過」——我用程式解析了 `routing.py` 的每一個 `add_resource` 呼叫：總共 25 支，25 支全部第一個參數都是 `R(...)`，零遺漏。** 這是本棒最該核的一件事，因為漏包一支就是那支端點變成不需要登入，而路由表看起來完全正常。

**金鑰進套件的入口**（`container_env`，`contract.py:63`）：我 grep 了整個套件裡所有碰到它的地方（7 處），**沒有任何一處把它 log 出來**。它只被拿去傳給容器執行器（`assembly.py:100` → `classifier_container_runner.py:150` 合併成容器環境變數）。**金鑰沒有在本棒範圍內被記錄下來。**（容器執行那一段是 E1 的範圍，E1 已經掃過並報了「金鑰走 docker 命令列」那一條。）

**還有一個容易誤會的地方值得記**：`api/guards.py` 這個檔名看起來像放守門邏輯的地方，但**它裡面一行守門都沒有**，只有一支 `runtime()` 用來取執行期組件。檔案自己的註解有講明這件事（`:8-11`：「認證守門不在本檔」）。下一個人 grep `guards.py` 想找授權邏輯會找不到——真正的位置是 `assembly.py` 的 `_guarded` 加上 service 層的 `_require_*`。

### ⑥ 零件規格書裡的契約假設（人工查證，工具未報）

**問的是**：套件對主系統開出的「零件規格」（`domain/ports.py`），有沒有留下會被誤解的空間？

**專案角色守門那三支**（`IProjectRoleGuard` `:63-82`）：規格寫明三支**都只回 true/false，由套件自己負責拋錯誤碼**。理由寫在註解裡——錯誤碼是套件的對外契約（前端的多語系訊息對著它），讓主系統各拋各的會讓前端訊息掉回英文。**這個設計是對的，也確實被遵守**（我核過主系統那一邊 `core/plugins/evidence_classification.py:110-120`：它 catch 住 `ForbiddenError` 然後 `return False`，沒有自己往外拋）。

`is_any_project_manager` 的語意（「租戶內任一專案的管理者」）規格有寫清楚，並註明「租戶範圍由資料庫隔離鎖」。

**🔴 `IEvidenceStorage.stage_to_dir` 這一項卡片點名要記給下一棒**（`ports.py:238`）：規格本身沒有寫「傳進來的檔案編號必須屬於這個租戶」。**我開主系統那一邊核對過實際實作**（`core/plugins/evidence_classification.py:331-361`），結論與卡片檔頭的自陳**不同**——實作裡**有**跨租戶防呆：

> 拉檔前先擋越租戶：本方法跑在背景執行緒（無 RLS 收斂），少了這道就能靠猜 uid 把別租戶的檔拉進自己的分類工作目錄。

程式碼是 `:354-357`：取出每個檔案的擁有租戶編號，跟傳進來的租戶編號比，不一樣就拋 403。**所以這一點實作是守住的，但規格書沒寫**——下一個寫別的實作的人可能不知道要做這道。**這是文件缺口不是資安缺口**，記給主系統接線那一棒參考。

**`IAiProviderConfig.container_env`**（`:104-116`）：規格明寫「套件只問哪幾家可用，不碰金鑰值」。主系統那一邊的實作（`:186-203`）**確實只放這次要用的那一家的那一把**，註解寫明「不是把幾家的鑰匙全倒進容器：多給一把就是多一份會隨容器外洩的憑證」。**規格與實作一致，而且方向是對的。**

### ⑦ 型號碼表（人工查證，工具未報）

**問的是**：這張寫死在程式裡的型號表（`llm_models.py`）有沒有問題？

**這是一支純資料檔，沒有安全疑慮**——沒有外部輸入、沒有查詢、沒有寫入。我核了卡片問的三件事：

- **`spec_for` 查不到時回什麼**（`:80-85`）：回 `None`，由呼叫端擋下。**卡片提到的 `f8fcde4` 那次改動（不再硬退 anthropic）確認在這個版本裡**——`spec_by_model_id`（`:88-95`）在兩家出現同名型號時回 `None` 而不是猜一家，註解寫明理由（猜錯的症狀是整批送去錯的供應商，比明確退回預設更難查）。
- **`supports_vision`**：四個型號目前全是 `True`。碼表註解提醒「不支援圖片的型號遇到圖片證據要明確標示，不可靜默跳過」，實際執法在容器端（不在本棒）。
- **思考深度三檔**：`low／medium／high`，註解說明兩家 API 其實還收更高檔位但**刻意不開給使用者**（成本倍數成長、準確度邊際遞減）——這是產品決策的紀錄，不是缺口。

**順便查到的事（記為觀察 B）**：設定表有 `provider_default` 與 `model_default` 兩欄，畫面填得出來、也存得進資料庫。但我 grep 了分類流程實際怎麼用設定——`_resolve_run_params`（`evidence_batch_service.py:1286-1326`）**只讀了 `effort_default`，沒有讀 `provider_default` 與 `model_default`**。程式自己的註解也承認了：

> 供應商／模型的 profile 那一層（framework_version_chain 沿鏈解析）尚未接上，接上之後插在「批次上的設定」與「內建預設」之間。

**這不是資安問題，是功能沒接完**——使用者在設定裡選了「預設用 OpenAI」，實際跑分類時還是會落到批次自己的設定或內建預設。症狀是「我明明設了怎麼沒用」。記在這裡給 FR-107 的後續參考。

## 這份結果可信到什麼程度

要分兩層講，這兩層的可信度差很多：

**第一層：「這 16 個檔案裡有沒有我漏掉的洞？」——中等偏可信。**

正面的依據：驗證機制完整跑完了（驗證章 `verified`），三位檢查員全部投了票，沒有人因為額度用完而失聯；而且**卡片列的七個重點我全部逐項開檔核過**，不是只靠工具。守門那三道（程式層的 `_require_settings_manager`、資料庫層的 RLS 寫入三條、插件層的 25 支路由包裹）我是一支一支數過的。

要打折的地方：**工具的兩位研究員視角很窄**，兩位提的是同一條、同一類（守門缺失），這代表它們沒有從其他角度看過這批檔案。零發現有一部分是「這批檔案確實乾淨」，也有一部分是「工具只從一個角度看」。第二部分由我的人工查證補了，但人工查證是照卡片的七個重點走的——**卡片沒想到的角度，這一棒也沒查到**。

**第二層：「AI 分類設定這整件事安全嗎？」——這一棒答不了。**

因為**金鑰解析鏈的本體不在本棒範圍**。「租戶的鑰匙 → 原廠的鑰匙 → 環境變數」這三層退路怎麼查設定、有沒有可能在無脈絡時挑到別租戶的那一列，本體在主系統的 `ai_provider_key_resolver.py`（199 行）與 adapter 那一段。本棒只查得了「套件這一半有沒有把鑰匙或『誰有鑰匙』露出去」——答案是沒有。**另一半要等主系統接線那一棒。**

另外，**設定被消費的那一端（`evidence_batch_service.py` 怎麼把人設寫進給 AI 的指令檔）屬於 E2 的範圍**，E2 已經掃過並報了相關的事（證據檔內文可對 AI 下指令那條在 E1）。本棒只確認了「設定寫得進去的是誰」。

## 執行概況

| 項目 | 數值 |
|---|---|
| 掃描工具 | Claude Code `claude-security` plugin 0.10.2.3 |
| Run ID | `wf_ea23b9f1-1a0` |
| 報告目錄 | `jedi-evidence-classification/CLAUDE-SECURITY-20260919-072959/` |
| 驗證章 | `CLAUDE-SECURITY-REVISION-955e40964baa-dirty.json` |
| 掃描版本 | `955e40964baa3cfc0e8766078db7d65075d3c50b`（branch `feature/review`） |
| dirty | `true`（工作區有未追蹤的 `.claude/` 目錄，非範圍內檔案） |
| mode／effort／focus | `scan`／`low`／`attack-surface` |
| 範圍 | 16 檔 1,850 行 |
| 研究員派出／回報 | 2／2 |
| 候選數 | 2（去重後 1） |
| 面板票數 | 3（＝候選 1 × 3 位檢查員），**全數投出，無漏投** |
| 通過門檻的發現 | **0**（唯一候選 0 票贊成、3 票反對） |
| `verification.status` | **`verified`**（照 stamp 原文抄；意思是「驗證程序完整跑完」，不是「程式證明無漏洞」） |
| 被上限砍掉的候選 | 0 |
| 未被審的候選 | 0 |
| 耗時 | 約 69 分鐘（4,143 秒） |
| agent 總數 | 5（2 研究員 ＋ 3 檢查員），全部完成、零錯誤 |
