---
title: "Agent 主控台權限能力點化 — 設計定案 (FR-190)"
brand: "Guidant AI · **FR-190** Agent 主控台權限能力點化"
eyebrow: "FR-190 · 設計定案 v1.0 · 2026-11-09"
h1: "Agent 主控台權限能力點化"
subtitle: "7 個範圍拆成 26 顆能力點；三邊共用一份清單；同步機制沿用；14 張卡 7 批"
lede: "本文件是 FR-190 的定案設計。討論稿 D1～D9 全部裁定（見 §3），拆卡表經 `handoff/cards.py` 程式驗過依賴與同批撞檔。讀者只看這一份就能開工；討論過程與被排除方案在 [討論稿](discussion.md)。"
chips: [
  {text: "狀態：設計定案 v1.0（2026-11-09）", kind: ok},
  {text: "14 張卡 7 批；批 1＝環境卡＋契約卡", kind: accent},
  {text: "前作 FR-166／166.1；推翻 FR-166 一條（重啟下放離線操作者）", kind: warn},
  {text: "討論稿 discussion.md · 雛形 prototype/index.html", kind: plain}
]
footer: "FR-190 · 設計定案 v1.0 · 2026-11-09 · 拆卡表 handoff/cards.py（python 跑一次＝驗批次）"
---

## 變更紀錄 {#changelog nav="變更紀錄"}

| 日期 | 版本 | 內容 |
|---|---|---|
| 2026-11-09 | v1.0 | 討論稿 D1～D9 全部定案；拆卡 14 張 7 批 |
| 2026-11-09 | v1.0 | 開卡後資安審查：26 顆模組前綴從 `detection` 改 `agent`（避開 seed 的 `detection.*` 萬用字元 pattern，見 §5.1）；P1-2 加守衛測試。卡未派、就地改 |

## 需求背景與端到端流程 {#why nav="背景"}

Agent 主控台的權限由主系統決定：使用者點「進入主控台」時主系統逐顆查他持有的 Agent 能力點，把對應範圍寫進五分鐘票；斷線用的離線操作者名單也是每次心跳依當下能力點重算下發。這套機制正確，但只有 7 個範圍：`configure` 包 9 個設定頁、`download-files` 包 5 種下載、看與執行不分、主系統派的掃描紀錄掛在 `configure`。管理員做不到「可下載報告但不可下載證據」「只管 LDAP 不碰帳密」。

本案把 7 個範圔拆成 **11 個主控台功能 × 檢視／設定／執行／下載＝26 顆**，全部進主系統能力點表，角色頁以「Agent 管理 › Agent 主控台」子頁分組逐顆給；三邊（主系統、jedi-remote-agent、evidence-agent）改讀 `jedi-agent-protocol` 同一份清單。

```{.mermaid cap="圖 1 — 權限兩條路（都已存在，本案只換顆粒）"}
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
sequenceDiagram
    participant Admin as 管理員
    participant Main as 主系統
    participant Agent as Agent
    participant User as 使用者
    Admin->>Main: 角色頁「Agent 主控台」子頁勾顆（或按「比照帶入」）
    User->>Main: 點「進入主控台」
    Main->>Main: capability_checker 逐顆查 26 顆 → 持有的寫進票 scope
    User->>Agent: 帶票登入
    Agent->>Agent: 不認得的範圍丟掉並記數；全丟才拒
    Agent-->>User: 側欄依 *.read 出現；按鈕依 update／execute／download
    Main->>Agent: 每次心跳：離線名單帶該使用者當下 24 顆內持有的
```

## 分工概述（30 秒版） {#overview nav="分工概述"}

**整案一句話**：7 個範圍拆成 26 顆，三邊改讀同一份清單，角色頁逐顆給，同步機制不動。

