---
title: D3-4a 檢查結果：編排服務前半 — 派工／取消／刪除（jedi-detection）
---

# D3-4a 檢查結果：編排服務前半 — 派工／取消／刪除（jedi-detection）

> 檢查日期 2026-09-19｜對應卡片 CM-1875（D3 第 4 小棒前半）｜檢查範圍 1 個檔案 2,498 行（本棒歸屬前 1,151 行）

## 🔴 一句話結論

**找到 1 條新的中風險：解密後的帳密被原封不動寫進工單資料表，而且這份副本根本沒人用。** 系統把客戶的 SSH 密碼、掃描器管理員 token 解密之後，一邊交給代理程式、一邊又存了一份明文到 `agent_tasks.params` 這張表——**代理程式拿到的其實是另一份**（心跳組裝時當場重解的），存下來這份從頭到尾沒有任何程式讀過。這張表沒有任何清理或保存期限，所以那份明文會一直躺著。修法是刪掉那一行加一支清資料的 migration，**不影響代理程式**（已逐環核對過）。另外工具還報了兩條，都是**前面棒次已經記過的**：`list_executions` 缺參與者檢查＝重複第 88 項、換工具繞過台數上限＝重複第 105 項，本棒不重複計數。卡片重點①「誰能按」工具沒直接查，首腦人工核對**九個寫入端點全部有守門、無缺口**。

## 這一棒在檢查什麼

「執行檢測」這件事的中控台就是這支檔案。客戶在畫面上按「開始掃描」「取消」「重跑這一台」「刪掉這筆失敗紀錄」，每一個按鈕背後都是這支檔案裡的一個方法。

這一棒看的是**按下去之後發生什麼**，兩件事：

1. **誰能按**。這些按鈕的破壞力差很多——「開始掃描」會對客戶的正式機器發動掃描，「刪除」會讓紀錄消失。每一支都該先確認「按的人是不是這個專案的人」，漏掉任何一支都是缺口。
2. **帳密解密之後往哪裡流**。掃描要登入被掃的機器，所以系統必須把加密存放的帳密解開。解開之後那份明文經過哪些地方、有沒有在不該留的地方留下來，是這一棒的重點。

掃描目標 `/Users/chouraymond/Projects/Jedicogy/module/jedi-python-package/jedi-detection`，revision `955e40964baa`，mode `scan`，effort `low`，範圍就是編排服務 `detection_orchestration_service.py` 這一個檔案（2,498 行）。

**行號分界說明**：這支檔案由 D3-4a／D3-4b 兩小棒分掃，D3-4a 負責前 1,151 行（派工／取消／刪除），1,152 行之後歸 D3-4b。但工具的低強度模式是**整檔一次讀完、不吃行號範圍**，所以它報出來的東西可能落在後半段——本棒依卡片分界處理：落在前半的計入本棒，落在後半的標註清楚、留給 D3-4b 那棒判斷。

## Coverage

`low` 強度：一位研究員讀完這個檔案就提報候選，未做元件盤點、未做威脅建模、未跑額外密鑰專項掃描，`completenessCheckOutcome` 為 `not-applicable`（低強度本來就不跑盤點）。驗證跑了 **1 輪**，3 個候選去重後仍是 3 個，**三條全數通過**，沒有候選遺失、沒有嚴重度被降低、沒有候選被略過。

研究員為了追資料流另外讀了範圍外的幾處（主專案的路由層與心跳組裝、代理程式端的工單執行器、jedi-remote-agent 的工單 model、SQL migration、綁定處理器），那些只當佐證、沒有納入稽核。研究員沒有回報「哪些檔案沒讀完」的自述（`coverage.research` 為 null），所以工具端沒有做讀取完整度的交叉檢查。

工具沒有實際執行任何程式碼：沒跑測試、沒發請求、沒示範攻擊，所有判斷都是讀原始碼推出來的。首腦核對階段同樣只做讀取（開檔、grep），沒有改動任何檔案、沒有連任何資料庫。

