# W1 掃描報告 — 問卷的接線本體（CM-2090）

> 範圍：8 檔／934 行（`core/plugins/survey.py`、`infra/survey/adapters.py`、問卷三支 DI 容器檔、`di_containers/dashboard_apis/survey.py`、`config/socketio_namespaces.py`、接縫檔 `core/plugins/_host.py`）。
> 掃描工具：Claude Code 官方 `claude-security` plugin，effort low，focus 生產程式碼。
> 掃描基準 commit：`aa23a63554ce`（工作區有其他 session 的未提交改動）。
> 驗證章：**verified**（2 條候選，面板 6 票全數投出，1 條成立、1 條否決）。只掃不修。
> 本棒與 W2（CM-2091）由同一個 runner 連續跑，W2 報告會回頭引用本棒第 6 節的「守門零件清單」。

---

## 1. 一句話結論

**宿主遞給問卷套件的守門零件，本身都是對的；問題出在「遞的時候有兩份、只改了一份」。** 目前工作分支（`feature/review`）上，套件已經開始要求一個新零件「專案參與者檢查」，但宿主這邊兩處專案角色守門、和一處 DI 容器都還沒補上——修正版都在 `fix/security-b1` 分支上寫好了，**兩個分支合起來才對**（見 W1-2）。

工具這一棒找到一條低風險的真問題：**多人同時填問卷時，「現在誰在線上」那份名單是照前端送來的名字寫的，不是照登入身分寫的**，所以一個參與者可以把別人踢出名單、冒用別人的名字「佔住」某一題（W1-1）。

另外有一件事卡片沒問、但本棒讀到了：**問卷的「填答」那一半網址，沒有一支掛上商務授權檢查**；「設計問卷」那一半 33 支裡 32 支有掛（唯一沒掛的是不需登入的範例檔下載）。沒買問卷模組的客戶，只要知道網址，填答相關功能照樣能用（W1-3）。

卡片點名的其餘幾點都查過了：專案管理者守門是直接委派主專案的正牌實作、即時共編連線時有驗登入、AI 儀表板兩支查詢在修正分支上已補能力點檢查。逐條理由在第 5 節。

---

## 2. 這一棒在檢查什麼

問卷功能（設計問卷、派給任務填答、多人同時填）寫在 `jedi-survey` 套件裡，FR-109 兩棒已經掃完。這一棒看的是**主專案把套件接上產品的那一層**：套件自己不知道「誰登入了」「這家客戶買了什麼」「這個人在專案裡是什麼角色」，這些都由主專案遞給它。

要問的是：**主專案遞過去的守門零件對不對、齊不齊；套件以為主專案會守的地方，主專案真的有守嗎。**

問卷套件有三條被外面碰到的路：

| 路 | 誰會走 | 守門零件由誰給 |
|---|---|---|
| 網頁 API（設計問卷＋填答兩組網址） | 使用者在畫面上操作 | `core/plugins/survey.py` 的 `build_adapters()` |
| 即時共編（`/socket/fill-survey`） | 多人同時打開同一份問卷填答 | 連線驗身分：`core/app_factory.py` 接的 jedi-iam；房間權限：套件自己 |
| AI 儀表板（使用者用一句話要資料，AI 挑一支查詢來叫） | 使用者在 AI 儀表板打字 | `di_containers/dashboard_apis/survey.py` 的申報 |

---

## 3. 掃到什麼：總覽

