# FR-048 Phase 2a — 授權守門盤點：participant / remote_agent

> 事實盤點，不下判斷。分支 `fix/v1.8.0-bugs`。所有 route 前綴 `/api/1.0`。
> `assert_project_manager` 定義於 `common/util/participant_guard.py`（route 提示的 `common.authz.project` 尚未遷入，現況仍 `common.util.participant_guard`）。

## api/participant

**重要脈絡**：`api/participant/__init__.py` 內多個 resource 已被註解停用（FR-038 DEAD-V1，2026-06-18，FE 無 caller）。停用者標「路由已註解」。仍掛載的 live 寫入端點：`/task-assignees/batch`(POST)、`/task-assignee`(POST/PUT/DELETE)、`/project-participant`(POST/PUT/DELETE)、`/project-control-participant`(POST/PUT/DELETE)、`/control-group-participant`(POST/PUT/DELETE)。

| Route path | Method | Route file:line | @jwt_required? | 呼叫的 service method | 現況守門 | 備註 |
|---|---|---|---|---|---|---|
| /api/1.0/task-assignees | POST | api/participant/routes/task_assignee_route.py:49 | 是 | `TaskAssigneeService.get_task_assignees` | 僅 JWT | POST 實為分頁查詢(讀) |
| /api/1.0/task-assignees/batch | POST | api/participant/routes/task_assignee_route.py:24 | 是 | `TaskAssigneeService.batch_add_task_assignees` | 僅 JWT | 寫入端點但 service 為 FR-038 2A 死碼 stub，直接 `return []`（app/participant/service/task_assignee_service.py:173-188），無守門也無實際寫入 |
| /api/1.0/task-assignee | POST | api/participant/routes/task_assignee_route.py:65 | 是 | `TaskAssigneeService.add_task_assignee` | 已有: `assert_project_manager` @ app/participant/service/task_assignee_service.py:142 | role level: project manager，以 `control_id` scope（enforce_role 預設 True）|
| /api/1.0/task-assignee | PUT | api/participant/routes/task_assignee_route.py:80 | 是 | `TaskAssigneeService.update_task_assignee` | 已有: `assert_project_manager` @ app/participant/service/task_assignee_service.py:212 | role level: project manager，control_id scope |
| /api/1.0/task-assignee | DELETE | api/participant/routes/task_assignee_route.py:103 | 是 | `TaskAssigneeService.delete_task_assignee` | 已有: `assert_project_manager` @ app/participant/service/task_assignee_service.py:249 | role level: project manager，control_id scope |
| /api/1.0/project-participants | POST | api/participant/routes/project_participant_route.py:61 | 是 | `ProjectParticipantService.get_project_participants` | 僅 JWT | POST 實為分頁查詢(讀) |
| /api/1.0/project-participant | POST | api/participant/routes/project_participant_route.py:78 | 是 | `ProjectParticipantService.add_project_participant` | 已有: `assert_project_manager` @ app/participant/service/project_participant_service.py:83 | role level: project manager（無 group/control scope）|
| /api/1.0/project-participant | PUT | api/participant/routes/project_participant_route.py:95 | 是 | `ProjectParticipantService.update_project_participant` | 已有: `assert_project_manager` @ app/participant/service/project_participant_service.py:125 | role level: project manager |
| /api/1.0/project-participant | DELETE | api/participant/routes/project_participant_route.py:109 | 是 | `ProjectParticipantService.delete_project_participant` | 已有: `assert_project_manager` @ app/participant/service/project_participant_service.py:144 | role level: project manager |
| /api/1.0/project-control-participants | POST | api/participant/routes/project_control_participant_route.py:59 | 是 | `ProjectControlParticipantService.get_control_participants_with_inherits[_by_uid]` | 僅 JWT | POST 實為分頁查詢(讀) |
| /api/1.0/project-control-participant | POST | api/participant/routes/project_control_participant_route.py:89 | 是 | `ProjectControlParticipantService.add_project_control_participant` | 已有: `assert_project_manager` @ app/participant/service/project_control_participant_service.py:66 | role level: project manager，scope=group_id+control_id |
| /api/1.0/project-control-participant | PUT | api/participant/routes/project_control_participant_route.py:114 | 是 | `ProjectControlParticipantService.update_project_control_participant` | 已有: `assert_project_manager` @ app/participant/service/project_control_participant_service.py:110 | role level: project manager，scope=group_id+control_id |
| /api/1.0/project-control-participant | DELETE | api/participant/routes/project_control_participant_route.py:139 | 是 | `ProjectControlParticipantService.delete_project_control_participant` | 已有: `assert_project_manager` @ app/participant/service/project_control_participant_service.py:143 | role level: project manager，scope=group_id+control_id |
| /api/1.0/control-group-participant | POST | api/participant/routes/control_group_participant_route.py:78 | 是 | `ControlGroupParticipantService.add_control_group_participant` | 已有: `assert_project_manager` @ app/participant/service/control_group_participant_service.py:87 | role level: project manager，scope=group_id |
| /api/1.0/control-group-participant | PUT | api/participant/routes/control_group_participant_route.py:101 | 是 | `ControlGroupParticipantService.update_control_group_participant` | 已有: `assert_project_manager` @ app/participant/service/control_group_participant_service.py:126 | role level: project manager，scope=group_id |
| /api/1.0/control-group-participant | DELETE | api/participant/routes/control_group_participant_route.py:124 | 是 | `ControlGroupParticipantService.delete_control_group_participant` | 已有: `assert_project_manager` @ app/participant/service/control_group_participant_service.py:156 | role level: project manager，scope=group_id |
| (control-group-participants) | POST | api/participant/routes/control_group_participant_route.py:51 | 是 | `ControlGroupParticipantService.get_control_group_participants_with_inherits[_by_uid]` | 僅 JWT | POST 實為分頁查詢(讀)；**路由已註解**（__init__.py:64）|
| (control-group-participants/menu) | GET | api/participant/routes/control_group_participant_route.py:27 | 是 | `ControlGroupParticipantService.get_control_group_participant_menu[_by_uid]` | 僅 JWT | 讀取 menu；**路由已註解**（__init__.py:65）|
| (process-participant) | POST | api/participant/routes/process_participant_route.py:57 | 是 | `ProcessParticipantService.add_process_participant` | 已有: `assert_project_manager` @ app/participant/service/process_participant_service.py:65 | role level: project manager，control_id=process_id；**路由已註解**（__init__.py:50）|
| (process-participant) | PUT | api/participant/routes/process_participant_route.py:70 | 是 | `ProcessParticipantService.update_process_participant` | 已有: `assert_project_manager`（經 `_assert_manager_for_process`）@ app/participant/service/process_participant_service.py:90→57 | role level: project manager，反解 record.project_id；**路由已註解** |
| (process-participant) | DELETE | api/participant/routes/process_participant_route.py:86 | 是 | `ProcessParticipantService.delete_process_participant` | 已有: `assert_project_manager`（經 `_assert_manager_for_process`）@ app/participant/service/process_participant_service.py:111→57 | role level: project manager；**路由已註解** |
| (process-participants) | POST | api/participant/routes/process_participant_route.py:40 | 是 | `ProcessParticipantService.get_process_participants_with_inherits` | 僅 JWT | POST 實為分頁查詢(讀)；**路由已註解**（__init__.py:48）|
| (process-participants/menu) | GET | api/participant/routes/process_participant_route.py:25 | 是 | `ProcessParticipantService.get_process_participant_menus` | 僅 JWT | 讀取 menu；**路由已註解**（__init__.py:49）|
| (project-group-participant) | POST | api/participant/routes/project_group_participant_route.py:78 | 是 | `ProjectGroupParticipantService.add_project_group_participant` | 僅 JWT | **無 assert_project_manager**（service 全檔無守門，app/participant/service/project_group_participant_service.py:67-92）；**路由已註解**（__init__.py:72）|
| (project-group-participant) | PUT | api/participant/routes/project_group_participant_route.py:101 | 是 | `ProjectGroupParticipantService.update_project_group_participant` | 僅 JWT | **無 assert_project_manager**（同上 service:95-119）；**路由已註解** |
| (project-group-participant) | DELETE | api/participant/routes/project_group_participant_route.py:124 | 是 | `ProjectGroupParticipantService.delete_project_group_participant` | 僅 JWT | **無 assert_project_manager**（同上 service:122-136）；**路由已註解** |
| (project-group-participants) | POST | api/participant/routes/project_group_participant_route.py:51 | 是 | `ProjectGroupParticipantService.get_project_group_participants_with_inherits[_by_uid]` | 僅 JWT | POST 實為分頁查詢(讀)；**路由已註解**（__init__.py:70）|
| (project-group-participants/menu) | GET | api/participant/routes/project_group_participant_route.py:27 | 是 | `ProjectGroupParticipantService.get_project_group_participant_menu[_by_uid]` | 僅 JWT | 讀取 menu；**路由已註解**（__init__.py:71）|
| /api/1.0/project-participants/menu | GET | api/participant/routes/project_participant_route.py:25 | 是 | `ProjectParticipantService.get_project_participant_menus[_by_uid]` | 僅 JWT | 讀取 menu（live）|
| (project-participant/permission/&lt;project_id&gt;) | GET | api/participant/routes/project_participant_route.py:45 | 是 | `ProjectParticipantService.get_project_participants_permissions` | 僅 JWT | 讀取；**路由已註解**（__init__.py:43）|
| /api/1.0/project-control-participants/menu | GET | api/participant/routes/project_control_participant_route.py:28 | 是 | `ProjectControlParticipantService.get_control_participant_menu[_by_uid]` | 僅 JWT | 讀取 menu（live）|
| (enums/participant-roles) | GET | api/participant/routes/participant_enum_route.py:19 | 是 | 無（直接 enum dump） | 僅 JWT | 靜態 enum；**路由已註解**（__init__.py:76）|