**工具碰到了卡片重點②「憑證解密後流向」，重點①「誰能按」一條候選都沒提**，由首腦回頭開檔逐支核對補上，詳見下方「卡片重點逐項人工查證」段。

> **現況（2026-10-01）**：本棒各條後來的處理結果如下（過程紀錄保留，不改）。
>
> - F1（解密帳密另存明文進工單表）＝M03 第 9 條，✅ 已修（FR-114.4-3）。
> - F2（查執行歷史缺成員檢查）＝M03 第 12 條，✅ 已修（CM-2040）。
> - F3（換工具範圍不重驗）＝M03 第 11 條，✅ 已修（CM-2058）。

## 掃到什麼：總覽

| 編號 | 這是什麼問題 | 出事會怎樣 | 要先有什麼 | 在哪裡 | 嚴重度 | 計數 |
|---|---|---|---|---|---|---|
| F1 | 解密後的帳密被寫進工單資料表存著，而且沒人讀它 | 拿到資料庫備份／唯讀帳號的人，直接得到客戶正式機器的可用帳密 | 有人按過一次執行 ＋ 之後取得資料庫讀取權 | `detection_orchestration_service.py:480` | MEDIUM | **本棒新增** |
| F2 | 查執行歷史沒檢查是不是專案成員 | 同租戶任何人看得到別專案的掃描歷史與報告檔編號，再拿編號下載完整報告 | 一個有效登入身分 ＋ 知道別專案的任務編號 | `detection_orchestration_service.py:1667` | MEDIUM | 重複第 88 項 |
| F3 | 換工具時掃描範圍不重驗，超大網段被逐台展開 | 後端記憶體被吃爆，API 服務變慢或倒掉 | 專案管理者身分 ＋ 兩次修改 ＋ 按執行 | `detection_orchestration_service.py:555` | MEDIUM | 重複第 105 項 |

**淨新增 1 條（F1）**，登記跨 arc 總表。F2 落在後半段（1,667 行）本不屬本棒，且已是第 88 項；F3 的落點雖在前半（555 行），但缺口本體與修法在 D3-2 已完整記為第 105 項，本棒只是從另一端又看到同一件事。

## Findings

### F1 — 解密後的帳密被寫進工單資料表存著，而且從頭到尾沒有任何程式讀它（MEDIUM，confidence high）**本棒新增**

**這是什麼問題。** 客戶要掃描自己的機器，系統得有登入用的帳密——SSH 的 root 密碼、Windows 遠端管理的帳密、OpenVAS／ZAP／SonarQube 的管理員 token。這些東西存進資料庫時是**加密**的，這是產品刻意做的保護（負責這件事的元件在 `domain/ports.py` 的註解裡自己寫著「缺了就是明文落庫」）。

按下「開始掃描」時，系統必須把它解開才能用。問題出在解開之後多做了一步：`:480` 這一行把解出來的明文原封不動塞進派工參數，而派工參數整包會被寫進 `agent_tasks` 這張表的 `params` 欄位。那個欄位是普通的 JSON 欄位，**沒有再加密**。

於是同一份帳密有兩種存在形式：`tenant_detection_tool_configs` 裡那份是密文（受保護），`agent_tasks.params` 裡這份是明文（不受保護）。加密這道防線等於被繞過了。

**最關鍵的一點：存下來這份沒有任何程式讀過。** 這是首腦核對時查出來的，比工具原本講的更乾淨——詳見下方「修法成立性核對」。

**出事會怎樣。** 任何拿得到資料庫讀取權的人，一句 SQL 就得到客戶正式機器的可用帳密：

```sql
SELECT params->'_credentials' FROM compliance.agent_tasks;
```

「拿得到讀取權」的情境比想像中多：資料庫備份檔、`pg_dump` 出來的檔案被複製到筆電、給報表用的唯讀帳號、唯讀的複本資料庫、或是別處的一個唯讀型 SQL 注入漏洞。這些情境本來都只該洩漏業務資料，現在額外附送一整份可以直接登入客戶機房的帳密。