| # | 這是什麼問題 | 出事會怎樣 | 要先有什麼才會發生 | 在哪裡 | 嚴重度 | 來源 |
|---|---|---|---|---|---|---|
| W1-1 | **多人填問卷的「誰在線上」名單，照前端送來的名字寫** | 能把填答人從名單踢掉、用同事的名字顯示「正在編輯第 5 題」讓那題在別人畫面上被鎖住；也能反覆塞超長字串把快取吃大 | ① 登入 ② 是該任務所屬專案的參與者（唯讀的檢視者也行） ③ 知道那份問卷的編號 | 名單寫入點 `infra/survey/adapters.py:36`（`join`）、`:39`（`leave`）；名字來源是套件的即時共編處理器（見 4.1） | **低** | 工具，3:0 成立 |
| W1-2 | **套件要一個新零件「專案參與者檢查」，目前分支上宿主兩處守門、一處容器都沒給** | 在目前分支上：問卷讀取、即時共編房間檢查一呼叫就出錯（不是放行，是整組壞掉）；在修正分支上已補齊 | 兩個分支沒有一起合併就上線 | `infra/survey/adapters.py:43`（`ProjectRoleGuardAdapter` 類別起點）<br>`core/plugins/participant.py:90`（`_ProjectRoleGuardAdapter` 類別起點）<br>`di_containers/task_survey/question_answer_containers.py:90`（`question_answer_history_detail_service`）<br>`di_containers/survey/survey_containers.py:116`（`survey_discussion_service`） | **不是漏洞**：失敗方向是「壞掉」不是「放行」；但合併順序錯就是整組問卷讀取壞 | runner 開檔，未經投票 |
| W1-3 | **問卷「填答」那一半 14 支網址，一支都沒掛商務授權檢查** | 沒買問卷模組的客戶，照樣能叫出任務問卷清單、讀寫答案、看歷史版本、匯入答案 | ① 登入 ② 通過該筆問卷本身的歸屬檢查（修正分支已補） ③ 客戶沒買問卷模組 | 套件 `jedi_survey/api/routes/task_survey_route.py:29`、`:40`、`:48`、`:63`、`:131`、`:171`；`question_answer_route.py:28`、`:37`、`:55`、`:84`；`question_answer_history_route.py:22`、`:32`、`:43`；`question_answer_history_detail_route.py:18`（每支方法起點，`@require_license("survey")` 該掛在這些方法上） | **低**：不是資料外洩（歸屬檢查仍在），是商務授權被繞過 | runner 開檔，未經投票 |

---

## 4. 每條發現的詳述

### 4.1 W1-1 多人填問卷時，「誰在線上」名單照前端送來的名字寫

**白話說明**

問卷有一個「多人同時填」的功能：幾個人打開同一份問卷時，畫面上會顯示「誰在線上」，某人點進一題開始打字時，別人畫面上那一題會被鎖住、顯示「某某正在編輯」。

這份線上名單存在快取（Redis）裡，由主專案的 `RedisPresenceStoreAdapter` 負責寫。問題是：**要寫進去的名字，是前端在訊息裡自己填的**（`data.get('user')`），不是伺服器從登入身分取的。伺服器其實早就知道你是誰——同一支處理器裡「更新答案」那個動作已經改成用登入身分署名（CM-2060 修的總表第 85 項），但「加入」「離開」「開始編輯」「結束編輯」這四個動作沒有跟著改。

**攻擊情境**

一位只有檢視權限的專案成員，打開某份任務問卷後，自己送一則「離開」訊息、名字填負責填答的同事——同事就從大家的線上名單裡消失。接著送「開始編輯」、名字填另一位同事、題目填第 5 題——其他人的畫面顯示「某某正在編輯第 5 題」並把那題鎖住。他也可以反覆送「加入」、每次帶一段很長的新名字，名單沒有過期時間也沒有上限，會一直長下去（而這台 Redis 同時也在跑即時訊息的佇列）。

**不會發生的事**：答案本身不會被改到、署名不會被偽造——寫答案那條路用的是登入身分，而且有「必須是填答人或專案管理者」的檢查。

**為什麼是低不是中**：要先是那個專案的成員才進得了房間；造成的是畫面上的誤導與干擾，資料不受影響。

**在哪裡**