| 階段 | 批 | 做什麼 | 完成怎麼判定（決策者檢查法） |
|---|---|---|---|
| P0 契約 | 1 | 環境卡（兩個 repo 改走本機路徑裝套件）＋ protocol 套件寫下 26 顆清單 | 開 `jedi_agent_protocol/console_scopes.py` 看到 11 資源 × 動作的表；`python -c "import jedi_remote_agent; print(jedi_remote_agent.__file__)"` 印出套件 repo 路徑 |
| P1 四邊接上 | 2～3 | jedi-remote-agent 三張表改推導；主系統 migration＋端點換顆＋矩陣；Agent 驗票丟不認得＋60 條守門換顆；主控台 FE 換顆＋橫幅；主站角色頁文案 | **對照雛形第 1～3 頁**：角色頁看到「Agent 管理 › Agent 主控台」子頁 26 顆；給角色只勾「檔案 · 檢視」「掃描 · 檢視」「掃描 · 下載」，用該角色進主控台：側欄只剩總覽／掃描／執行紀錄／檔案／授權，檔案頁無下載鈕，執行紀錄有下載鈕 |
| P2 帶入鈕 | 4～5 | 主系統唯讀端點算對照 → FE 角色頁子頁標題列一顆鈕 | **對照雛形第 1 頁**：按「比照主系統功能帶入」，有「檢測工具 · 修改」的角色自動勾上「工具設定 · 設定」，可再拔 |
| P3 收口 | 6～7 | 兩支套件發版、兩個 repo 改回釘版、SPEC 與手冊 | 用釘版 wheel 起主系統 HEAD，簽票端點 200；手冊「角色權限」節列 26 顆 |

**依賴**：批 1 的環境卡是硬前置——主系統釘 `jedi-remote-agent==1.17.6`、Agent 釘 `jedi-agent-protocol==0.5.3`，不改走本機路徑，批 2 的卡 import 到的是舊發行版、測試假綠。

**測試前提**：26 顆新權限的守門在新版 Agent 裡，驗收用的 Agent 必須先裝到 P1-4／P1-5 的版本。

## 決策定案表 {#decisions nav="決策"}

| # | 決策 | 定案 | 理由 | 被排除 |
|---|---|---|---|---|
| D1 | 顆粒 | **11 功能 × 檢視／設定／執行／下載，共 26 顆**（表見 §5.1） | 拆一半等於再做一次；四個動作是角色頁現成的詞 | 只拆 download 一顆 |
| D2 | 舊 Agent 收到不認得的範圍 | **丟掉、照常登入、頂部橫幅提示；全部不認得才拒** | 丟掉只會少給不會多給；主系統可先升、Agent 慢慢跟 | 整張票拒收（現況） |
| D3 | 主系統派的掃描紀錄 | **歸 `scan.read`／`scan.download`，不分來源** | 對使用者都是掃描結果 | 維持掛 configure |
| D4 | 角色頁排法 | **角色樹多一個「Agent」大組（`capabilities.module='agent'` 自動成第一層分組），底下一頁「Agent 主控台」（`ui_routes` 一列 `remote-agent-console`，`is_menu=false`、`tree_page=true`，掛在 Agent 管理底下但分組看 module），26 顆標籤帶功能前綴依資源分組**。Agent 管理頁本身的 4 顆仍在「檢測」組 | 一層分清「管清單」與「進主控台做事」；不為 11 個畫面造 11 列；模組換 `agent` 是資安審查結論（§5.1） | 11 個子頁；平鋪；留在 detection 模組 |
| D5 | 舊 7 顆退場 | **migration 把舊顆授權展開到新顆（含 pattern 角色），然後刪舊顆與三張關聯表的列；新顆 `legacy_name` 記舊名**。對照見 §5.2 | 出貨前無客戶；留退役顆是第二套真相 | 保留標退役 |
| D6 | 離線操作者不下放 | **只不下放 `update.read`、`update.execute`；重啟 `ops.execute` 下放**（推翻 FR-166 D14「重啟不下放」） | 斷線時卡住要能現場重啟；重啟可逆、換版不可逆 | 維持 FR-166 的線 |
| D7 | 主系統端點換顆 | **照 §5.6 對照表** | 舊顆刪掉後這些端點沒人守 | — |
| D8 | 票上寫法 | **原樣寫 26 個字串 `<資源>.<動作>`**（能力點名去 `agent.` 前綴） | 約 2.5 KB，登入上限 8 KB；可讀性除錯值錢 | 位元編碼 |
| D9 | 跟主系統對應功能連動 | **不連動，但一次給**：兩處等價的（工具帳密、金鑰換發）守同一顆；其餘靠角色範本預帶＋角色頁「比照主系統功能帶入」鈕（依 protocol `MIRRORS` 算，一次性、可再拔） | 風險面不同的事綁死會多給權限；勾兩次的痛點用鈕解 | 自動跟；全部手勾 |