而且**這份明文會一直留著**。`agent_tasks` 這張表沒有任何清理機制（見下方查證），所以每一次執行掃描就多留一份，累積下去。

**要先有什麼才打得到。**
- 有人按過至少一次「開始掃描」（或重跑），而且那個工具有設定帳密。實務上這是常態——目前六個工具全部宣告為需要帳密。
- 攻擊者之後取得 `compliance.agent_tasks` 的讀取權：資料庫帳號、備份、dump 檔、複本，或唯讀型 SQL 注入。

第二個條件是「額外還要發生一件事」，所以是中風險不是高風險——但它的性質是**把別的事故放大**：原本只是「備份檔外流」，現在變成「備份檔外流＋客戶機房被登入」。

**在哪裡。**
- 缺口本體：`jedi-detection/jedi_detection/app/service/detection_orchestration_service.py:480` —— `params["_credentials"] = creds`
- 明文從哪來：同檔 `:1397` `_resolve_credentials()` —— 解密 `tenant_detection_tool_configs.credentials_encrypted`
- 寫進資料庫：同檔 `:488-491` `create_task(params=params)`
- 落地的欄位：`compliance-manager-be/scripts/sql/packages/jedi_remote_agent/001-remote-agent-tables.sql:80` —— `params JSONB NOT NULL DEFAULT '{}'::jsonb`，沒有加密包裝

**修法成立性核對（首腦補查，這是本條能不能修的關鍵）。**

工具的建議是「刪掉 `:480`，因為心跳組裝時會另外現場注入帳密，存這份是多餘的」。這句話**屬實，而且實際情況比它講的更乾淨**。逐環查證如下：

1. **代理程式領單時帳密確實另有來源。** 代理程式定期「心跳」問雲端有沒有新工單，組工單內容的是主專案的 `infra/remote_agent/adapter/detection_task_payload_provider.py`。它的 `build_pending_payloads()`（`:48`）組出來的每張工單長這樣：

   ```python
   {
       "uid": ...,
       "detection_tool_id": ...,
       "detection_tool_code": ...,
       "params": self._inject_source_file_limits(params),
       "credentials": self._resolve_tool_credentials(t.detection_tool_id, tenant_id),  # ← :68
   }
   ```

   `credentials` 是**頂層獨立的鍵**，由 `_resolve_tool_credentials()`（`:109`）當場去查 `tenant_detection_tool_configs` 重新解密產生——**完全不看 `params` 裡有什麼**。

2. **代理程式讀的是哪一個鍵。** 查代理程式本體（`evidence-agent` repo）`core/task_executor.py:101`：

   ```python
   connector = get_connector(
       task.get("detection_tool_id"), params, task.get("credentials") or {}, ...
   )
   ```

   它讀的是 `task["credentials"]`，也就是心跳當場解的那份。**全 repo grep `_credentials` 的結果：代理程式端沒有任何一處讀取這個鍵**，只有三處註解提到它（都是在講「`_` 開頭是雲端注入的內部欄位」這個命名慣例）。

3. **雲端這邊也沒人讀。** 三個 repo 一起 grep，`params["_credentials"]` 的讀取端掛零：唯一的寫入在 `:480`，其餘全是註解或 docstring 提及。反而有一處是**專門把它剝掉**的——`_visible_scan_params()`（`:2271`）在回 API 之前會把所有 `_` 開頭的欄位濾掉，註解寫明理由是「`_credentials`：租戶層憑證明文，派工時塞入」。換句話說，**程式自己知道這裡有明文、自己在出口做了補救**，但補救的是 API 回應，資料庫裡那份沒有被處理。

4. **同一份檔案裡已經有正確的做法可以對照。** `_resolve_source_file()` 的註解（`:796-800`）在講另一個欄位（限額）為什麼不存進 JSON 時寫道：「改由心跳組裝時當場注入（與 `_credentials` 同時機）」——**設計者本人已經認定帳密是「心跳當場注入」那一類**，`:480` 這行存進去的是與該設計矛盾的殘留。