| 位置 | 是什麼 |
|---|---|
| `infra/survey/adapters.py:36` | `RedisPresenceStoreAdapter.join`：把名字 `lpush` 進名單，無過期、無上限 |
| `infra/survey/adapters.py:39` | `RedisPresenceStoreAdapter.leave`：把名字從名單移除 |
| 套件 `jedi_survey/app/handler/fill_survey_socketio_handler.py:76`（`on_join`）、`:124`（`on_leave`）、`:207`（`on_edit`）、`:229`（`on_endedit`） | 四支事件處理器的起點：名字取自 `data.get('user')`，**該改成取登入身分的位置** |
| 同檔 `:164`（`on_update`） | 對照組：這支已改成 `get_user_context().login_name`，是正確寫法 |

**怎麼修**

1. 套件側：四支事件處理器一律用 `get_user_context().login_name`，忽略前端送來的 `user`（照 `on_update` 的寫法，一行一支）。
2. 宿主側（`infra/survey/adapters.py`）：`join` 時給名單鍵設過期時間，並限制名單長度。
3. 可討論：「開始編輯／結束編輯」會鎖別人的題目，要不要限定只有填答人或管理者才能送（目前任何參與者都行）。

**runner 核對註記**：三個獨立檢查員從「打不打得到」「出事多嚴重」「有沒有其他機制擋」三個角度投票，**三票全部認定成立、三票都評低**。我另外開套件原始碼核對：本機環境載入的是修正分支上的套件（`jedi-wt-fix-security` worktree），`on_join` 在第 82 行、`on_leave` 在第 130 行取 `data.get('user')`，屬實；已發版的 jedi-survey 1.1.2 這幾支更早（連房間檢查都還沒有，那是總表第 51 項），修正分支上的房間檢查也不會蓋掉這條。

**跟總表的關係**：總表第 85 項（M05-9）是「更新答案時署名可偽造」，CM-2060 只修了 `on_update` 那一支。這一條是**同一個病的另外四支**——典型的「守了一半」。建議併進同一張修正卡的延伸。

---

### 4.2 W1-2 套件要一個新零件，目前分支上宿主沒給（修正分支已給）

**白話說明**

問卷套件判斷「你能不能看這份任務問卷」時，會去問主專案：「這個人是不是這個專案的參與者？」這個「問」的動作走的是一個叫 `IProjectRoleGuard` 的接頭，主專案要實作兩個方法：`assert_manager`（是不是管理者）和 `assert_participant`（是不是參與者）。後者是 FR-114.2-4 新加的。

主專案有**兩處**實作這個接頭：`core/plugins/participant.py` 的 `_ProjectRoleGuardAdapter`，和本棒範圍內的 `infra/survey/adapters.py` 的 `ProjectRoleGuardAdapter`。它們寫進同一個全域位置，**後掛載的會蓋掉先掛載的**——而問卷排在人員指派之後，所以實際生效的是問卷那一份。

在目前工作分支 `feature/review` 上：

- 兩處實作都**只有** `assert_manager`、沒有 `assert_participant`。
- `question_answer_history_detail_service`（歷史版本明細）和 `survey_discussion_service`（問卷討論串）兩支 service，套件已改成需要 `task_survey_domain_service`、`task_assignee_domain_service`、`participant_role_service` 才能做歸屬檢查，宿主的 DI 容器還沒注入。

修正分支 `fix/security-b1` 上（commit `05870c4cc`、CM-2036；以及 CM-2034 的容器補丁），**四處都已補齊**，兩處 adapter 都委派主專案的 `common.authz.project.assert_project_participant`。

**為什麼不是漏洞**：少了這個方法，套件一呼叫就會拋「沒有這個方法」的錯誤，使用者看到的是「讀不到」而不是「看到別人的」。少了容器注入，套件那端的守門會因為拿不到反查資料而**一律擋下**（`assert_task_survey_reader` 查不到專案就拋 403）。兩者都是往「壞掉」的方向失敗，不是往「放行」。