## api/remote_agent

**重要脈絡**：FR-039 File Agent registry。程式碼註解多處寫「需 admin」（EnrollToken / Revoke / Rebind 等），但**追進 service 層後，所有 FE 端點皆無任何 admin / role / is_admin 守門**——僅 `@jwt_required()`。控制面端點（`/agents/register`、`/agents/heartbeat`）**刻意無 JWT**（agent→cloud，走 token+mTLS，非 user JWT）。`agent_enrollment_service.py:186` 的 `ForbiddenError` 是 agent status=revoked 的業務拒絕，**非授權守門**。

| Route path | Method | Route file:line | @jwt_required? | 呼叫的 service method | 現況守門 | 備註 |
|---|---|---|---|---|---|---|
| /api/1.0/remote-agents | GET | api/remote_agent/routes/remote_agent_route.py:34 | 是 | `RemoteAgentService.list_enabled_agents` | 僅 JWT | 下拉清單(讀) |
| /api/1.0/remote-agents | POST | api/remote_agent/routes/remote_agent_route.py:47 | 是 | `RemoteAgentService.create_agent` | 僅 JWT | 寫入；registry 新增，無 admin 守門 |
| /api/1.0/remote-agents/all | GET | api/remote_agent/routes/remote_agent_route.py:88 | 是 | `RemoteAgentService.list_all_agents` | 僅 JWT | 管理頁列表(讀，含停用) |
| /api/1.0/remote-agents/health-check | POST | api/remote_agent/routes/remote_agent_route.py:61 | 是 | `RemoteAgentService.check_health` | 僅 JWT | 雲端打 agent /health；POST 但非寫 DB、非分頁查詢（動作型）|
| /api/1.0/remote-agents/&lt;uid&gt;/reconcile | POST | api/remote_agent/routes/remote_agent_route.py:74 | 是 | `RemoteAgentService.reconcile` | 僅 JWT | 對帳動作(讀取/比對)，無 admin 守門 |
| /api/1.0/remote-agents/&lt;uid&gt; | GET | api/remote_agent/routes/remote_agent_route.py:102 | 是 | `RemoteAgentService.get_agent` | 僅 JWT | 單筆 detail(讀) |
| /api/1.0/remote-agents/&lt;uid&gt; | PUT | api/remote_agent/routes/remote_agent_route.py:115 | 是 | `RemoteAgentService.update_agent` | 僅 JWT | 寫入(含啟用/停用)，無 admin 守門 |
| /api/1.0/remote-agents/&lt;uid&gt; | DELETE | api/remote_agent/routes/remote_agent_route.py:128 | 是 | `RemoteAgentService.delete_agent` | 僅 JWT | 寫入(刪除)，無 admin 守門 |
| /api/1.0/agents/register | POST | api/remote_agent/routes/remote_agent_route.py:143 | 否 | `AgentEnrollmentService.register` | 無 JWT | FR-039 控制面，agent→cloud，刻意不擋（token+TLS bootstrap）|
| /api/1.0/agents/heartbeat | POST | api/remote_agent/routes/remote_agent_route.py:155 | 否 | `AgentEnrollmentService.heartbeat` | 無 JWT | FR-039 控制面，agent→cloud，刻意不擋（mTLS）；service:186 對 revoked agent 拋 ForbiddenError（業務拒絕非授權）|
| /api/1.0/agents/enroll-token | GET | api/remote_agent/routes/remote_agent_route.py:168 | 是 | `AgentEnrollTokenService.get_current` | 僅 JWT | 讀 token 摘要；註解稱「需 admin」但 service 無 admin 守門 |
| /api/1.0/agents/enroll-token | POST | api/remote_agent/routes/remote_agent_route.py:178 | 是 | `AgentEnrollTokenService.generate` | 僅 JWT | **敏感 GET/寫**：產生註冊 token(明文僅此次回)；註解稱「需 admin」但 service 無 admin 守門 |
| /api/1.0/agents/enroll-token/revoke | POST | api/remote_agent/routes/remote_agent_route.py:193 | 是 | `AgentEnrollTokenService.revoke` | 僅 JWT | 停用全 tenant token；註解稱「需 admin」但 service 無 admin 守門 |
| /api/1.0/remote-agents/&lt;uid&gt;/revoke | POST | api/remote_agent/routes/remote_agent_route.py:206 | 是 | `RemoteAgentService.revoke_agent` | 僅 JWT | 撤銷 agent；註解稱「需 admin」但 service 無 admin 守門 |
| /api/1.0/remote-agents/&lt;uid&gt;/unrevoke | POST | api/remote_agent/routes/remote_agent_route.py:220 | 是 | `RemoteAgentService.unrevoke_agent` | 僅 JWT | 解除撤銷；註解稱「需 admin」但 service 無 admin 守門 |
| /api/1.0/remote-agents/&lt;uid&gt;/rebind | POST | api/remote_agent/routes/remote_agent_route.py:235 | 是 | `RemoteAgentService.rebind_agent` | 僅 JWT | 重新綁定設備指紋；註解稱「需 admin」但 service 無 admin 守門 |