**結論：刪掉 `:480` 之後，代理程式照樣拿得到帳密，修法成立。**

**`agent_tasks` 有沒有清理或保存期限 —— 沒有（首腦查證）。**

查了兩處：

- **排程工作清單**（`core/scheduler.py`）：全系統共九支定期工作，分別是完整性抽查、Drive 同步、webhook 續約、框架解析暫存清理、授權到期狀態機、防竄改同步、**檢測執行逾時收斂**、任務綁定孤兒清理、日誌分區維護。其中沒有任何一支碰 `agent_tasks`。名字最接近的 `detection_execution_timeout` 收斂的是 `detection_executions`（執行紀錄）的狀態，不刪工單、也不清參數。
- **程式碼層**：`app/`、`common/`、`core/` 全域 grep `agent_task` 配上清理類關鍵字（purge／cleanup／delete／retention／expire），**零命中**。

所以那份明文沒有時限，只會累積。**這讓 F1 的嚴重度撐在 MEDIUM 而不是更低**——若有 30 天保存期限，風險窗口至少有界。

**怎麼修。** 兩步，兩步都要做：

1. **拔掉源頭**：刪掉 `:480` 的 `params["_credentials"] = creds`。這一行之後 `creds` 在 `_dispatch_one` 裡就沒有其他用途了，連帶可以把參數傳遞鏈上多餘的 `creds` 一併收掉（`start_execution` 仍要保留 `_resolve_credentials` 的呼叫——`:219-226` 用它做「沒設帳密就擋下來」的前置檢查，那個檢查要留）。
2. **清存量**：寫一支 migration 把既有資料裡的這個欄位拿掉。這一步不能省——光改程式只讓「以後不再新增」，資料庫裡已經躺著的那些明文原封不動。

   ```sql
   UPDATE compliance.agent_tasks
      SET params = params - '_credentials'
    WHERE params ? '_credentials';
   ```

   套用規範照 `sql-migration` skill：`cmmgr` 帳號、`--single-transaction`、收尾寫 `schema_migrations`。**先只套 DEV**，STG／POC 等決策者放行（環境異動鐵律）。

⚠️ **修完要實測一次完整掃描**。這條路徑的驗證不能只看程式碼——`_dispatch_one` 改完之後，要實際派一張需要帳密的工單（例如 OpenSCAP）、讓代理程式領走、確認它真的登入成功。理由是本條的修法建立在「代理程式讀的是另一份」這個判斷上，判斷雖然逐環核對過，但**沒有實跑驗證**；掃描能不能登入成功是這個判斷唯一的硬證據。

**驗證。** 3/3 三位檢查員確認成立（可達性、影響、既有防護三個角度），嚴重度維持 MEDIUM，沒有被降級。三位各自獨立追同一條鏈：解密 → `:480` 塞入 → `create_task` → 落進沒加密的 JSON 欄位，並各自確認了中間沒有任何一處做過遮蔽或剝除。

**首腦核對註記。** 屬實，逐環開檔核對過：`:480` 確實把明文塞進去、`:1397` 確實是解密來源、`:488-491` 確實寫進資料庫、SQL 定義確實是普通 JSONB 沒有加密。工具聲稱的「心跳會另外現場注入」經三個 repo 交叉核對**確認屬實**（見上），且進一步查出**代理程式端沒有任何一處讀 `_credentials`**——這比工具的說法更強，代表刪除的風險比工具評估的還低。`agent_tasks` 無清理機制亦經排程清單與程式碼雙重查證確認。

### F2 — 查執行歷史沒有檢查是不是專案成員（MEDIUM，confidence high）**重複第 88 項，不計新發現**

**這是什麼問題。** 同一個檔案裡有九支方法會對「某個任務」做事，其中八支都會先問一句「按的人是不是這個專案的成員」，只有查執行歷史那支沒問。它只檢查了租戶（也就是「是不是同一家客戶」），所以同一家客戶底下、不同專案的人，拿著別的專案的任務編號就查得到。