**為什麼還是要列**：本機環境現在載入的是修正分支的套件＋目前分支的主專案程式碼——**這個組合正是會壞的那一種**。上線時如果套件先發版、主專案的 CM-2036 還沒合，或反過來，問卷的讀取與即時共編會整組壞掉。

**在哪裡**（該補的位置）

| 位置 | 目前分支缺什麼 | 修正分支 |
|---|---|---|
| `infra/survey/adapters.py:43`（`ProjectRoleGuardAdapter` 類別） | 缺 `assert_participant` 方法 | ✅ 已補（第 58 行） |
| `core/plugins/participant.py:90`（`_ProjectRoleGuardAdapter` 類別） | 缺 `assert_participant` 方法 | ✅ 已補（第 104 行） |
| `di_containers/task_survey/question_answer_containers.py:90`（`question_answer_history_detail_service`） | 只注入一個參數，缺四個 | ✅ 已補 |
| `di_containers/survey/survey_containers.py:116`（`survey_discussion_service`） | 缺三個參數 | ✅ 已補 |

**怎麼處理**：不需要另開修正卡，只要確認**套件發版與主專案 `fix/security-b1` 合併是同一批**。另外建議：兩處 adapter 實作同一個接頭、而且互相覆蓋，這本身就是「同一件事兩份」的形狀，下次加第三個方法時一樣要記得改兩處。可以考慮讓 `core/plugins/survey.py` 直接用人員指派那一份，刪掉問卷自己那份（要動 `infra/survey/adapters.py` 被別層 import 的例外，見該檔開頭）。

---

### 4.3 W1-3 問卷「填答」那一半網址，全都沒掛商務授權檢查

**白話說明**

這個產品是按模組賣的，「問卷」是加購包。主專案給套件一個 `license_guard`（`require_license("survey")`），套件應該把它掛在每一支問卷網址上：沒買問卷的客戶按下去就回「未授權」。

我逐支數過套件的網址檔：

| 網址檔 | 方法數 | 掛了商務授權 | 掛了能力點 |
|---|---|---|---|
| `survey_route.py`（設計問卷） | 13 | 12 | 8 |
| `survey_folder_route.py`（問卷資料夾） | 6 | 6 | 3 |
| `survey_page_route.py`、`survey_question_route.py`（頁、題目） | 10 | 10 | 10 |
| `survey_discussion_route.py`（討論串） | 4 | 4 | 2 |
| **`task_survey_route.py`（任務問卷）** | **6** | **0** | 0 |
| **`question_answer_route.py`（填答）** | **4** | **0** | 0 |
| **`question_answer_history_route.py`（填答歷史）** | **3** | **0** | 0 |
| **`question_answer_history_detail_route.py`（歷史明細）** | **1** | **0** | 0 |

`survey_route.py` 唯一沒掛的那一支是「下載問卷上傳範例檔」，走的是簽章網址，不需要登入，合理。

**設計層 33 支掛了 32 支、作答層 14 支全沒掛**——這不是漏一支，是整個作答層都沒有。能力點沒掛是刻意的（作答層用「是不是這個任務的填答人或專案成員」判斷，不看角色能力點），但商務授權沒有理由不掛：問卷模組沒買，就不該能填問卷。

**會怎樣**：沒買問卷模組的客戶，前端選單不會出現問卷（FR-062 T-8.1 會把選單反灰），但直接打網址照樣能用：列出任務問卷、讀寫答案、看歷史版本、匯入答案。**不會看到別人的資料**——每一支都還有「這筆問卷歸屬」的檢查（修正分支已補），所以這是「商務授權被繞過」，不是資料外洩。

**為什麼是低**：客戶要自己知道網址、而且裡面要本來就有問卷資料（沒買問卷模組的客戶通常不會有任務問卷可以填）。

**宿主側有沒有擋**：沒有。主專案的全域「唯讀閘門」（`common/middleware/license_readonly_mw.py`）只管「照過期變唯讀時擋寫入」，不管「有沒有買這個模組」。