## 現況接入點盤點 {#inventory nav="接入點"}

| 元件 | 現況 | 本案動作 | 卡 |
|---|---|---|---|
| `jedi-agent-protocol` | 事件碼／錯誤碼／指令／設定／事件／信任錨；無權限範圍 | **new** `console_scopes.py`；bump 0.6.0 | P0-1 |
| jedi-remote-agent `plugin/contract.py` 三張表 | 硬寫 11 顆＋兩張對照 | **extend**：從 protocol 推導 | P1-1 |
| 簽票 `RemoteAgentConsoleTicketRoute` | 逐顆 `capability_checker` | **沿用**，清單換 | P1-1 |
| 離線名單 `OfflineOperatorService._scopes_for` | 每次心跳重算 | **沿用**，對照表換 | P1-1 |
| 主系統 `capabilities` 表 | 套件宣告由 `flatten_pkg_migrations.sh` 攤成 `_capabilities.sql` 每次升級重跑 | **沿用**：26 顆自動進表；展開與刪舊另寫主線 migration | P1-2 |
| 主系統 `ui_routes` | `tree_page` 機制（FR-132） | **extend**：加 `remote-agent-console` 一列 | P1-2 |
| 主系統 `_ADMIN_CAPABILITIES`／`KEY_ADMIN_CAPABILITIES` | 約 40 端點守舊顆 | **extend**：照 §5.6 換 | P1-3 |
| `gen_permission_matrix.py` 空殼判定 | 掃主系統與 jedi 套件原始碼；沒掃 Agent | **extend**：protocol 清單內的顆視為 Agent 端有守 | P1-3 |
| Agent `shared/constants.py CONSOLE_SCOPES` | 硬寫 7 個 | **extend**：import protocol | P1-4 |
| Agent `agent_auth.verify_console_ticket` | 不認得整張拒 | **extend**：過濾＋記數；全丟才拒 | P1-4 |
| Agent `console/api/*.py` 約 60 條 `require_session` | 7 個範圍名 | **extend**：一對一換；`runs_routes` 來源判斷收掉 | P1-5 |
| 主控台 FE `router.js`＋13 個 view | 硬寫 scope | **extend**：換顆；`Shell.vue` 加橫幅 | P1-6 |
| 主站 `permission-tree.json`／`remote-agent.json`／`ACTION_ORDER` | 11 顆文案、5 條 scope 文案、無 `download` | **extend** | P1-7 |
| 角色頁 `PermissionMenuTree.vue` page 列 | 有「只看」link 鈕 | **extend**：子頁列多一顆「比照帶入」 | P2-2 |

## 詳細設計 {#design nav="詳細設計"}

### 5.1 26 顆清單（protocol `console_scopes.py` 的內容）

能力點全名 `agent.<資源>.<動作>`（**模組是 `agent`，不是 `detection`**）；票上與 Agent 守門用 `<資源>.<動作>`。`license_module` 一律 `remote-agent-manage`（授權照模組鍵不變）、`home_route` 一律 `remote-agent-console`。

::: {.callout .crit}
**🔴 為什麼不放 `detection` 底下**

出貨 seed（`scripts/init/04-seed-core.sql`）四個系統角色帶萬用字元：稽核主管 `detection.*.*`、稽核員／一般成員／唯讀 `detection.*.read`。26 顆若叫 `detection.agent-*`，新裝客戶的稽核主管升級後自動拿到全部 26 顆（含檔案下載、重啟、換版），唯讀角色拿到 11 顆檢視（含「這台全部證據不分專案」）。FR-166 的 7 顆已經踩在這個洞上，本案順手補掉：模組改 `agent`，沒有任何既有 pattern 命中；P1-2 加守衛測試「seed 內非 `*.*.*` 的 pattern 不得命中任何 `agent.*`」。DEV 沒有這些 pattern，runner 在 DEV 測不出來，所以守衛要掃 seed 檔本身。
:::

