---
title: "Agent 本機設定主控台 — 設計文件 (FR-157)"
brand: "Guidant AI · **FR-157** Agent 本機設定主控台"
eyebrow: "FR-157 · 設計文件 v1 · 2026-10-22（D1～D11 全數定案）"
h1: "工具帳密留在客戶機房，存了什麼檔自己看得到"
subtitle: "從主系統一鍵跳進 Agent 機上的主控台：填檢測工具帳密、瀏覽這台存的證據檔；兩個能力點決定看得到哪些分頁"
lede: "管理員在主系統 Agent 清單按一下，新分頁直接進到 Agent 機上的主控台（免再登入）。檢測工具的連線帳密在那裡填、加密存 Agent 本機，主系統從此不存；派工時 Agent 用自己機上那份。同一個主控台有「檔案」分頁，列這台 Agent 實際存在 SeaweedFS 或磁碟上的證據檔，可查可下載。權限拆成「Agent 設定」與「Agent 檔案」兩個能力點，稽核人員只勾檔案就能進去看檔、看不到帳密分頁。本文件把 11 項決策展開到「照做就能交」的程度，拆成五個子需求、10 張實作卡。"
chips: [
  {text: "D1～D11 全數定案", kind: ok},
  {text: "10 張實作卡、5 批", kind: accent},
  {text: "跨 4 repo：BE／FE／jedi-python-package／evidence-agent", kind: accent},
  {text: "推翻 FR-056 D9：帳密不再存雲端", kind: accent},
  {text: "陷阱：既有客戶的帳密要搬家，升級不能壞", kind: crit},
  {text: "陷阱：檔案頁權限是租戶層，看得到這台全部檔", kind: crit}
]
footer: "FR-157 · 設計文件 · 2026-10-22 · 討論稿：[discussion.html](discussion.html) · 雛形：[prototype/index.html](prototype/index.html) · 姊妹案：[FR-156](../FR-156-2610-agent-side-sbom-scan-and-vulndb-sync/) · 沿革見 LOG 與 git log"
---