**在哪裡**：套件四支網址檔、共 14 支方法的起點（行號見總覽表）。修法是每支方法加 `@require_license("survey")`，放在 `@jwt_required()` 之下（和設計層同一個寫法）。

**runner 核對註記**：這條工具沒報，是我數網址檔時看出來的。FR-109 兩棒只查了「讀取有沒有歸屬檢查」，沒有逐支數商務授權。套件總表 M05 也沒有這一條。**需要首腦判斷是否登記為新項次**（見第 8 節）。

---

## 5. 卡片點名要追的問題：逐條結論

**① `ProjectRoleGuardAdapter.assert_manager` 是直接委派還是另寫一套？`group_id`／`control_id` 有沒有用到？**
**直接委派，沒有另寫。** `infra/survey/adapters.py:51-56` 原封不動把四個參數轉給 `common.authz.project.assert_project_manager`，那支是主專案的正牌實作（由下往上查控制項→群組→專案三層角色）。`group_id`／`control_id` 在接頭上**有接、有轉**，但**問卷套件自己呼叫時從來不傳**（`task_survey_guard.py:76`、`:84` 只帶專案編號）——所以實際只判專案層。這是正確的：問卷掛在任務上、任務屬於專案，不屬於某個控制項，沒有更細的層級可以判。**不成立。** 唯一要注意的是 W1-2 講的「少了一個方法」。

**② 宿主給套件的 `capability_required`、`license_guard` 有沒有給對？**
**給對了。** `core/plugins/survey.py:97-113` 兩支都是傳「工廠函式本身」，對應 `common.authz.require_license(resource_type)` 與 `require_capability(*names)`，簽名與套件 `api/guards.py:82-122` 的呼叫方式吻合（我在本機實際 import 核對過簽名）。套件在掛載前會檢查四樣守門零件（`auth_required`、`license_guard`、`capability_required`、`project_role_guard`）是否齊全，缺一就拒絕掛載（`plugin/assembly.py:19-33`），宿主四樣都給了。**宿主這側不成立**；套件拿到之後有一半沒套用，見 W1-3。

**③ 即時共編 socket 連線時有沒有驗登入？**
**有。** 問卷的即時共編處理器繼承 jedi-iam 的 `AuthenticatedNamespace`，連線時用與網頁同一把 JWT 驗簽、驗期、查登出黑名單、查使用者、查租戶隸屬，任一不過就拒絕連線。宿主在 `core/app_factory.py:334-336` 接上了這兩個查詢零件；**沒接的話是拒連，不是放行**（jedi-iam `socketio_auth.py:138`）。已發版的 jedi-survey 1.1.2 也已經繼承這個類別。**不成立。**

順帶一提：`config/socketio_namespaces.py:43-46` 登記的**另一條** socket `/socket/notification`（流程討論區的即時重新整理）是**完全不驗登入**的普通 Namespace。工具把它列為候選，三個檢查員**一致否決**：這條只轉發「請重新整理」的訊號、不帶任何資料，前端收到後會自己再打一支有登入檢查的網址去拿內容。所以無法讀到任何東西，最多讓別人的討論區多重新整理一次。**記錄在此，下次不用重查。**