| 資源 | read（檢視） | update（設定） | execute（執行） | download（下載） | 離線可下放 |
|---|---|---|---|---|---|
| `files` 檔案 | 列表、預覽、上傳者清單 | — | — | 證據檔原檔 | 全部 |
| `scan` 掃描 | 工具、目標、基準、紀錄（含派工，D3） | — | 執行、取消、重送結果 | 報告原檔、工具 log | 全部 |
| `netdiag` 網路除錯 | 目標、紀錄、預估、報告預覽 | — | 執行、重送 | 除錯報告 | 全部 |
| `logs` 日誌 | 查日誌 | — | — | 匯出 | 全部 |
| `tools` 工具設定 | 看已設定（不回密碼） | 改帳密、測試連線、清除 | — | — | 全部 |
| `ldap` LDAP | 看設定 | 改來源、改本機、測試、重新信任 | — | — | 全部 |
| `keys` 金鑰 | 看狀態 | 換發 | — | — | 全部 |
| `license` 授權 | 看狀態 | 上傳、線上啟用 | — | — | 全部 |
| `ops` 維運 | 服務狀態、離線登入設定、儲存設定 | 改名稱、主系統位址、儲存、離線登入；補更新弱點庫／基準／目標 | 重啟 | 診斷包 | 全部（含重啟，D6） |
| `update` 更新 | 版本、檢查新版 | — | 線上換版、離線上傳換版 | — | **不下放** |
| `setup` 連線設定 | 註冊狀態 | 註冊、探測、換主系統 | — | — | 全部 |

共 26 顆（4＋4＋4＋2＋2＋2＋2＋2＋4＋2＋2）。`OFFLINE_GRANTABLE` 24 顆。

`MIRRORS`（D9 帶入對照，主系統能力點 → Agent 顆）：`detection.plugin.update → tools.update`、`detection.plugin.read → tools.read`、`detection.profile.read → scan.read`、`detection.profile.update → scan.execute`、`system.ldap-config.read → ldap.read`、`system.ldap-config.update → ldap.update`、`platform.license.read → license.read`、`platform.license.update → license.update`、`system.storage-config.read → ops.read`、`system.storage-config.update → ops.update`、`system.product-update.read → update.read`、`system.product-update.update → update.execute`、`detection.remote-agent.update → keys.update, ops.execute`、`detection.remote-agent.create → setup.update`、`grc.job-evidence.download → files.download`。無對照：`logs.*`、`netdiag.*`、各顆 read 以外沒列到的。

**為什麼放 protocol 不放 jedi-remote-agent**：Agent 不裝 jedi-remote-agent（帶 Flask／jedi-common），只裝 protocol；三邊都 import 得到的只有這裡。

### 5.2 舊顆 → 新顆展開（D5，P1-2 migration）

| 舊顆 | 展開成 |
|---|---|
| `configure` | `tools.*`、`ldap.*`、`keys.*`、`license.*`、`ops.read`、`ops.update`、`setup.*`（14 顆）＋ `scan.read`（D3） |
| `browse-files` | `files.read`、`logs.read` |
| `run-local-scan` | `scan.read`、`scan.execute`、`logs.read` |
| `net-diag` | `netdiag.read`、`netdiag.execute`、`logs.read` |
| `download-files` | `files.download`、`scan.download`、`netdiag.download`、`logs.download`；**`ops.download` 只給同時持 `configure` 者**（診斷包現況要兩顆都有） |
| `restart` | `ops.execute` |
| `upgrade` | `update.read`、`update.execute` |

展開走 `role_capabilities` 與 `role_capability_patterns` 兩條路（照 FR-166 migration 第 3 段形狀）。刪舊顆前先 `DELETE` 三張關聯表（`role_capabilities`、`route_capabilities`、`role_capability_patterns` 字面 pattern）的對應列——這三張表對 `capabilities` 無外鍵，不清會留孤兒。Administrator 靠 `*.*.*` 自然持有，不另處理。出貨預帶照 §5.1「離線可下放」旁的角色欄不在本案 migration 內（角色範本在 `_capability_grants.sql` 模板，P1-2 一併補 IT Manager／稽核員兩列）。