**出事會怎樣。** 查得到的內容包括對方專案掃了哪些內網網段、用什麼工具、失敗訊息，以及**報告檔的編號**。報告檔編號等同下載憑證——下載端點只驗「有沒有登入」不驗「這份報告是不是你的」，所以拿到編號就能下載對方專案的完整弱點掃描報告。

**在哪裡。** `detection_orchestration_service.py:1667` `list_executions`。

**計數處理。** 這條是**第 88 項**，前面棒次已經記過，本棒不重複計入。另外它落在 1,152 行之後，依 D3-4a／D3-4b 的分界本來就不屬本棒範圍——工具因為整檔一次讀而撞到。**D3-4b 那棒若再撞到，同樣標重複即可。**

**驗證。** 3/3 檢查員確認成立。本棒首腦核對時順帶確認了「八支有、一支沒有」這個對比屬實（見下方重點①的逐支清單），**與第 88 項的既有記載一致，沒有新增資訊**。

### F3 — 換工具時掃描範圍不重驗，超大網段被逐台展開（MEDIUM，confidence medium）**重複第 105 項，不計新發現**

**這是什麼問題。** 「掃哪些機器」這欄有台數上限，但上限只在存檔時檢查、而且只檢查「這次送上來的值」。先綁一個不受上限管的工具把 `10.0.0.0/8` 存進去，再只換工具不帶參數，那個超大網段就留在原地跟到新工具底下。按下執行時，派工端會把它逐台展開成 1,670 萬個位址，後端記憶體被吃爆。

**在哪裡。** 展開那一端在 `detection_orchestration_service.py:555`（本棒範圍內），寫入端的缺口在 `detection_job_binding_handler.py:201-204`（D3-2 範圍）。

**計數處理。** 這條是 **第 105 項**，D3-2 那棒已經從寫入端完整記過，連修法（寫入端重驗 ＋ 展開端硬上限）與「展開端刻意不擋是設計判斷、硬上限要訂寬」的注意事項都寫清楚了。本棒只是從展開端這一側又看到同一件事，**不重複計入，也沒有新增資訊**。

**驗證。** 3/3 檢查員確認成立，confidence 記為 medium（攻擊路徑要兩步操作，是讀程式碼推出來的、沒有實跑）。

## 卡片重點逐項人工查證

卡片重點有兩項：①「誰能按」②「憑證解密後流向」。工具碰到了②（就是 F1），①一條候選都沒提，首腦開檔補查。

### ① 誰能按：九支寫入端點的守門 — ✅ 沒問題（工具未報，人工查證）

本棒範圍的七支方法（加上範圍外的兩支一併核對）逐支開檔確認，**全部都有專案成員檢查，沒有缺口**：

| 方法 | 行號 | 成員檢查 | 額外守門 |
|---|---|---|---|
| `start_execution` 開始掃描 | `:196` | ✅ | 任務狀態須為處理中、無執行中群組、帳密須已設定 |
| `cancel_execution` 取消 | `:871` | ✅ | — |
| `rerun_assignment` 重跑單台 | `:925` | ✅ | — |
| `cancel_assignment` 取消單台 | `:1007` | ✅ | — |
| `cancel_group` 整組取消 | `:1032` | ✅ | — |
| `delete_execution` 刪單筆 | `:1097` | ✅ | 只准刪 failed；跨租戶回 404 不回 403 |
| `delete_group` 整組刪 | `:1127` | ✅ | 全組皆 failed 才准；空群組也擋 |
| `start_assignment_now` 立即開始（範圍外） | `:1470` | ✅ | — |
| `list_executions` 查歷史（範圍外） | `:1667` | ❌ | **缺口＝F2／第 88 項** |

