# FR-048 Phase 2b-4 交接 prompt（裸奔稽核後的寫入面真漏網收口）

> 給執行 session 的 prompt。前置:FR-048 主體已完工(SUMMARY `2026-07-07-FR-048-SUMMARY.md`)。
> 本批是**收尾後的裸奔稽核**(5 群 subagent + 主導逐條驗 code)挖出的**真漏網**——
> 大多數 agent 報的裸奔是誤判(只看 route 沒追 service 層守門),以下是通過驗證的真缺口。

---

【接手主題】FR-048 Phase 2b-4 — 收掉裸奔稽核確認的 4 個寫入面真漏網

必讀:
1. `docs/features/FR-048-2607-unified-auth-guard/endpoint-authz-matrix.md`(軸模型、既有守門 pattern)
2. `common/authz/`(canonical:`assert_project_manager` / `assert_project_participant` / `require_capability`)

【branch】fix/v1.8.0-bugs,禁止切 branch。不動 jedi-* 套件。守門一律 `common.authz` canonical。

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
【鐵則:先驗 live 再改(每條都要)】

修 bug 前先驗真實狀態(DB/FE/呼叫路徑),不要憑本 prompt 直接刻。每條的驗證步驟已列在下方。

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
## 1. task_survey 填答 7 條(最大真漏網,軸③)

**現況(已驗)**:`app/task_survey/service/question_answer_service.py`(update_task_survey_answer:61 / checkpoint:168 / patch:314)、`task_survey_service.py`(update_task_survey:71 / update_task_survey_configure:529)、question_answer_history revert——**service 完全無身分檢查**,任何登入者可改**任何人**的填答。route 只有 @jwt_required。

**定調(D10)**:填答 = **被指派者本人(assignee)**;configure/管理類 = manager。

**先驗**:
- FE 怎麼呼叫這些端點(哪個角色在用填答頁)——`compliance-manager-fe` grep 對應 api。
- task_survey → 如何拿到 assignee / project:service 已注入 `task_assignee_domain_service`(task_survey_service.py:37),grep 既有 `TaskAssigneeQueryEntity` 用法找反查(**禁重複造輪**)。task_survey 有 task_id → task_assignees 有 project_id + user_id。

**改**:
- 填答類(answer update/patch/checkpoint/revert):驗「current user 是該 task 的 assignee 本人」——查 task_assignees 有無 (task_id, current_user_id) 這列;無 → `ForbiddenError`。若 D10 允許 manager 代填,manager 也放行(比照 AR override)。
- configure(update_task_survey_configure)、update_task_survey:manager 級 → resolve project 後 `assert_project_manager`。
- 新增對應 error code(task_survey 模組碼,或復用 `GRC_NOT_PROJECT_PARTICIPANT`)。

## 2. poam update_poam(silent 漏網,矩陣沒記,軸③ manager)

**現況(已驗)**:`api/grc/routes/poam_route.py:73` PUT 呼叫 **`app/grc/service/poam_service.py:96 update_poam`**——此 service **零守門**(`grep assert_project_manager` = 0)。注意這跟**已守**的 `poam_app_service.py`(v2,有 `_check_manager`)是**兩個不同 service**,route 用的是沒守的這個。

**先驗**:
- 確認 `poam_route.py` 這條 PUT 是 **live**(FE 有在用?還是 v1 殘留)——grep FE + 看 `api/grc/__init__.py` 有無註冊。若 dead 就標 dead 不改。
- poam_service.update_poam 的 `ap_uid` → 怎麼 resolve project_id(看 poam_app_service 或 audit_round 既有反查)。

**改**(若 live):update_poam 開頭 resolve project → `assert_project_manager`。若同 service 有其他寫入 method(create/delete poam)一併檢視補齊。

## 3. report file-list 對齊 preview(軸②)

**現況(已驗)**:`api/report/routes/scheduler_report_route.py` — preview(:69)有 `@require_capability("report.read")`,但 **file-list(:44)POST 只有 @jwt_required**,不一致。路徑已消毒(`_safe_report_path`),風險低但該對齊。

**改**:file-list 掛同款 `@require_capability("report.read")`(或與 preview 一致的守門)。

## 4. ⚠️ report.read 能力點是否存在(可能是 4a 的 latent bug)

**疑點**:`report.read` 在**任何 migration seed 都找不到**(`grep -rn "report" scripts/sql/*.sql` 無)。若它**不在 DB 既有 121 顆 capability 裡**,則 preview(及本批要加的 file-list)的 `@require_capability("report.read")` 會**把 super_admin 以外所有人擋掉**(break-glass 才進得去)= 現行 preview 可能已壞。

**先驗(DEV DB)**:
```
SELECT name, is_platform FROM public.capabilities WHERE name LIKE 'report%';
```
(用 cmmgr,連線資訊查 `.env` DB_SECRET / CLAUDE.md DB 環境清單;跑 SQL 前正確 source .env)

**處置**:
- 若 `report.read` **存在** → 沒事,file-list 直接掛。
- 若**不存在** → 這是 4a 換裝挑錯名字的後遺症。二擇一:①改用一顆確實存在的 capability(grep 4a 換裝紀錄 §6.3 看 report 當初對到什麼);②seed 一顆 `report.read`(幂等 migration,配 Administrator,只套 DEV,STG/POC 列交付)。**先回報 user 選哪個,別自己拍板 seed**。

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
【驗證 + 收尾】

- 每條補單測(非授權 403 / 授權放行;task_survey 用 assignee mock)。
- **回歸**:baseline worktree 集合 diff(起點 commit 開 worktree 跑 `python -m pytest test/ -q --continue-on-collection-errors`,comm 比對,新增失敗 0)。
- 矩陣加一段「§10 裸奔稽核後補洞」記這 4 條 + 銷帳;順手把稽核的**誤判澄清**(flow_engine 12/oscal 13/cloud 4/participant 那批其實已守)寫進矩陣備查,避免下次又被當缺口重查。
- commit 按條分批(顯式 add、禁 -am、不 push)。收尾:一句話 status + 手測 checklist(含 report.read 定調項),收尾動作等 user 下令。

【手測 checklist 必含】非 assignee 帳號改別人填答 → 403;assignee 本人 → 正常;report 預覽/檔案清單 admin 能開、一般帳號行為一致。