### 5.3 Agent 守門對照（P1-5）

| 檔 | 舊 | 新 |
|---|---|---|
| `files.py` 列表／上傳者／預覽 | `files` | `files.read` |
| `files.py` 下載 | `download-files` | `files.download` |
| `scan_routes.py` tools／targets／profiles／schema／runs GET | `run-local-scan` | `scan.read` |
| `scan_routes.py` uploads／runs POST／cancel | `run-local-scan` | `scan.execute` |
| `runs_routes.py` 清單與明細 | 依 (kind, source) 三種 | 掃描（不分來源）`scan.read`；除錯 `netdiag.read` |
| `runs_routes.py` resend | 同上 | 掃描 `scan.execute`；除錯 `netdiag.execute` |
| `netdiag_routes.py` dashboard／targets／estimate／runs GET／report preview | `net-diag` | `netdiag.read` |
| `netdiag_routes.py` runs POST | `net-diag` | `netdiag.execute` |
| `netdiag_routes.py` report 下載 | `download-files` | `netdiag.download` |
| `log_routes.py` 查 | 四顆任一 | `logs.read` |
| `log_routes.py` 匯出 | 四顆任一＋`download-files` | `logs.download` |
| `tools.py` GET | `configure` | `tools.read` |
| `tools.py` PUT／DELETE／test | `configure` | `tools.update` |
| `ldap_routes.py` GET | `configure` | `ldap.read` |
| `ldap_routes.py` PUT／DELETE／test／retrust | `configure`（寫入另加 `local_admin_only`） | `ldap.update`（`local_admin_only` 不動） |
| `keys_routes.py` GET | `configure` | `keys.read` |
| `keys_routes.py` renew | `configure` | `keys.update` |
| `license_routes.py` GET | 任何 session | 不變（任何登入者可看） |
| `license_routes.py` upload／activate | `configure` | `license.update` |
| `ops_routes.py` status | `restart` 或 `configure` | `ops.read` |
| `ops_routes.py` offline-login GET／storage GET | `configure` | `ops.read` |
| `ops_routes.py` offline-login PUT／agent-name／base-url／storage PUT・DELETE・test／vulndb・profiles・targets check | `configure` | `ops.update` |
| `ops_routes.py` restart | `restart` | `ops.execute` |
| `ops_routes.py` support-bundle | `configure`＋`download-files` | `ops.download`（單顆） |
| `update_routes.py` status／check | `update` | `update.read` |
| `update_routes.py` request／offline | `update` | `update.execute` |
| `setup_routes.py` state | `configure` | `setup.read` |
| `setup_routes.py` probe／enroll；`cloud_change_routes.py` probe／apply | `configure` | `setup.update` |
| `pages.py` me／overview；`local_auth_routes.py` password | 任何 session | 不變 |

### 5.4 主控台 FE 對照（P1-6）

`router.js`：scan→`scan.read`；netdiag→`netdiag.read`；runs→`['scan.read','netdiag.read']`；logs→`logs.read`；tools→`tools.read`；ldap→`ldap.read`；files→`files.read`；ops→`ops.read`；keys→`keys.read`；license→null（不變）；update→`update.read`；setup→`setup.update`。各 view 的操作鈕：`canDownload` 看各自 `*.download`；`canConfigure` 看各自 `*.update`；掃描／除錯「執行」鈕看 `*.execute`；`Ops.vue` 重啟鈕看 `ops.execute`。`Shell.vue`：`me.dropped_scopes > 0` 顯示 `.hint` 橫幅，文案進 `agent-console.json`。

### 5.5 主站 FE 對照（P1-7）

Agent 管理頁按鈕 `v-can`：「進入主控台」→26 顆任一（reuse `useMenuItemsCan` 的 ANY）；「重啟」→`ops.execute`；「換發憑證」→`keys.update`；「補更新基準」→`ops.update`；執行紀錄頁下載→`scan.download`／`netdiag.download` 依列種類。