**守門的實際判定邏輯。** `assert_project_participant` 走的是 FR-048 軸③（資源域守門，在 app service 層而非 route decorator，符合規範）。判定鏈是：任務 → 流程執行 → 反查專案 → 查使用者在該專案的角色；**查無角色就擋**（fail-closed）。任一角色都放行（manager／auditor／reviewer／viewer），所以它擋的是「不是這個專案的人」，不是「角色不夠大」。

**路由層另有一道授權鎖。** 這些端點在路由層都掛了 `@require_license("plugin")`，也就是客戶得買了檢測模組才能用。這是產品授權層，**不是權限層**——同一租戶內所有人的授權狀態相同，所以它擋不了「同租戶跨專案」，F2 的缺口不會因為有這道鎖而被補上。

**兩個設計細節值得記。**

- **跨租戶一律回「查無此物」不回「你沒權限」**（`delete_execution` `:1090-1095`、`_require_group`）。註解寫明理由是「避免成為探測管道」——回 403 等於告訴對方「這個編號是存在的」，攻擊者可以用它逐一試出有效編號。這個處理是對的。
- **刪除限制得很嚴且理由充分**：只能刪失敗的紀錄。成功的掛著報告與證據，刪了證據鏈就斷；執行中的要先取消；已取消的是使用者自己按的、屬有意義的軌跡。整組刪還多一層「含重跑歷史在內全部都是失敗」才准，避免夾著一筆成功的被整組刪掉。**判定：限制方向正確，沒有把破壞力開得太大。**

**判定：重點①這一項沒有問題**，唯一的缺口是已記在案的第 88 項。

### ② 憑證解密後流向 — ⚠️ 有一處缺口（就是 F1），其餘處理正確

完整的流向追蹤：

**解密只發生在一處**，`_resolve_credentials()`（`:1397`）。它的設計有兩點值得記：

- **查詢時才解析，不在綁定時寫死。** 註解點明使用者可能「先建任務、後設工具憑證」，綁定當下寫死會拿到空值。而且綁定表那個 `tenant_config_id` 欄位**實務上一律是 NULL**（寫入端從沒寫過它），所以主要靠 `(租戶, 工具)` 反查——那個組合有唯一約束，定位得了，且歷史 NULL 資料自動自癒、不必補資料 migration。**判定：設計合理。**
- **沒設帳密就提前擋下來**（`:219-226`）。註解的理由站得住：空帳密派下去代理程式必炸，而且錯在雲端卻只能從代理程式的 log 看到——提前擋並講清楚「要去設定工具憑證」，比讓它炸在遠端好查得多。零帳密工具有例外處理（否則一個合法狀態會永遠開始不了），且往嚴的方向失敗（查不到工具一律當作需要帳密）。**判定：方向正確。**

**流向有三條，兩條正確、一條是缺口：**

1. **→ 代理程式（心跳組裝當場解）**：✅ 正確。走 mTLS 通道下發，代理程式用完即丟不落地，不存在本地。這是設計上的正規路徑。
2. **→ API 回應**：✅ 有擋。`_visible_scan_params()`（`:2271`）把所有 `_` 開頭的欄位濾掉，且單筆與批次兩條讀取路徑**共用同一份剝除實作**——註解自己寫明「兩份剝除實作就是『這裡忘了剝』的溫床（憑證明文會落進 `api_logs.response`）」。這個共用是對的。
3. **→ 資料庫 `agent_tasks.params`**：❌ **缺口，就是 F1**。第 2 點的剝除只保護 API 出口，保護不到資料庫本身。

**一個容易誤判的地方要講清楚。** 同一個欄位裡還有另一種機敏資料——「任務層敏感參數」（例如被掃網站的登入密碼），那一類**在資料庫裡是密文**，因為它在綁定寫入時就被包成加密信封了（D3-2 查證過），心跳組裝時才解。所以 `agent_tasks.params` 裡面是**混的**：任務層那些是密文（安全），`_credentials` 這個鍵是明文（就是 F1）。看到「這欄位裡有密文」不代表整欄安全，兩者要分開看。

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

分兩層講：