**④ AI 儀表板兩支問卷查詢，空條件時回什麼？**
- **目前分支 `feature/review`**：兩支申報都**沒有**寫「需要什麼能力點」，而目前安裝的 jedi-ai-dashboard 會對「沒寫」的查詢一律拒絕（fail-closed）——所以這兩支在目前分支上**根本叫不動**。已發版的 jedi-ai-dashboard 1.2.0 沒有這層檢查，**任何登入者都能叫**，空條件時 `get_surveys` 回整個客戶所有未刪除的問卷（**含流程自動產生的快照問卷**，網頁那支會排除、這支不會）、`get_folders` 回全部資料夾（系統資料夾會排除）。這就是總表第 60 項（M15-2）。
- **修正分支 `fix/security-b1`**（commit `8f94e249d`、CM-2038）：兩支都補上 `required_capabilities=("survey.read",)`，並接上 `viewer_has_capability` 判定。修好後只有持有問卷讀取能力點的人叫得動。
- **剩下的差異**：問卷設計（題庫本身）在網頁那側也只要求登入＋買了問卷模組、**不要求** `survey.read` 能力點，所以 AI 儀表板這條修好後反而比網頁嚴格，不是比較鬆。快照問卷會多列出來這點，只是多看到自己客戶的內部副本，不是跨客戶。
- **AI 儀表板本身有掛商務授權嗎？** 有，但掛的是 `ai-dashboard` 模組，**不是 `survey`**。所以買了 AI 儀表板、沒買問卷的客戶，還是能透過 AI 儀表板查到問卷清單。跟 W1-3 同一類問題（商務授權被繞過），**併進 W1-3 討論**。
- **結論**：主問題由總表第 60 項涵蓋、修正分支已修，**不另計**。

**⑤ `_host.py` 的 `host_defaults()` 給出去哪幾樣？**
四樣：`auth_required`（`jwt_required()`）、`capability_required`（`require_capability`）、`current_user_login_name`、`response_builder`。**問卷接線沒有用它**，而是自己逐項填（`core/plugins/survey.py:128-141`），填的內容與 `host_defaults()` 的前兩樣完全相同，外加 `license_guard` 與 `project_role_guard`（`host_defaults()` 沒有這兩樣）。只有資產、公告、議題三支用 `**host_defaults()` 整包吃。W6 需要知道的事：**`host_defaults()` 不含商務授權與專案角色守門**，所以「吃整包就等於守門給齊」這個假設不成立，每支要自己補。

---

## 6. 守門零件清單（給 W2 對照用）

W2 要回答「W1 看到的守門零件，在 W2 那條路上有沒有被用到」，所以把本棒確認過的零件列成一張表：

| 零件 | 宿主在哪裡給 | 實作 | 用在哪 |
|---|---|---|---|
| 登入檢查 `auth_required` | `core/plugins/survey.py:130` | `jwt_required()` | 套件每支網址 |
| 商務授權 `license_guard` | `core/plugins/survey.py:97`、`:131` | `common.authz.require_license` | 套件設計層 32 支（作答層 0 支，W1-3） |
| 能力點 `capability_required` | `core/plugins/survey.py:104`、`:132` | `common.authz.require_capability` | 套件設計層寫入類 23 支 |
| 專案角色守門 `project_role_guard` | `core/plugins/survey.py:133`（實作在 `infra/survey/adapters.py:43`） | 委派 `common.authz.project.assert_project_manager`（修正分支加 `assert_project_participant`） | 套件 `task_survey_guard.py` 的三支守門：填答者、讀者、管理者 |
| 任務編號換算 `task_uid_resolver` | `core/plugins/survey.py:139`（實作 `infra/survey/adapters.py:65`） | 查流程引擎的 `workflow_executions`／`job_executions` | 套件用任務編號查問卷時（反查不到回空，不退化成不過濾） |
| 填答歸屬反查 | `di_containers/task_survey/task_survey_containers.py:52`、`question_answer_containers.py:77`、`:87` 注入 `task_assignee_domain_service` ＋ `participant_role_service` | 查 `task_assignees` 找出任務屬於哪個專案 | 套件守門判斷「你是不是填答人／專案成員」 |
| 即時共編連線身分 | `core/app_factory.py:336` | jedi-iam `AuthenticatedNamespace` | `/socket/fill-survey` 每個事件 |

**要特別留意的一件事**：`TaskUidResolverAdapter` 只負責「換算」，**不做任何權限判斷**。它能把任何存在的任務編號換成內部編號，不問你是誰。這沒問題——權限判斷在套件的 `task_survey_guard.py`，靠的是 `task_assignees` 表反查專案。**但這代表：誰能在 `task_assignees` 裡寫一列，誰就能決定一份問卷屬於哪個專案。** 問卷怎麼被掛上任務、那一列怎麼來的，是 W2 要查的。