### 5.6 主系統端點對照（D7，P1-3）

| 端點 | 舊 | 新 |
|---|---|---|
| `RemoteAgentConsoleTicketRoute.post` | `configure`／`browse-files`／`upgrade` 任一 | 26 顆任一 |
| `RemoteAgentLocalRunReportRoute.get`／`ReportViewRoute.get`／`ToolLogRoute.get` | `download-files` | `scan.download`、`netdiag.download` ANY；route 內依紀錄 kind 細判 |
| `RemoteAgentLocalRunsRoute.get`／`DetailRoute.get` | `_AGENT_LOCAL_RUNS` | `read`＋`scan.read`＋`netdiag.read` ANY |
| `RemoteAgentToolCredentialsRoute.post／delete` | `configure` | `tools.update` |
| `RemoteAgentProfilesCheckRoute.post` | `configure` | `ops.update` |
| `RemoteAgentRestartRoute.post` | `restart` | `ops.execute` |
| `AgentCredentialsRenewRoute.post` | `remote-agent.update` | `keys.update` |
| `_AGENT_LIST`（清單可見） | `read`＋`browse-files` | `read`＋26 顆任一 |
| `RemoteAgentsRoute.get`（下拉） | 四顆含 `browse-files` | `browse-files` 換 `files.read` |

### 5.7 Agent 驗票（D2，P1-4）

`verify_console_ticket` 回 `(claims, dropped: list[str])`：`scope` 過濾成 `[s for s in scope if s in CONSOLE_SCOPES]`，空 → 403 `TICKET_SCOPE_EMPTY`（新錯誤碼進 protocol `error_codes`）；`TicketScopeUnknown` 類別與路徑移除。`ConsoleSession.dropped_scopes: int`，不落地（`_PERSISTED` 不含）。`/api/me` 多 `dropped_scopes`。離線名單 `offline_operators.py` 第 85 行的過濾改讀新清單，行為不變。舊 7 個名字一律不認（不做舊主系統相容）。

### 5.8 資安規範對照

- SR-02（授權）：守門只收緊不放寬——26 顆對 7 顆是細分，任一端點的新顆集合 ⊆ 舊顆持有者集合（§5.3 表逐列可驗）；守衛測試 `test_fr190_scope_table.py` 對每顆單一 session 逐支打。
- SR-01（身分）：D2 丟掉不認得的範圔只縮不放；全丟拒絕用既有同一回應形狀。
- 票不帶訊息、不加心跳鍵（FR-181）：本案不動心跳與事件契約，protocol 只加 `console_scopes` 模組與一個錯誤碼。

## 拆分表 {#cards nav="拆卡"}

由 `handoff/cards.py` 產；批＝可同時派的一組，階段只是分組。`python handoff/cards.py` 驗依賴在更早批、同批不撞檔。