> 狀態：**設計定案（D1～D11），已開卡待派**｜日期：2026-10-22｜母卡 [CM-3026](https://app.notion.com/p/FR-157-Agent-Agent-401346da4cd081b5a4a6ce12ea1cead5)、子卡 CM-3027～CM-3036｜前作：FR-156（入場券＋8444 門）、FR-056 D9（被推翻）、FR-039／066.6（Agent 儲存）
> 討論稿（PM 意見、選項推演）：[`discussion.html`](./discussion.html)｜雛形：[`prototype/index.html`](./prototype/index.html)（六幕，右上可切「IT 管理員／稽核人員」身分看權限差異）
> 開發 branch：四個 repo 2026-10-22 查皆在 `feature/FR-139`；runner 開工 `git branch --show-current` 再確認，不對就停下問，不切

> 🔴 **這份是設計決策，不是現況。** 內文的行號與座標停在 2026-10-22 定稿當時，之後實作改動的結果不回頭改寫這裡——上線後的現況看 SPEC 頁。

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

| 日期 | 變更 | 對應 |
|---|---|---|
| 2026-10-22 | 初版設計定案（D1～D11），拆 5 子需求 10 張實作卡 | 母卡 CM-3026，子卡 CM-3027～CM-3036 |

---

## 1. 需求背景與端到端 {#background nav="背景"}

PM 判定**檢測工具的連線帳密是客戶機敏資料，不該存在我們主系統**。現況（FR-056 D9）是主系統 `config.tenant_detection_tool_configs.credentials_encrypted` 存 Fernet 密文，派工時解密塞進派工單下發，Agent 用完即丟。落地版客戶的立場是「能打進我內網主機的密碼，為什麼在你們資料庫裡」。

決策者再加一項：客戶部署時會把 Agent 當儲存設備，證據檔本體落在 Agent 機的 SeaweedFS（主系統只有 `storage_type=remote_agent` 的參照列；DEV 實查 186 列），但 Agent 那邊沒有任何畫面，客戶想知道「這台存了什麼」只能問我們。

本案兩件事：**帳密搬 Agent 本機**、**Agent 開一個主控台讓人看檔**。兩件事共用同一道門：FR-156 E1 做好的五分鐘入場券＋Agent 8444 瀏覽器門。

### 做完之後

```{.mermaid cap="圖 1 — 從主系統一鍵進主控台；票帶範圍，Agent 照範圍開分頁與 API"}
%%{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 U as 使用者瀏覽器
    participant M as 主系統
    participant AG as Agent 8444（瀏覽器門）
    U->>M: Agent 管理頁按「開啟設定頁」或「瀏覽檔案」
    M->>M: 查他有哪些能力點：Agent 設定？Agent 檔案？
    M-->>U: 五分鐘入場券（誰、租戶、哪台、scope=[configure, files]）
    U->>AG: 新分頁開 https://agent:8444/console#票
    AG->>AG: 用雲端公鑰驗票：這台嗎、過期嗎、jti 用過嗎
    AG-->>U: 發 30 分鐘 HttpOnly session（記 scope），抹掉網址上的票
    U->>AG: 總覽／檔案（files）、檢測工具（configure）
    AG->>AG: 每個 API 先看 session 的 scope，不在範圍回 403
    AG->>AG: 帳密 Fernet 加密存本機；操作寫本機稽核
    AG->>M: 心跳：configured_tools、storage、console_events
```

```{.mermaid cap="圖 2 — 帳密的存放與使用"}
%%{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'}}}%%
flowchart LR
  A1[Agent 主控台<br/>檢測工具分頁] -->|填寫| A2[(Agent 本機<br/>tool-credentials.enc)]
  A3[主系統派工單<br/>不帶 _credentials] --> A4[Agent task_executor<br/>依工具代號讀本機]
  A2 --> A4
  A2 -.心跳只報已設定／最後測試.-> A5[主系統<br/>remote_agents.configured_tools]
  A5 --> A6[工具設定頁<br/>各 Agent 狀態表]
  A5 --> A7[派工前檢查<br/>沒設定立即失敗]
```

---

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

整案一句話：**使用者在主系統 Agent 清單按一下，新分頁直接進到 Agent 機上的主控台（免再登入）；有「Agent 設定」權限的人在那裡填檢測工具帳密，有「Agent 檔案」權限的人在那裡看這台存了哪些檔；主系統從此不存帳密，派工時 Agent 用自己機上的那份。**

| 子需求 | 做什麼 | 產出 | 完成怎麼判定（決策者檢查法，對照雛形第幾幕） |
|---|---|---|---|
| **A 跳轉與主控台殼** | 兩個能力點；主系統簽帶範圍的票；Agent 8444 掛主控台頁、驗票換 session、指路頁；主控台三分頁的殼 | 新分頁開出主控台總覽 | 幕 1→2：按「開啟設定頁」新分頁出現主控台、不輸帳密；切「稽核人員」身分只看到「瀏覽檔案」、進去沒有「檢測工具」分頁；幕 6：直接打 `https://<agent>:8444/console` 是指路頁；三十分鐘不動再點回指路頁 |
| **B 檔案瀏覽** | 主控台列 Agent 索引裡的證據檔，可依檔名／日期／上傳者查、可下載；容量統計 | 主控台「檔案」分頁 | 幕 4：在主系統上傳一份證據到走這台 Agent 的專案，到主控台檔案頁搜檔名找得到、大小與 SHA-256 一致、按下載拿到同一份；源碼暫存與弱點資料只顯示 GB 數 |
| **C 工具帳密搬 Agent** | 表單定義從主系統工具目錄經心跳下發；主控台填寫；Agent 加密存本機；測試連線在 Agent 本機跑；派工不再下發帳密，Agent 讀本機；心跳回報設定狀態 | 主控台「檢測工具」分頁＋Agent 加密儲存＋派工改道 | 幕 3：填 OpenVAS 帳密按「測試連線」綠勾；DEV DB `credentials_encrypted` 該租戶該工具為空；主系統派一支 OpenVAS 檢測正常跑完；派到沒設定的 Agent 立即失敗且訊息寫「Agent X 尚未設定 OpenVAS 連線」 |
| **D 主系統端配合** | 工具設定頁拿掉連線欄位、改各 Agent 設定狀態表；Agent 清單加「已設定工具」「儲存」欄；升級時既有帳密自動搬家；主控台操作回傳稽核 | 主系統三頁改版＋搬家＋稽核 | 幕 5：工具設定頁沒有帳密欄、有每台 Agent 的狀態表；幕 1：清單看得到每台設了哪些工具、存了多少檔；升級舊客戶 DB 後原本能跑的 OpenVAS 檢測不重填也能跑、密文欄清空；稽核報表看得到「誰在 Agent 改了帳密、下載了哪個檔」 |
| **E 文件與出貨** | SPEC、手冊、安裝文件、出貨基線 | 文件改成現況 | 手冊「檢測工具設定」一節的截圖是 Agent 主控台；安裝需求寫 8444 埠與憑證警告處理；188 基線庫含本案 migration |

依賴：A → B 與 C 平行 → C 的派工改道 → D 的三頁改版與搬家 → E。FR-156 E1 已 Done，本案直接接上。**決策者功課**：無。

---

## 3. 決策定案（D1～D11） {#decisions nav="決策定案"}

| # | 決策 | 定案 | 理由 | 被排除的方案 |
|---|---|---|---|---|
| **D1** | 怎麼進主控台 | **從主系統跳轉：簽五分鐘票、Agent 驗票換 30 分鐘 session；Agent 不開登入表單** | 使用者本來就在主系統做事做到一半才會想進 Agent；不用記網址、不用多登一次、帳密不經 Agent；Agent 不存帳號、驗票不回頭問主系統，FR-156 E1 機制現成 | 用主系統同一組帳密登入 Agent（Agent 要連主系統驗、或存一份帳號；多一個登入表單多一個攻擊面） |
| **D2** | 連線設定搬多少 | **整組搬**（網址、帳號、埠、密碼全在主控台填） | 填一半在 A、一半在 B 是最差的使用體驗；PM 說的「連線設定」本來就是整組；主系統連客戶 OpenVAS 在哪個 IP 都不知道，機敏範圍寬一點也涵蓋 | 只搬機敏欄位 |
| **D3** | 表單定義從哪來 | **主系統工具目錄經心跳下發、Agent 快取** | 目錄 `config.detection_tools.config_field_schema` 已有且有 i18n；新增工具或改欄位不用改 Agent 程式 | Agent 每支 connector 自帶（兩份定義改一邊漏一邊） |
| **D4** | 既有帳密怎麼搬家 | **升級後經既有 mTLS 派工通道自動交接一次、所有 Agent 回執後主系統清除** | 這條通道本來就天天送帳密，多送最後一次再關掉；客戶升級後什麼都不用做；搬完才清，中途斷線重跑不丟 | 升級後請客戶重填（升級當天所有檢測跑不動） |
| **D5** | 主控台前端放哪 | **FE repo 第二個 Vite 入口 `console/`，build 產物複製進 Agent 安裝包** | 設計系統、PrimeVue、i18n 都在 FE repo，手抄就是第二套真相；build 鏈多一步用腳本固定 | evidence-agent 內手寫靜態 HTML＋JS |
| **D6** | 檔案瀏覽動作範圍 | **只查只下載，不刪** | 刪除是主系統證據生命週期的事；從 Agent 端刪會讓主系統參照變孤兒 | 可刪（要再做反向通知） |
| **D7** | 檔案瀏覽顯示業務脈絡 | **第一版不做** | 檔案屬於哪個專案只有主系統知道，要經雲端給對照表；先列檔案本身 | 第一版就做對照快取 |
| **D8** | 主控台操作回傳主系統稽核 | **要，走心跳** | 稽核報表一處看完；客戶問「誰下載過這份證據」答得出；事件只有誰、何時、做了什麼，沒有帳密內容 | 只留 Agent 本機 log（主系統稽核缺一塊） |
| **D9** | 多台 Agent 設定同步 | **不做，各設一次** | 會經過雲端，與動機矛盾；決策者裁一個網段通常一台 Agent、或資料台與掃描台分開 | 雲端設一次同步各台 |
| **D10** | 誰能開主控台 | **兩個能力點「Agent 設定」「Agent 檔案」，簽票時把範圍寫進票，主控台照票開分頁與 API** | 檔案頁的對象（稽核、顧問、專案成員）比 Agent 管理寬，綁在 Agent 管理權限上會把他們鎖在外面；走 FR-155 做好的 RBAC，不另發明守門 | 與 Agent 管理頁同一權限、主控台內不分讀寫（本稿原建議，決策者不採） |
| **D11** | Agent 端加密金鑰放哪 | **獨立金鑰檔 `secrets.key` 0600** | 換 mTLS 憑證（重新註冊）不影響帳密；「重新註冊就要重填所有帳密」會是客服電話來源 | 用 mTLS 私鑰派生 |

::: {.callout .decided}
**D10 的限制要寫進手冊**

第一版檔案頁的權限是**租戶層**：有「Agent 檔案」就看得到這台 Agent 的全部檔案，做不到「只看自己專案的證據」。Agent 不知道檔案屬於哪個專案，那是 D7 第二期的事。對客戶 IT 或稽核通常可接受；若要讓一般專案成員也能看，D7 要提前做。
:::

::: {.callout .decided}
**UI 設計約束（沿用 FR-134／FR-156）**

版面照 Guidant 設計系統；**沒做的功能不在畫面預告**（檔案頁不出現刪除、不出現專案欄）；`grep border-left` 為零；下拉選項若有一律進 `system_menus`。
:::

---

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

| 元件 | 現況座標 | 現況 | 本案動作 | 卡 |
|---|---|---|---|---|
| Agent 管理能力點 | DEV `public.capabilities`：`detection.remote-agent.{read,create,update,delete}`，`license_module='remote-agent-manage'`；路由 `remote-agent-manage` 綁 read=ALL、其餘 ANY | 四顆 CRUD | 加 `detection.remote-agent.configure`（Agent 設定）、`detection.remote-agent.browse-files`（Agent 檔案）；路由綁 `browse-files` 為 ANY（只有檔案權限也看得到清單頁）；出廠範本角色「稽核員」補 `browse-files` | A1 |
| 主系統 Agent 管理端點守門 | BE `core/plugins/remote_agent.py:174 admin_required`，`_ADMIN_CAPABILITIES` 依 `__qualname__` 查能力點 | 清單 `RemoteAgentManageRoute.get` 守 read | 清單改守 read **或** browse-files（`require_capability(*names)` 多值＝任一）；新端點 `RemoteAgentConsoleTicketRoute.post` 守 configure 或 browse-files | A1 |
| 入場券簽發 | jedi-remote-agent `app/service/agent_ticket_service.py:68 issue(purpose, agent_uid, subject, tenant_id, scan_uid, max_bytes, origin, ttl)`；`PURPOSE_CONSOLE_LOGIN="console_login"` 已預留；claims `aud/sub/tenant_id/purpose/jti/origin` | 只有上傳票在用（jedi-detection `DetectionToolJobUploadTicketRoute`） | 加 `scope: list[str]` 參數，`console_login` 必填且非空；claims 多 `scope`、`display_name`（右上角「以王小明登入」用） | A1 |
| 主系統取使用者能力點 | `common/authz` `require_capability`；FE `$can(CAP.X)` | 守門用 | `RemoteAgentAdapters` 加 `capability_checker: Callable[[str], bool]`（宿主注入），簽票 service 用它算 scope | A1 |
| Agent 8444 瀏覽器門 | evidence-agent `core/browser_plane_server.py:111 create_browser_app`：只有 `PUT/OPTIONS /scan-input/<uid>`，`_method_not_allowed` 把其他路徑當不存在；`:181 start_browser_plane_tls` | 單一用途 | 加 `/console`（靜態）、`POST /console/session`、`/console/api/*`、`POST /console/logout`；`/scan-input` 驗票、`/console/api` 驗 session，互不認對方憑據 | A2 |
| Agent 驗票 | evidence-agent `core/agent_auth.py` `verify_upload_ticket`（雲端公鑰、claims 檢查） | 認 `scan_upload` | 加 `verify_console_ticket`：`purpose=console_login`、`aud=agent:<自己>`、`scope` 非空；`jti` 一次性記憶體集合（五分鐘淘汰） | A2 |
| Agent 心跳 payload | evidence-agent `core/enroll.py:260 _heartbeat_once`（`capabilities`、`browser_port`、`vulndb_sync.heartbeat_fields()`）；observers `:326 _observe_error_tracking`、`:333 _observe_allowed_targets` | 自報能力與狀態 | payload 加 `credential_store: true`、`configured_tools`、`storage`、`console_events`；observer 加 `_observe_tool_schemas`（版本不同就拉） | C1、C2、D3 |
| 雲端心跳處理 | jedi-remote-agent `app/service/agent_enrollment_service.py:464 _heartbeat`；`_capabilities_from_payload`、`_browser_port_from_payload`、`vulndb_values_from_payload`；回應 `pending_tasks`／`allowed_targets`／`vulndb` | 欄位逐一從 payload 取 | 收 `configured_tools`→`remote_agents.configured_tools JSONB`、`storage`→`storage_stats JSONB`、`credential_store`→`credential_store BOOLEAN`；回應加 `tool_schemas_version`；`console_events` 交宿主 hook 寫稽核 | C2、D3 |
| 心跳宿主擴充先例 | BE `core/plugins/_agent_vulndb.py vulndb_heartbeat_extra`（FR-156 C1） | 宿主往心跳回應塞 `vulndb` | 照抄一支 `core/plugins/_agent_tool_schemas.py`：回 `tool_schemas_version`；新端點 `GET /agents/tool-schemas`（AUTH_AGENT）回工具目錄 | C1 |
| 工具目錄 | DEV `config.detection_tools`：9 支，`config_field_schema` 全有、`credential_group_schema` 只有 gcb／inspec、`requires_credentials` 除 sbom-scan 外皆 true、`requires_target_host` gcb／inspec／nmap／openscap | 主系統工具設定頁表單來源 | 原樣下發給 Agent（code、名稱、i18n 標籤、欄位、群組、`requires_target_host`） | C1 |
| 主系統工具設定表 | `config.tenant_detection_tool_configs`：`credentials_encrypted`、`field_values`、`status`、`last_tested_at`、`last_test_result` | 每租戶每工具一列 | 加 `handover JSONB`（每台 Agent 回執）；搬家完成後 `credentials_encrypted=NULL`、`field_values` 連線欄位清空、`status='on_agent'`；表與欄位留著給「啟用開關」 | D2 |
| 派工解密下發 | jedi-detection `app/service/detection_orchestration_service.py:240/:812 _resolve_credentials`、`:1237 _tool_requires_credentials` | 解密塞 `_credentials` | 目標 Agent `credential_store=true` 時不帶 `_credentials`，改查該台 `configured_tools` 含此工具，沒有→立即 failed 新錯誤碼 `DETECTION_AGENT_TOOL_NOT_CONFIGURED`；`credential_store=false`（舊版 Agent）照舊下發（搬家過渡） | C2 |
| 主系統測試連線 | jedi-detection `api/routing.py:76 /detection-tools/configs/<uid>/test-connection`→`DetectionToolService.test_connection` 解密後打 Agent `POST /detection/probe`（evidence-agent `api/detection/routes/detection_route.py:51`） | 雲端帶帳密去 probe | 改成選一台 Agent、不帶帳密；Agent probe 改讀本機儲存 | C2 |
| Agent 執行器拿帳密 | evidence-agent `core/task_executor.py`（`_credentials` 夾帶慣例，`:100` 註解）、`task_executor_connectors/base.py:51 DetectionConnector(params, credentials)`、`:106 probe()` | 從派工單取 | 派工單沒帶就從本機 `credential_store.get(tool_code)` 取；兩者都沒有→failed「尚未設定」 | C2 |
| Agent 檔案索引 | Agent SQLite `/data/agent.db` 表 `upload_files`（jedi-file-upload `infra/models/upload_file.py`：uid／file_name／file_ext／size／mimetype／storage_type／sha256／created_at／created_user…）；`core/app_factory.py:37-46` 建表 | 只有程式在讀 | 主控台檔案 API 讀它；下載沿用 `FileUploadService` 既有串流（8443 `GET /blob/<uid>` 同一支 service） | B1 |
| Agent 儲存設定 | evidence-agent `config/config.py:23 STORAGE_TYPE`（local／seaweedfs／minio）、`:45 data_dir()`、`core/disk_space.py` | 啟動時組 DTO | 總覽與檔案頁的「儲存後端／健康／磁碟剩餘」從這裡算；SeaweedFS 健康＝S3 `head_bucket` | A2、B1 |
| Agent 資料目錄 | `/data`（`AGENT_DATA_DIR`）：`agent.db`、憑證、`scan-input/`、`vulndb/` | 升級不動的那層 | 加 `secrets/secrets.key`（0600）、`secrets/tool-credentials.enc`、`tool-schemas.json`、`console-audit.jsonl` | C1、D3 |
| Agent 安裝包 | evidence-agent `scripts/build/build_agent_bundle.sh`、`build_compose_bundle.sh`、`assets/` | 不含前端 | FE console build 產物進 `assets/console/`；browser plane 以靜態檔服務 | A2 |
| FE Agent 清單 | `src/views/remote-agent/RemoteAgentManage.vue`（`v-can="CAP.DETECTION_REMOTE_AGENT_*"` 控新增／啟停／撤銷）；`src/config/capabilities.js:43-46` | 名稱／位址／狀態／能力／版本／弱點資料 | 加兩顆 CAP 常數；加「已設定工具」「儲存」欄與「開啟設定頁／瀏覽檔案」按鈕；只有 browse-files 的人看到唯讀清單 | A1、D1 |
| FE 工具設定頁 | `src/views/plugin/ToolPluginManage.vue`（958 行，超標）、`src/utils/detectionCredentialFields.js` | 對話框填連線欄位、`has_credentials` 不回顯 | 連線欄位整段拿掉改「各 Agent 設定狀態」表；**超標檔只准拆不准塞**：狀態表另開子元件 | D1 |
| FE build | `vite.config.js` 單入口、`package.json` `build:ONPREM` 等 | 一個站 | 加 `vite.console.config.js`（root `console/`、outDir `dist-console/`）與 `build:CONSOLE` | A2 |
| 稽核事件碼 | BE `common/enum/event_code.py:77 AGENT_TICKET_ISSUED=6084`，`6085～6089` 空；jedi-remote-agent `common/code.py:64 AgentEventCode` 鏡像 | 一顆票事件 | 加 `AGENT_CONSOLE_LOGIN=6085`、`AGENT_CREDENTIAL_SET=6086`、`AGENT_CREDENTIAL_CLEARED=6087`、`AGENT_FILE_DOWNLOADED=6088`（兩邊同值） | D3 |

---

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

### 5.1 A 跳轉與主控台殼

**能力點（主線 `phase=active` migration，照 `scripts/sql/2026-10-10-fr134-source-scan-capabilities.sql` 形狀）**

| name | module | resource_type | action | description | license_module | 路由綁定 |
|---|---|---|---|---|---|---|
| `detection.remote-agent.configure` | detection | remote-agent | configure | Agent 設定（檢測工具帳密） | remote-agent-manage | `remote-agent-manage` ANY |
| `detection.remote-agent.browse-files` | detection | remote-agent | browse-files | Agent 檔案（瀏覽、下載） | remote-agent-manage | `remote-agent-manage` ANY |

- `capabilities.action` 若有 CHECK 約束要先查（`\d public.capabilities`），新值不在清單就一併放寬。
- 出廠範本角色：「稽核員」補 `browse-files`；「負責人」「審核者」「瀏覽者」不補（決策者要加去角色管理勾）。`role_capability_patterns` 既有 `detection.*.*` 的範本會自動涵蓋。
- `CAPABILITY_CATALOG/VERSION` 加一。

**簽票端點**（jedi-remote-agent，`ROUTE_TABLE` 加 `("/remote-agents/<string:uid>/console-ticket", RemoteAgentConsoleTicketRoute, AUTH_ADMIN)`；BE `_ADMIN_CAPABILITIES["RemoteAgentConsoleTicketRoute.post"] = (configure, browse-files)`）：

```
POST /api/1.0/remote-agents/{uid}/console-ticket
→ scope = [s for s in ("configure","files") if capability_checker(對應能力點)]
→ agent_ticket_service.issue(purpose="console_login", agent_uid, subject=login_name,
      tenant_id, origin=request Origin, scope=scope, display_name=nickname)
← {url: "<agent_url>/console#<ticket>", expires_in: 300, scope: [...]}
```

票 claims（凍結）：`aud=agent:<uid>`、`sub=<login_name>`、`tenant_id`、`purpose=console_login`、`jti`、`origin`、`scope: ["configure","files"]`（子集）、`display_name`、`exp`（5 分鐘）。`scope` 空就不簽（403）。

**Agent 8444 主控台路由**（`browser_plane_server.py` 已 200 餘行，新路由放 `core/console/` 套件，`create_browser_app` 只掛 blueprint）：

| 路由 | 驗什麼 | 做什麼 |
|---|---|---|
| `GET /console`、`GET /console/assets/*` | 無 | 靜態檔（`assets/console/`）；沒有 session cookie 時前端自己顯示指路頁（靜態頁內含兩種狀態，不另開端點） |
| `POST /console/session` body `{ticket}` | `verify_console_ticket` ＋ `jti` 未用過 | 發 session：`Set-Cookie: agent_console=<隨機 32 bytes>; HttpOnly; Secure; SameSite=Strict; Path=/console; Max-Age=1800`；記憶體 `{sid: {sub, display_name, tenant_id, scope, expires_at}}`；每次 API 成功呼叫把 `expires_at` 往後推 30 分鐘；回 `{display_name, scope, expires_at}`；寫本機稽核 `login` |
| `GET /console/api/me` | session | 回同上，前端刷新時用 |
| `GET /console/api/overview` | session | 名稱、租戶、角色（儲存／檢測）、版本、儲存後端與健康、主系統網址、最後心跳、容量四數字、各工具設定狀態（只有 `configure` 才給這段） |
| `POST /console/logout` | session | 刪 session、清 cookie |
| `/console/api/tools/*` | session ＋ `configure` in scope | §5.3 |
| `/console/api/files/*` | session ＋ `files` in scope | §5.2 |

- 票放 URL `#` 後：前端載入後讀 `location.hash`，POST 換 session，`history.replaceState` 抹掉。
- `jti` 一次性：記憶體 set，五分鐘後淘汰；Agent 重啟後視窗可接受（票只五分鐘），不持久化。
- session 存記憶體；Agent 重啟全部失效，使用者回主系統再按一次即可。
- 不在 scope 的 API 回 403 `{error_code: "AGENT_CONSOLE_SCOPE_DENIED"}`；前端照 `me.scope` 不渲染該分頁。
- CORS：主控台與 API 同源（都在 8444），不需 CORS；`/scan-input` 的 CORS 處理不動。

**FE console 入口**（D5）：`compliance-manager-fe/console/`（`index.html`、`main.js`、`App.vue`、`views/{Overview,Tools,Files}.vue`、`api.js`）；`vite.console.config.js`：`root: 'console'`、`base: '/console/'`、`build.outDir: '../dist-console'`，alias `@` 同主站以共用 `src/assets` 的設計 token、`src/config/locales/i18n`（只載 `remote-agent.json`、`plugin_manage` 與新 `agent-console.json`）、PrimeVue 元件按需 import；**不載 Pinia 使用者 store、不載主站 router**。`package.json` 加 `build:CONSOLE`。evidence-agent `build_agent_bundle.sh` 加一步：`FE_CONSOLE_DIST`（預設 `../compliance-manager-fe/dist-console`）存在才複製進 `assets/console/`，不存在 build 失敗（不是略過）。體積上限 2 MB（gzip 前），量過寫進回寫。

### 5.2 B 檔案瀏覽

```
GET /console/api/files?q=<檔名包含>&from=<date>&to=<date>&uploader=<login>&sort=created_at|size&order=desc&page=1&size=50
← {total, items: [{uid, file_name, size, mimetype, storage_type, sha256, created_at, created_user}], stats: {file_count, bytes_used, backend, backend_healthy, disk_free_bytes, scan_input_bytes, vulndb_bytes}}
GET /console/api/files/<uid>/download      → Content-Disposition attachment，串流自後端（FileUploadService 既有 adapter）
GET /console/api/files/uploaders            → 上傳者清單（distinct created_user）給篩選用
```

- 讀 Agent SQLite `upload_files`，只列 `storage_scope='customer'` 的證據檔；源碼暫存（`scan-input/`）與弱點資料（`vulndb/`）只算目錄大小進 `stats`，不列。
- 下載寫本機稽核 `file_downloaded {uid, file_name, sha256}`。
- 沒有刪除端點（D6）；沒有專案／證據欄（D7）。
- 上傳者顯示 `created_user`（主系統 login name）；暱稱 Agent 沒有，第一版就顯示 login name。

### 5.3 C 工具帳密搬 Agent

**表單定義下發（D3）**：BE `core/plugins/_agent_tool_schemas.py` 提供 `tool_schemas_heartbeat_extra()`→心跳回應多 `tool_schemas_version`（＝`config.detection_tools` 最大 `updated_at` 的 epoch 或 `CAPABILITY_CATALOG` 式計數）；新端點 `GET /api/1.0/agents/tool-schemas`（AUTH_AGENT，掛在 jedi-remote-agent `ROUTE_TABLE`，資料由宿主 `payload_provider` 風格的 `tool_schemas_provider` 注入）回：

```
{version, tools: [{code, name, name_i18n: {zh_Hant_TW, en}, requires_credentials, requires_target_host,
                   config_field_schema: [...原樣...], credential_group_schema: {...原樣或 null}}]}
```

Agent `_observe_tool_schemas`：版本與 `/data/tool-schemas.json` 不同就拉一次、原子寫入。主控台工具頁從這份畫表單；拉不到時工具頁顯示「尚未取得工具目錄，請稍候（Agent 需先與主系統完成一次心跳）」。

**Agent 本機儲存（D11）**：`core/credential_store.py`

- `secrets/secrets.key`：第一次寫入時 `Fernet.generate_key()`，`os.open(..., 0o600)`；讀取時檢查 mode 不是 0600 就警告（不拒絕）。
- `secrets/tool-credentials.enc`：Fernet 密文，解開是 `{<tool_code>: {fields: {...所有連線欄位含非機敏...}, configured_at, configured_by, last_test_at, last_test_result, last_test_message}}`；整檔讀寫、寫入走 temp＋`os.replace`。
- API：`get(code) -> dict|None`、`put(code, fields, actor)`、`clear(code, actor)`、`status() -> {code: {configured_at, last_test_at, last_test_result}}`（不含欄位值）、`mark_test(code, ok, message)`。
- 日誌與稽核永不印欄位值；`status()` 是心跳 `configured_tools` 的來源。

**主控台工具 API**：

```
GET  /console/api/tools                      → 目錄（tool-schemas.json）＋ status()
GET  /console/api/tools/<code>               → 非機敏欄位回顯、機敏欄位只回 {set: true}
PUT  /console/api/tools/<code> body {fields} → 機敏欄位留空＝不改（與主系統 has_credentials 慣例同）；寫稽核 credential_set
DELETE /console/api/tools/<code>             → clear；寫稽核 credential_cleared
POST /console/api/tools/<code>/test body {target_host?, credential_group?} → 用本機帳密呼叫 connector.probe()（與 api/detection/routes/detection_route.py 同一條路，差別是帳密來源），回 {ok, message, duration_ms}，mark_test
```

欄位驗證照 `config_field_schema`（required、type）在 Agent 做一次；gcb／inspec 的 `credential_group_schema` 兩組（SSH／WinRM）各自存在 `fields` 下的群組 key，形狀與主系統 `field_values` 一致，派工時 connector 不用改讀法。

**派工改道**（C2，jedi-detection `detection_orchestration_service`）：

```
if agent.credential_store:                      # 新版 Agent（心跳自報）
    if tool_code not in agent.configured_tools:  # 心跳自報的 status()
        → execution failed, error DETECTION_AGENT_TOOL_NOT_CONFIGURED,
          message「Agent {name} 尚未設定 {tool} 連線，請至該台設定頁填寫」
    params 不帶 _credentials
else:                                           # 舊版 Agent，搬家過渡
    照 FR-056 D9 解密下發（直到 D2 搬家完成清掉密文）
```

Agent `task_executor`：`credentials = params.pop("_credentials", None) or credential_store.get(code)`；兩者皆無→failed 同一句話。`probe` 路由（8443 `/detection/probe`）同樣：body 沒帶 `credentials` 就讀本機。主系統「測試連線」改：選一台 Agent→`POST /detection-tools/configs/<uid>/test-connection {agent_uid, target_host?, credential_group?}` 不帶帳密→Agent 本機帳密 probe→結果寫 `tenant_detection_tool_configs.last_test_*`（與主控台本機測試各自記錄，兩邊都能測）。

**心跳回報**：payload `configured_tools: {<code>: {configured_at, last_test_at, last_test_result}}`、`credential_store: true`；雲端 `remote_agents.configured_tools JSONB`、`credential_store BOOLEAN DEFAULT false`（套件 `migrations/010-remote-agent-console.sql`）。

### 5.4 D 主系統端配合

**三頁改版（D1 卡）**

- `RemoteAgentManage.vue`：加「已設定工具」欄（`configured_tools` 的 key 綠標、目錄內其餘工具灰標、tooltip 最後測試）、「儲存」欄（`storage_stats`：`{file_count} 檔 · {bytes_used} · {backend}`）；按鈕：`$can(configure)`→「開啟設定頁」、否則 `$can(browse-files)`→「瀏覽檔案」；只有 browse-files 的人：隱藏新增／啟停／撤銷／註冊 token 區塊與「已設定工具」欄；離線的 Agent 按鈕 disabled。按下→`POST console-ticket`→`window.open(url, '_blank', 'noopener')`。按鈕旁提示「需在可連到 Agent 的網路」。
- `ToolPluginManage.vue`（超標）：對話框連線欄位整段移除，改掛新子元件 `ToolAgentStatusTable.vue`（每台 Agent 一列：名稱、已設定／未設定、最後測試、「前往設定」＝同一支簽票）；「測試連線」改先選 Agent（只有一台自動選）；`detectionCredentialFields.js` 不再被工具頁 import（主控台 console 入口改用它，搬到 `src/utils/` 共用位置不動）。
- BE 清單 API（jedi-remote-agent `RemoteAgentManageRoute`）serializer 加 `configured_tools`、`storage_stats`、`credential_store`。

**搬家（D2 卡）**

```
升級後 Agent 第一次心跳 credential_store=true 且 remote_agents.credential_store 由 false→true
→ 宿主 lifecycle hook：對該租戶每支 credentials_encrypted 非空的工具，建 agent_tasks
    type="credential_handover", params={tools: {<code>: {fields: field_values ∪ 解密後機敏欄位}}}（既有 mTLS pending_tasks 通道）
→ Agent 執行器收到：credential_store.put 每支（已設定的不覆蓋，回執 skipped）→ result {stored:[...], skipped:[...]}
→ 雲端 result handler：tenant_detection_tool_configs.handover[<agent_uid>] = {at, stored, skipped}
→ 每次回執後檢查：該租戶所有 enabled 且 credential_store=true 的 Agent 都回執了
    → credentials_encrypted=NULL、field_values 只留非連線欄位（啟用等）、status='on_agent'、稽核 CREDENTIALS_HANDED_OVER
→ Agent 清單「已設定工具」欄在過渡期顯示「帳密搬家中：2/3 台」（有 handover 未齊）
```

- 租戶還有 Agent 沒升級：主系統等；期間舊 Agent 照舊收帶帳密的派工單（§5.3 的 else 分支）。
- 已撤銷／停用的 Agent 不算在「全部回執」分母。
- 重跑安全：Agent 端已設定的工具不覆蓋；雲端 handover 以 agent_uid 為 key 冪等。

**稽核回傳（D3 卡）**

- Agent 本機 `console-audit.jsonl`（append，每列 `{at, actor, action, tool|file_uid, file_name, sha256, ok}`），心跳帶 `console_events: [...上次心跳後新增的...]`，雲端回 `console_events_ack: <最後一筆 at>` 後 Agent 才前進游標（沒 ack 下次重送，雲端以 `(agent_uid, at, action)` 去重）。
- 雲端寫既有稽核 `audit()`：事件碼 6085～6088，`actor=<sub>`、`agent_uid`、`tool`／`file_uid`；FE 稽核報表的事件碼 i18n 補四條。

### 5.5 E 文件與出貨

SPEC 三頁（Agent 管理、檢測工具、新頁「Agent 主控台」）；手冊「檢測工具設定」改成主控台流程、加「誰能開」（兩個能力點）、「檔案頁看得到這台全部檔案」限制、憑證警告處理；安裝文件 8444 埠與 FE console 體積；`docs/claude/domain-capabilities.md` 補 `credential_store`、`console session`、`tool_schemas_provider` 三列；出貨基線重產（本案主線 migration：能力點 1 支、`tenant_detection_tool_configs.handover` 1 支）。

### 5.6 安全規則對照

| 規則 | 本案怎麼守 |
|---|---|
| 票在 URL | `#` fragment 不進日誌；一次性 `jti`；5 分鐘；綁 `aud` |
| session | HttpOnly／Secure／SameSite=Strict／Path=/console；30 分鐘滑動；登出即刪 |
| scope | API 層檢查，不是只藏分頁；403 有錯誤碼 |
| 帳密 | Fernet；金鑰 0600；日誌／稽核／心跳永不含欄位值；回顯只回 `{set: true}` |
| 機器擁有者 | 與 FR-064 D6 邊界一致：不宣稱擋機器擁有者；手冊寫明 |
| 下載 | 走 session，稽核留 `sha256`；不開匿名下載 |
| CSP | console 入口沿用主站 nonce 作法（`cspNonce`），browser plane 回 `Content-Security-Policy` 與主站同形 |

---

## 6. 拆分（10 張實作卡） {#breakdown nav="拆分"}

排序以「決策者何時看得到畫面」為準：先讓主控台開得出來、檔案頁有真資料（Agent 索引本來就有東西），再做帳密。同批依卡號串行。

| 批 | 卡 | 白話 | 改哪些檔（repo） | 驗收（對照雛形） | 依賴 |
|---|---|---|---|---|---|
| 1 | **A1** 兩個能力點＋簽票端點＋清單按鈕 | 主系統這邊：種兩個權限、簽一張帶範圍的票、清單加按鈕 | BE `scripts/sql/2026-10-xx-fr157-agent-console-capabilities.sql`、`core/plugins/remote_agent.py`（`_ADMIN_CAPABILITIES`、adapters 加 `capability_checker`）；jedi-remote-agent `agent_ticket_service.py`（scope／display_name）、`api/routes/remote_agent_route.py`＋`routing.py`（console-ticket）、`plugin.py`（adapters 欄位）；FE `capabilities.js`、`RemoteAgentManage.vue`（按鈕與依權限顯示）、i18n | 幕 1 兩種身分；`curl` 打 console-ticket 回 `url` 以 `#` 帶票、`jwt decode` 看到 `scope`；沒有兩顆能力點的人 403；只有 browse-files 的人清單頁看得到但無管理按鈕 | — |
| 1 | **A2** Agent 主控台殼：8444 掛頁、驗票換 session、指路頁、總覽；FE console 入口 | Agent 這邊：開門、換 session、三分頁的殼與總覽；前端骨架進 Agent 包 | evidence-agent `core/console/{__init__,session,routes,overview}.py`（新）、`core/agent_auth.py`（`verify_console_ticket`）、`core/browser_plane_server.py`（掛 blueprint 一行）、`scripts/build/build_agent_bundle.sh`、`assets/console/`；FE `console/`（新）、`vite.console.config.js`、`package.json` | 幕 2、幕 6；用 A1 的票新分頁開出總覽；`#` 票被抹掉；同票二用 409；直接打網址是指路頁；30 分鐘不動回指路頁；切稽核身分沒有「檢測工具」分頁且 `GET /console/api/tools` 403 | A1（claims 形狀已凍結，可先用手簽票開發） |
| 2 | **B1** 檔案瀏覽 | 列這台存的證據檔，搜、篩、下載、容量 | evidence-agent `core/console/files.py`（新）、`core/disk_space.py`（加統計）；FE `console/views/Files.vue` | 幕 4：主系統上傳一份證據→主控台搜到、SHA-256 一致、下載同一份；暫存與弱點資料只有 GB 數；沒有刪除按鈕 | A2 |
| 2 | **C1** 工具表單下發＋Agent 加密存＋本機測試連線 | 主控台能填帳密、存 Agent、按測試 | BE `core/plugins/_agent_tool_schemas.py`（新）、`core/plugins/remote_agent.py`（接 provider）；jedi-remote-agent 心跳回應 `tool_schemas_version`、`GET /agents/tool-schemas`；evidence-agent `core/credential_store.py`（新）、`core/console/tools.py`（新）、`core/enroll.py`（observer）；FE `console/views/Tools.vue` | 幕 3：填 OpenVAS 帳密測試綠勾；`cat /data/secrets/tool-credentials.enc` 是密文；`ls -l secrets.key` 0600；機敏欄位重開只顯示「已設定」；稽核 jsonl 無欄位值 | A2 |
| 3 | **C2** 派工改讀本機＋心跳回報設定狀態＋主系統測試連線改派 Agent | 主系統派工不再帶帳密；Agent 用自己那份；沒設定立即失敗 | jedi-detection `detection_orchestration_service.py`、`detection_tool_service.py`（test_connection）、錯誤碼；jedi-remote-agent 心跳收 `configured_tools`／`credential_store`、`migrations/010`；evidence-agent `core/task_executor.py`、`api/detection/routes/detection_route.py`、`core/enroll.py`（payload） | 主系統派 OpenVAS 檢測跑完（派工單 JSON 無 `_credentials`）；派到沒設定的 Agent 立即 failed 訊息指名哪台哪支；主系統測試連線選 Agent 成功；舊版 Agent（`credential_store=false`）照舊能跑 | C1 |
| 3 | **D1** 主系統三頁改版 | 工具設定頁拿掉帳密欄改各台狀態表；清單加兩欄 | FE `ToolPluginManage.vue`（只拆不塞）、`components/plugin/ToolAgentStatusTable.vue`（新）、`RemoteAgentManage.vue`（兩欄）；jedi-remote-agent serializer 加三欄 | 幕 5、幕 1 兩欄；工具對話框無任何帳密欄；狀態表每台一列、「前往設定」開得出主控台對應工具 | C2 |
| 4 | **D2** 既有帳密搬家 | 升級後自動把雲端的帳密交給每台 Agent，收齊回執才清 | jedi-detection（handover task 建立、result handler、`tenant_detection_tool_configs.handover` 套件 migration、清除邏輯）；jedi-remote-agent lifecycle hook；evidence-agent `task_executor_connectors/credential_handover.py`（新）；FE 清單「搬家中」標籤 | 用 FR-056 時代的 DEV 資料（`credentials_encrypted` 非空）起新版 Agent→心跳後 `handover` 有回執→密文清空、`status='on_agent'`→原本能跑的檢測不重填能跑；兩台只升一台→清單「搬家中 1/2」、密文未清 | C2 |
| 4 | **D3** 主控台操作回傳稽核 | 誰在 Agent 改了帳密、下載了什麼，主系統稽核看得到 | evidence-agent `core/console/audit.py`（新）、`core/enroll.py`（`console_events`＋ack 游標）；jedi-remote-agent 心跳收事件交宿主；BE `common/enum/event_code.py` 四碼、`core/plugins/remote_agent.py` hook；FE 稽核報表 i18n | 主控台登入、改帳密、下載各一次→主系統稽核報表三列，事件碼 6085／6086／6088，`actor` 是主系統帳號；心跳斷一次再連不重複 | A2、C1、B1 |
| 5 | **E1** SPEC、手冊、安裝文件 | 文件改現況 | BE `docs/spec-site/current/`（Agent 管理、檢測工具、新頁 Agent 主控台）、`docs/user-manual/`、`docs/claude/domain-capabilities.md`；evidence-agent `deploy/README.md` | 手冊「檢測工具設定」截圖是主控台；「誰能開」寫兩個能力點與租戶層限制；安裝文件 8444 與 console 體積；`grep "原本\|改判"` 零命中 | 全部 |
| 5 | **E2** 出貨基線重產＋收尾紀錄 | 新 migration 進基線；STATE／LOG | BE `scripts/init/`、`handoff/` | 188 基線庫 `schema_migrations` 含本案 2 支主線 migration；STATE 只描述此刻 | E1 |

**決策者功課（開工前）**：無。角色管理裡要不要給哪些角色「Agent 檔案」，上線後由客戶管理員自己勾；出廠只補「稽核員」。

---

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

| # | 步驟 | 期望 |
|---|---|---|
| 1 | 用有 configure＋browse-files 的帳號開 Agent 管理頁 | 看得到「開啟設定頁」、新增／啟停／撤銷按鈕、「已設定工具」「儲存」欄 |
| 2 | 按「開啟設定頁」 | 新分頁主控台總覽；網址 `#` 後沒有票；右上角「以 王小明 登入 · 設定＋檔案」 |
| 3 | 用只有 browse-files 的帳號（出廠「稽核員」）開 Agent 管理頁 | 唯讀清單、按鈕「瀏覽檔案」、沒有管理按鈕與「已設定工具」欄 |
| 4 | 按「瀏覽檔案」 | 主控台只有「總覽」「檔案」分頁；`curl -b cookie GET /console/api/tools` 403 |
| 5 | 直接打 `https://<agent>:8444/console` | 指路頁，不顯示任何這台的資訊 |
| 6 | 主控台檢測工具填 OpenVAS 帳密、測試連線 | 綠勾；`/data/secrets/` 兩檔存在、key 0600；重開表單密碼欄顯示「已設定」 |
| 7 | 主系統建 OpenVAS 檢測任務派到這台 | 跑完；`agent_tasks.params` 無 `_credentials`；DEV DB `credentials_encrypted` 為空 |
| 8 | 派到另一台沒設 OpenVAS 的 Agent | 立即 failed，訊息「Agent 鼎泰研發區 Agent-02 尚未設定 OpenVAS 連線」 |
| 9 | 主系統上傳一份證據到走這台 Agent 的專案 | 主控台檔案頁搜檔名找到；大小、SHA-256 一致；下載同一份；源碼暫存與弱點資料只有 GB |
| 10 | 主系統稽核報表 | 2、4、6、9 各有對應列（登入、登入、帳密設定、下載），actor 是主系統帳號 |
| 11 | 還原一份 FR-056 時代的 DEV dump（密文非空）、起新版 Agent | 心跳後密文清空、`status='on_agent'`；原本能跑的檢測不重填能跑；兩台只升一台時清單顯示「搬家中 1/2」且密文未清 |
| 12 | 主控台放 30 分鐘不動再點 | 回指路頁 |

---

## 8. 風險與陷阱 {#risks nav="風險"}

| 風險 | 怎麼擋 |
|---|---|
| 瀏覽器連不到 Agent（使用者在外網） | 設計上的必然；按鈕旁提示、開不出來時指路頁寫原因；手冊寫明只能在客戶內網操作 |
| 自簽憑證警告 | 同 FR-156 D9：手冊寫匯入 CA；第二期選配客戶自備憑證 |
| 票被攔截重放 | 5 分鐘、一次性 `jti`、綁 `aud`、放 `#` 不進日誌 |
| 搬家中途有 Agent 沒升級 | 等所有 enabled 且新版的 Agent 回執才清；舊版照舊收帶帳密的單；清單顯示進度 |
| 同租戶多台只設一台 | 清單「已設定工具」欄一眼看到；派工前檢查、失敗訊息指名哪台 |
| 檔案頁租戶層權限被誤解為專案層 | 手冊與角色管理的能力點說明寫明；D7 第二期 |
| `/scan-input` 票與主控台 session 共用 8444 | 路由分開、各驗各的憑據，互不承認 |
| Agent 機器擁有者讀得到金鑰與密文 | 與 FR-064 D6 邊界一致，手冊寫明 |
| FE console 體積進 Agent 包 | PrimeVue 按需 import；上限 2 MB，量過寫回寫 |
| `ToolPluginManage.vue` 958 行超標 | D1 只拆不塞：狀態表新子元件，連線欄位段刪除 |
| `capabilities.action` 有 CHECK 約束擋新值 | A1 開工先 `\d`，有就同支 migration 放寬 |
| 工具目錄沒下發到 Agent 時工具頁空白 | 顯示「尚未取得工具目錄」而非空白；心跳每 N 秒會補 |

---

## 9. 這份文件的定位 {#positioning nav="定位"}

| 你想知道 | 看 |
|---|---|
| PM 說什麼、選項怎麼推、D10 為什麼改 | [討論稿](discussion.md) §6.4 |
| 畫面長什麼樣、兩種身分差在哪 | [雛形](prototype/index.html)（右上切身分） |
| 每個決策為什麼 | 本文件 §3 |
| 開工要改哪些檔 | 本文件 §4、§6 |
| 入場券與 8444 門怎麼做的 | [FR-156 design](../FR-156-2610-agent-side-sbom-scan-and-vulndb-sync/design.md) E1 |
| 被推翻的帳密存雲端設計 | [FR-056 design](../FR-056-2607-detection-tool-integration/design.md) D9 |
| 做到哪了 | [README](README.md) 總進度、`handoff/`（開工後） |