**「報出來的這條存在嗎」——可信度高。** F1 的每一環首腦都開檔核對過，而且修法的成立性做了三個 repo 的交叉查證（雲端組裝端、代理程式讀取端、既有設計註解），三邊結論一致。三位檢查員從三個不同角度獨立追同一條鏈，三票全過。

**「只有這條嗎」——可信度中等，有四個已知限制。**

1. **強度是 `low`**，只有一位研究員讀一遍，不是多位分工交叉讀。低強度的設計目的是快速篩，不是窮盡。
2. **檔案大、強度低，這是本棒最該留意的一點。** 2,498 行由一位研究員一次讀完，是本 arc 目前單棒行數最高的一次（D3-2 是 1,328 行、D2 切成四小棒每棒約 1,600 行）。切成 4a／4b 是為了控制**首腦核對**的負荷，但**工具那一端並沒有跟著切**——它還是整檔讀。所以「前半 1,151 行被讀得多仔細」沒有獨立保證。
3. **重點①的「沒問題」是人工定向查證，不是面板投票的結果。** 工具對這一項一條候選都沒提，九支端點的守門是首腦逐支開檔比對出來的。這個結論沒有三票背書，可信度低於 F1 那條。查的是「有沒有呼叫守門函式」與「守門函式的判定邏輯對不對」，若有人用別的路徑繞過這整套（例如直接對資料庫下 SQL），本次查證看不到。
4. **F1 的修法沒有實跑驗證。** 「刪掉之後代理程式照樣拿得到帳密」是讀三個 repo 的程式碼推出來的，邏輯鏈完整但沒有真的派一張工單試。修的時候務必實測一次完整掃描（見 F1 的修法注意事項）。

`verification.status` 為 **`verified`**：三位檢查員對 3 條候選各投一票，**9 票全數投出，沒有漏投**，票數由工具自己的程式碼統計、不是任何 agent 自報。

## 執行概況

| 項目 | 數字 |
|---|---|
| run ID | `wf_3487f8c6-0ca` |
| 掃描 commit | `955e40964baa3cfc0e8766078db7d65075d3c50b`（工作區有其他未 commit 改動，與本棒無關） |
| 範圍 | 1 檔 2,498 行（本棒歸屬前 1,151 行） |
| 強度 | `low`（一位研究員 ＋ 三票面板） |
| 研究員 | 派 1 位、回 1 位、**零重試** |
| 面板 | 3 條候選 × 3 位檢查員 ＝ 9 票，全數投出 |
| 總耗時 | 約 102 分鐘（04:17 啟動 → 05:59 報告產出） |
| 候選 → 成立 | 3 → 3（零駁回） |
| 淨新增 | **1 條**（F2＝重複第 88 項、F3＝重複第 105 項） |
| 驗證輪數 | 1 |
| 驗證章 | `verified` |
| 工具原始產物 | `jedi-detection/CLAUDE-SECURITY-20260919-041735/`（不入版控） |

**三個觀察，留給後續切棒參考。**

1. **零重試，但耗時是 D3-2 的 1.9 倍不到。** 2,498 行對 1,328 行是 1.88 倍，102 分鐘對 140 分鐘反而更短——這一棒是**單獨跑的**（D3-2 是與另一支平行跑的）。單點樣本不足以下結論，但至少說明 2,500 行這個量級一位研究員扛得住，沒有觸發停滯判定。
2. **零駁回是本 arc 少見的。** 前幾棒常有候選被面板否決（D3-2 是 2 選 1）。這棒 3 條全過，三條也確實都經得起首腦複核——但其中兩條是前面已經記過的，**等於工具在同一個檔案裡重新發現了已知問題**。這是低強度整檔掃的必然副作用，不是工具變準了。
3. **「切棒」目前只切首腦這一端，工具那一端沒切。** 這是本棒暴露出來的機制落差（見上方可信度第 2 點）。若後續要讓行號分界真的生效，得想別的辦法（例如把檔案複製一份截斷後掃，或改用支援範圍的強度），現行做法只能控制核對負荷、控制不了研究深度。