| 卡 | 階段 | 批 | repo | 做什麼 | reuse／extend／new | 依賴 | model／effort |
|---|---|---|---|---|---|---|---|
| P0-0 | P0 | 1 | main | 環境卡：主系統與 Agent 改走本機路徑裝 protocol 與 remote-agent（assume-unchanged） | reuse：pyproject 既有 path 註解行 | — | sonnet／medium |
| P0-1 | P0 | 1 | jedi-packages | protocol `console_scopes.py`：26 顆、OFFLINE_GRANTABLE、MIRRORS、`scope_of`／`capability_of`；bump 0.6.0 | new：照 event_codes 零相依形狀 | — | opus／high |
| P1-1 | P1 | 2 | jedi-packages | remote-agent 三張表改推導；bump 1.18.0 | extend | P0-1 | opus／high |
| P1-2 | P1 | 2 | main | migration：子頁、展開、刪舊顆、角色範本預帶；守衛測試 | extend：照 FR-166／FR-132 migration 形狀 | P0-0、P0-1 | opus／high |
| P1-3 | P1 | 2 | main | 端點守門換顆（§5.6）；矩陣加 download 與 Agent 端守門；重產 capabilities.js | extend | P0-1 | sonnet／medium |
| P1-4 | P1 | 2 | agent | 身分層：import protocol、驗票丟不認得、me 回 dropped | extend | P0-0、P0-1 | opus／high |
| P1-6 | P1 | 2 | fe | 主控台 FE 換顆、橫幅、i18n | extend | P0-1 | opus／medium |
| P1-5 | P1 | 3 | agent | 60 條守門換顆（§5.3）；全 blueprint 對表測試 | extend | P1-4 | opus／high |
| P1-7 | P1 | 3 | fe | 主站角色樹文案、ACTION_ORDER、離線操作者標籤、按鈕換顆、commit capabilities.js | extend | P1-3 | sonnet／medium |
| P2-1 | P2 | 4 | main | 唯讀端點 `GET /roles/<uid>/agent-console-mirror` | new（一支 service＋route；對照來自 protocol） | P1-2、P1-3 | sonnet／medium |
| P2-2 | P2 | 5 | fe | 角色頁子頁列「比照帶入」鈕 | extend：照「只看」鈕形狀 | P2-1、P1-7 | sonnet／medium |
| P3-1 | P3 | 6 | jedi-packages | 發版 protocol 0.6.0、remote-agent 1.18.0 | reuse 發版流程 | 批 2～5 全部 | sonnet／medium |
| P3-2 | P3 | 7 | main | 改回釘版、取消 assume-unchanged、釘版 wheel 起 HEAD 實打 | reuse ENV.md 反向 | P3-1 | sonnet／medium |
| P3-3 | P3 | 7 | main | SPEC 三頁、Agent 手冊權限節 | extend | P2-2 | sonnet／medium |

**登記型檔 owner**：protocol `console_scopes.py`→P0-1；remote-agent `contract.py`→P1-1；BE `_ADMIN_CAPABILITIES`→P1-3；BE `manifest.tsv`→P1-2；Agent `constants.py`＋`agent_auth.py`→P1-4；Agent `console/api/*.py`→P1-5（一張卡全換，拆開會撞 `__init__.py`）；FE console `router.js`→P1-6；FE 主站三支 i18n／`capabilities.js`→P1-7；FE `api.js`→P2-2。

## 端到端驗收 {#acceptance nav="驗收"}

| # | 步驤 | 期待 |
|---|---|---|
| 1 | 角色頁「檢測 › Agent 管理」 | 底下 4 顆管理用＋「Agent 主控台」子頁；子頁 26 顆依資源分組、動作 read→update→execute→download；舊 7 顆不見 |
| 2 | 建角色只勾 `files.read`、`scan.read`、`scan.download`，用該角色登主系統進主控台 | 側欄：總覽／掃描／執行紀錄／檔案／授權；檔案頁只有預覽；執行紀錄列出手動與派工掃描、各有下載鈕、除錯紀錄不出現；直接打 `/console/api/files/<uid>/download` 403 |
| 3 | 同角色對舊版 Agent（1.7.4） | 登入成功、側欄只剩總覽／執行紀錄（舊版認得 `configure`…都不在票上）／授權；頂部橫幅「有 3 項權限尚未支援」 |
| 4 | 把該使用者加為離線操作者，拔網線，用離線密碼登入 | 畫面同第 2 步 |
| 5 | 管理員拔掉 `scan.download` | 下次進主控台下載鈕消失；離線名單下一次心跳縮權、已登入 session 被踢 |
| 6 | 角色頁對有 `detection.plugin.update` 的角色按「比照主系統功能帶入」 | 「工具設定 · 設定」自動勾上，toast「已帶入 N 項」，可取消 |
| 7 | 主系統「Agent 掃描工具設定」頁代設帳密 | 只有 `tools.update` 的角色看得到按鈕；無則 403 |
| 8 | DEV 套完 migration | `capabilities` 無舊 7 顆、有 26 顆；原持 `configure` 的角色現持 15 顆；`CAPABILITY_CATALOG VERSION` +1；回寫「出貨基線待重產」 |
| 9 | P3-2 後用釘版 wheel 起 HEAD | 簽票 200；Agent 用舊票 403、新票 200 |