---

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

**「這幾條存在嗎」——W1-1 可信度高；W1-2、W1-3 是開檔看到的事實。**

- W1-1：三個獨立檢查員各投一票，**3:0 成立**，嚴重度三票都評低。我另外開套件原始碼逐行核對屬實。
- W1-2：兩個分支的差異我用 `git show` 逐檔比對過，行號在修正分支上都找得到。沒有實際啟動程式驗證「會 AttributeError」，是依程式碼推斷。
- W1-3：網址檔逐支數過（上方表格），`@require_license` 有沒有掛是機械可查的事實。「沒買問卷的客戶真的打得到」是依程式碼推斷，**沒有實際用一個沒買問卷的測試租戶去打**。

**「只有這幾條嗎」——不保證。**

1. 這次用的是**最快的掃描檔位**（effort low）：一輪研究員加一輪投票，沒有跑威脅建模與廣度掃描。
2. W1-2、W1-3 **工具沒有報**，未經三人面板投票，是 runner 自行開檔核對的。
3. 本機環境的套件是**修正分支的 worktree**（`jedi-wt-fix-security`，editable 安裝），工具與我讀到的套件程式碼都是修正版，不是已發版的 1.1.2。凡是提到「已發版」的都是我另外用 `git show` 對照的。
4. 全程沒有打任何網址、沒有連 Redis、沒有連資料庫。

---

## 8. 執行概況（數字，給工程師看）

| 項目 | 數值 |
|---|---|
| 掃描範圍 | 8 檔 / 934 行 |
| 基準 commit | `aa23a63554ce`（工作區 dirty，有平行 session 改動） |
| 檔位 | effort low，focus 生產程式碼（含密鑰專項） |
| 研究員 | 派 2 支，回 2 支 |
| 原始候選 | 2 條，去重後 2 條 |
| 投票 | 三個檢查員 × 2 條 = 6 票，全數投出 |
| 票型 | W1-1（工具編號 F1）3:0 成立，三票評低；`/socket/notification` 未驗登入（F2）0:3 否決 |
| 驗證章 | **verified**（`CLAUDE-SECURITY-REVISION-aa23a63554ce-dirty.json`） |
| 工具 run ID | `wf_0a88cfa7-e8f` |
| 耗時 | 約 52 分鐘（8 個 agent，零失敗） |
| 工具產出原始報告 | `CLAUDE-SECURITY-20260923-130757/`（未入版控） |
| 密鑰專項 | 無新發現 |

---

## 9. 待首腦裁決

1. **W1-1 要不要登記成總表新項**，還是當作第 85 項（M05-9）的延伸。建議**當延伸**：同一支檔、同一個病（前端送的名字直接用），CM-2060 修了五支裡的一支。修正卡要把 `on_join`、`on_leave`、`on_edit`、`on_endedit` 四支逐一列成驗收項，外加宿主 `infra/survey/adapters.py` 名單加過期與上限。
2. **W1-3 要不要登記成總表新項**（建議要：作答層 14 支全缺商務授權、AI 儀表板查問卷也只驗 `ai-dashboard` 不驗 `survey`，是一整組；總表目前沒有）。修法是套件側加 decorator，不動主專案。
3. **W1-2 的合併順序**：套件 jedi-survey 發版要和主專案 `fix/security-b1` 的 CM-2036、CM-2034 容器補丁同一批上。要不要在修正分支的合併檢查清單上寫明。
4. **兩份 `IProjectRoleGuard` 實作互相覆蓋**（`core/plugins/participant.py` 與 `infra/survey/adapters.py`）要不要收成一份。目前靠「兩邊都記得改」維持一致，CM-2036 已經踩過一次。

---

沿革見 `FR-115` LOG 與 git log。
