---
title: "Agent 主控台 PM 回饋六項 — 設計文件 (FR-166.1)"
brand: "Guidant AI · **FR-166.1** Agent 主控台 PM 回饋"
eyebrow: "FR-166.1 · 設計文件 v2 · 2026-10-29（D1～D10 定案；27 張卡 10 批）"
h1: "下載收進權限點、剩餘天數看得到、profile 自動到位、主系統設定 Agent 掃描工具、稽核走 logging 管線、登入後能切語言"
subtitle: "FR-166 收尾後 PM 提六條回饋；每條先找既有機制再決定怎麼接，沒有一條另起爐灶"
lede: "PM 看完 FR-166 手測版提六點：證據檔只准線上看、下載要獨立權限且出貨全鎖；離線還剩幾天能登入要顯示；CINC profile 推到 Agent 要看得到有沒有成功、能補推；工具帳密要能從主系統設定但資料仍存 Agent；要有操作與事件日誌可瀏覽；登入後沒得切語言。本文件把六條各自對到既有機制：下載權限點掛在 jedi-file-upload 的簽票那一層一處生效；profile 照弱點資料庫的心跳同步模型；主系統設定工具帳密走測試連線同一條直連通道當場存；稽核改接 Python logging 管線（jedi-common 的 system_logs、標準庫 QueueHandler），不再自寫 jsonl；剩餘天數與語言切換是純畫面。所有下載出口與 FR-169 的檔案下載加密接在一起。"
chips: [
  {text: "D1～D8 定案", kind: ok},
  {text: "27 張卡 10 批；Agent 7／FE 8／BE 6／套件 6", kind: accent},
  {text: "下載／匯出能力點分模組：7 顆 download＋2 顆 export，出貨零角色；上傳者本人免", kind: accent},
  {text: "稽核：一個入口、filter 補操作者、handler 當 sink", kind: accent},
  {text: "主系統兩條路通 Agent：心跳（拉）與資料面 8443 直推；設定帳密走直推", kind: accent},
  {text: "下載一律加密（FR-169）：主控台檔案／報告／日誌匯出／診斷包", kind: accent},
  {text: "陷阱：jedi-common 0.0.31 的 DBLogHandler 不過濾，要包過渡子類", kind: crit}
]
footer: "FR-166.1 · 設計文件 v1 · 2026-10-29 · 前作：[FR-166](../FR-166-2610-agent-standalone-console/) · 沿革見 LOG 與 git log"
---

> 狀態：**v2 定案、實作完成待收尾**｜日期：2026-10-29｜母卡 [CM-3199](https://app.notion.com/p/FR-166-1-Agent-PM-profile-logging-17-6-407346da4cd08143a138feb3191a5b04)、子卡 CM-3200～CM-3222、CM-3282～CM-3285｜前作 FR-166（主控台、D23 離線權限同主系統、HTML 預覽）、FR-156（弱點資料心跳同步）、FR-157（工具帳密搬進 Agent）、FR-033（主系統稽核事件管線）｜並行 [FR-169](../FR-169-2610-download-file-encryption/)（下載檔加密）
> 開發 branch：五個 repo 皆在 `feature/FR-139`；runner 開工 `git branch --show-current` 再確認，不對就停下問，不切

> 🔴 **這份是設計決策，不是現況。** 上線後的畫面與 API 現況看 SPEC 頁。

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

| 日期 | 變更 | 對應 |
|---|---|---|
| 2026-10-29 | 初版：PM 六條回饋 → D1～D8，17 張卡 6 批；同日開卡 | 母卡 CM-3199，子卡 CM-3200～CM-3216 |
| 2026-10-29 | v2：D1／D2 改分模組能力點；D5 改直連當場存；新增 D9（離線超過天數 AD 也鎖）、D10（日誌覆蓋率與匯出）；工具頁改名「Agent 掃描工具設定」並重做；與 FR-169 加密接齊 | 子卡 CM-3217～3222、CM-3282～3285 |

---

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

PM 2026-10-28 看完 FR-166 手測版後的六條原話與決策者補充：

| # | PM 原話 | 決策者補充（2026-10-29） |
|---|---|---|
| 1 | Agent 檔案的地方也是只能預覽、下載拔掉；所有報告不要讓下載只能預覽；下載要可以設定成權限點，目前全部都鎖起來 | **全系統所有證據檔案下載**都收進權限點，不限 Agent；不能預覽的格式就不給看 |
| 2 | 離線情況下還剩幾天就不能再離線登入，要顯示在儀表板 | — |
| 3 | CINC Auditor profile 推到 Agent 也要讓 Agent 知道有更新成功，也要可以補更新 | 比照弱點資料庫那套 |
| 4 | 掃描工具設定檔本機還是要留機制給 user 從主系統設定，讓人遠端幫忙設置不透過 Agent 畫面，但資料還是存在 Agent | 等於在主系統開放編輯權限；Agent 離線直接說連不上 |
| 5 | 要有日誌管理，記錄 user 的操作行為跟 log audit，要有 UI 可瀏覽 | 只記操作與事件，系統 log 先不要；比照主系統 `system_logs`；**用 logging 機制，本機有範例，不要硬刻** |
| 6 | 登入後沒有 i18n 切換功能 | — |

### 做完之後

- 沒有「下載證據」權限的人，主站十個地方的下載鈕都不見、Agent 主控台只剩預覽、主系統「Agent 掃描執行紀錄」頁報告也只剩預覽。出貨預設沒有任何角色有這顆，管理員要給才有。SVG 這類 Agent 不 inline 的格式就看不到。
- 主控台總覽多一張卡「離線登入還能用 N 天」，到 0 變紅並說明只剩本機管理員能登；**離線密碼與 AD 一起鎖**，連回主系統才能再用。
- 主控台總覽多一張卡「檢測基準」：幾支、多久前更新、幾支失敗，可按「立即檢查更新」，健康區多一盞燈。
- 主系統上傳新版 profile 後，每台 Agent 下次心跳自己比對、背景拉、回報；profile 頁每個版本展開看得到每台 Agent「已取得／待更新／失敗／離線」，有「補更新」鈕。主控台也有「立即檢查」。
- 主系統「檢測工具」頁翻回來改名「Agent 掃描工具設定」：左邊選一台 Agent，右邊是那台每支工具一張卡（已設定／未設定、設定時間、設定方式、最近測試）；每張卡一顆功能選單：設定、測試連線、清除，都開彈窗、當場生效。帳密只過一手、主系統不落地；Agent 離線整頁鎖並說明。
- Agent 稽核改寫進本機 SQLite 的 `system_logs` 表，欄位與主系統同一份；主控台多「日誌」分頁可依人、事件、日期篩，可匯出 CSV。預覽、進頁、開工具表單、開執行紀錄詳情也記；升級時舊版 jsonl 稽核一次搬進表。回傳主系統的機制不變，游標改自增 id。
- 所有下載（主控台檔案、報告、日誌匯出、診斷包，主系統 Agent 執行紀錄報告）都照 FR-169 加密：下載前顯示一次性密碼。
- 主控台登入後右上角可切中英。

## 2. 分工概述 {#overview nav="概述"}

```mermaid
flowchart LR
  subgraph 主系統
    FU[jedi-file-upload 簽票<br/>action=preview/download]
    CAP[(各模組 download 能力點<br/>認領者自己查)]
    HB[心跳回應<br/>profiles 清單＋補更新旗標]
    DP[資料面直推 8443<br/>PUT／DELETE /detection/credentials]
    SL[(system_logs)]
  end
  subgraph Agent
    PC[profile_cache 既有取檔]
    PS[profile_sync 照 vulndb_sync]
    CS[credential_store]
    AP[audit() → Filter → Queue → DBLogHandler 子類]
    SQ[(SQLite system_logs)]
    UI[主控台：預覽／剩餘天數／日誌／語言]
  end
  CAP --> FU
  HB --> PS --> PC
  DP --> CS
  AP --> SQ --> |心跳 console_events| SL
```

主系統與 Agent 之間有兩條路：Agent 出站的心跳（每 60 秒拉一次，profile 清單、補更新旗標夾在回應裡），以及主系統連進 Agent 資料面 8443 的直推（mTLS＋綁定 agent 身分的短效 JWT，回應比對 Agent 指紋）。要當場知道結果的動作（測試連線、取證據檔、設定與清除工具帳密）走直推；狀態同步走心跳。直推的前提是主系統連得到 Agent 的 8443：落地版同在客戶內網一定通，SaaS 版要客戶開放。

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

| # | 決策 | 定案 | 理由 | 被排除 |
|---|---|---|---|---|
| **D1** | 下載權限點掛哪一層 | 掛在 jedi-file-upload **簽票端點＋認領者**：簽票分 `action=preview\|download`，下載端點只收 download 票；登記表多一個 `FILE_ACTION_DOWNLOAD` 動作，**每類檔案的認領者**（證據、SSP 佐證、意見回饋附件…）在 download 時自己決定要不要多看一顆能力點。看不到的檔一律 404，看得到但不能下載 403 `FILE_UPLOAD_403002` | 主系統所有檔案下載都走 `/file/download/<uid>` 一條路，票是唯一門；但「哪些檔算客戶資料」只有認領它的模組知道，全局一顆守門會把公版範本也鎖掉 | 全局一顆 各模組 download 能力點（能力點名撞三段約束，且分不出客戶資料與公版）；前端逐處擋（漏一處就是洞）；另開第二條下載端點 |
| **D2** | 能力點命名與預設 | **每類資料用自己模組的 download／export 動作**：下載 7 顆（`grc.job-evidence.download`、`grc.audit.download`、`oscal.ssp.download`、`detection.source-scan.download`、`evidence.classification.download`、`workspace.feedback.download`、`detection.remote-agent.download-files`），匯出補 2 顆只擋 read 的客戶資料（`grc.audit.export`、`grc.risk.export`）；角色設定頁多一欄「下載／匯出」讓管理員逐模組決定；**出貨全部不綁任何角色**，Administrator 靠 `*.*.*` 自然持有。判準：內容是客戶自己的資料就鎖；產品帶來的公版／範本、各種匯入範本、管理功能匯出不擋。13 類上傳檔鎖 9 開 4（資源庫建議附件、框架 PDF、檢測規則包、系統共用資產開放）。**上傳者本人永遠能下載自己上傳的檔**（比 `upload_files.created_user`），但仍要先看得到；範本 SSP 的佐證文件只鎖下載、預覽照舊 | PM 原話「目前全部都鎖起來」；管理員要能分模組開，一顆全系統的做不到；上傳者拿不回自己的檔不合理 | 一顆全系統 各模組 download 能力點；綁預設角色；連匯入範本一起鎖 |
| **D3** | Agent 主控台的下載 | 票上新範圍 `download-files` ↔ `detection.remote-agent.download-files`（主控台檔案頁全是掃描報告，整頁視同證據；網路除錯報告一樣鎖）；離線操作者同樣照 D23 由心跳算；檔案頁補預覽端點走既有 `inline_policy`；不能 inline 的格式（svg、xhtml）不給看 | 與 D23 同一份真相；預覽機制 FR-166 已有 | 給 svg 另開消毒預覽 |
| **D4** | profile 同步模型 | **照弱點資料庫**：心跳回應附 `profiles[]`（現行版本 uid／version／sha256／file_uid），Agent 比對本機快取、背景拉、下次心跳回報每支狀態；主系統存 `agent_profile_states`；「補更新」＝心跳旗標讓 Agent 清冷卻立刻檢查；主控台「立即檢查」同 vulndb 那顆 | 掛點（`heartbeat_extras`）與 Agent 端流程（`vulndb_sync`）都現成；取檔與快取 `profile_cache.py` 現成，只差「主動比對」這一步 | 主系統主動推（主系統連不進 Agent）；改掉派工時 lazy 拉（沒壞） |
| **D5** | 主系統設定／清除工具帳密的通道 | **直連 Agent 資料面**：主系統 `PUT／DELETE` Agent 的 `/detection/credentials`，與測試連線同一道 mTLS＋JWT＋回應指紋比對；Agent 照工具目錄 schema 驗欄位（與主控台存檔同一套），不合回 400 不存；回來的狀態當場寫回 `remote_agents.configured_tools`。帳密**不寫** `tenant_detection_tool_configs`、不進派工單；帶帳密只走加密通道；Agent 離線回 409；稽核 6086／6087 method=remote 不帶值 | 測試連線本來就直推，設定若改等心跳，同一頁兩種速度、使用者以為壞了；測試連線能通的環境設定就能通 | 走 `credential_handover` 派工單等心跳（要輪詢、最多等一分鐘）；主系統存一份再同步（第二份真相）；離線排隊（使用者不知道何時生效） |
| **D9** | 離線超過天數時 AD 登入 | **一起鎖**，不留例外；只剩本機管理員能登，連回主系統才能再用。原因碼沿用 `method_blocked`，擋在送 AD 之前 | 斷線越久名單越可能過時（撤銷收不到），AD 密碼對不代表授權還有效 | AD 不受天數限制（FR-166 D17，推翻） |
| **D10** | 日誌覆蓋與匯出 | 舊 `console-audit.jsonl` 啟動時一次搬進表（冪等）；預覽記自己的碼 6230（與下載 6088 分開，主系統「檔案離開 Agent」統計不被預覽灌水）；進頁 6231、開工具表單 6232、開執行紀錄詳情 6233（同一對象 5 分鐘內只記一次）；匯出 CSV 6236，守「看日誌」範圍＋`download-files`、走 FR-169 加密。6230～6236 只留本機 | 換表前寫在 jsonl 的紀錄不搬，日誌分頁就看不到升級前的操作；「看」類動作要留紀錄才查得到誰看過客戶資料 | 前端切頁主動打 API 記進頁（要多一支 API，先不做） |
| **D6** | Agent 稽核的架構 | **接 Python logging 管線**：入口只有 jedi-remote-agent 既有 `audit(logger, event_name, event_code, **fields)`；`ConsoleActorFilter` 從 `flask.g.console_session` 補操作者（背景執行緒補 `system`）；jedi-common `SecretMaskingFilter` 遮密；`QueueHandler`→`QueueListener`→`DBLogHandler` 子類寫 SQLite `system_logs`（表、DTO、service 用 jedi-common 現成）；心跳回傳改讀表、游標改自增 id | 41 個呼叫點現在手傳 actor、哪些回傳是寫死集合、sink 只有 jsonl，加一個 sink 就改 `record()`；logging 天生就是「一入口多 sink 中間掛 filter」，主系統已這樣用 | 自寫 SQLite audit 模組（第二套管線）；只改 jsonl 成寫表（架構不變） |
| **D7** | 資料庫 | **留 SQLite**，不起 Postgres | 123 密集手測三天稽核 61 筆；WAL＋busy_timeout 已設；QueueListener 讓寫表永遠單執行緒；多一個容器牽動安裝／升級／wipe／診斷包全部腳本 | Postgres（反悔條件：多台共用狀態或跨 Agent 集中查，後者已靠心跳回傳做到） |
| **D8** | jedi-common 版本 | Agent 釘 0.0.31 **不升**；`DBLogHandler` 用薄子類補「只放行帶 event_code、缺欄補 None」兩段（1.3.6 原生有），升版即刪 | jedi-file-upload 0.0.22 也釘舊版，兩個一起升牽動範圍不小，與本案無關 | 本案順便升 |

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

全部開卡前實查（2026-10-29）。

| 主題 | 既有機制 | 位置 | 缺什麼 |
|---|---|---|---|
| 下載出口 | 主系統所有檔案下載走 `/file/download/<uid>?st=`，票由 `/file/access-token/<uid>` 簽，守門＝`file_access_guard`（簽章票或 JWT）＋`file_ownership_guard`（歸屬），無能力點 | `jedi-file-upload/api/routes/upload_file_route.py` 141、160；宿主接線 `core/plugins/file_upload.py` 351 | 票不分用途；`FileUploadAdapters` 沒有下載守門欄 |
| 前端下載呼叫 | 10 處：`signedDownloadUtil.js`（簽票唯一入口）、`JobExecutionDrawer.vue` 1434、`RoundEvidenceView.vue` 80、`SystemFeedbackView/Form.vue`、`EvidenceFilePreviewDialog.vue` 135、`useSbomViewer.js`、`useScanFileDownload.js`；Agent 執行紀錄 `AgentLocalRunsView.vue` 213 | FE repo | 都沒有 `v-can` |
| Agent 主控台下載 | 檔案頁 `files_download` 守 `files` 範圍；報告預覽走 `inline_policy`＋CSP sandbox（FR-166）；`runs.js canDownload` 看 `files`／`net-diag` | `core/console/files.py` 169、`console/views/Files.vue` 90、`console/runs.js` 82 | 檔案頁無預覽；範圍不分瀏覽與下載 |
| 能力點 ↔ 票範圍 | `CONSOLE_SCOPE_CAPABILITIES` 五列、`OFFLINE_OPERATOR_SCOPE_CAPABILITIES` 四列 | `jedi_remote_agent/plugin/contract.py` 44、52 | 缺 download |
| 離線天數 | 心跳下發 `offline_login_days`；Agent `heartbeat_status.offline_days()`；總覽 banner 已顯示「已離線 n 天／上限 N」 | `console/views/Overview.vue` 141 | 沒算剩餘、沒有卡 |
| 弱點資料同步（範本） | 心跳附加 `vulndb`（`heartbeat_extras` 鉤子）→ Agent `vulndb_sync.observe` 比對→背景下載→回報 `vulndb_date`；主控台 `POST /vulndb/check` 只清冷卻喚心跳 | `core/plugins/_agent_vulndb.py`、`evidence-agent/core/vulndb_sync.py`、`ops_routes.py` 104 | — |
| profile 取檔 | 派工時 `_resolve_profile` → `profile_cache` 按 sha256 命中或拉；原子解壓 | `evidence-agent/core/profile_cache.py` 115～250 | 沒有「主動比對清單」；主系統沒有每台 Agent 的狀態 |
| 帳密交接 | `credential_handover` 工單（升級搬家用）：主系統從雲端密文解出 fields 進單，Agent `put_if_absent` 存本機、回執工具代號 | `jedi_detection/app/service/credential_handover_service.py`；`evidence-agent/core/credential_handover.py` | 只有搬家；當場設定另走直推（D5） |
| 主系統直推 Agent | 測試連線 `AgentProbeClient`、取證據檔 `RemoteAgentAdapter`：mTLS＋綁定 agent 身分的短效 JWT；Agent 資料面 `/blob`、`/detection` 前綴由 `data_plane_auth` 守門 | `jedi_detection/infra/detection_tools/connector/agent_probe_client.py`；`infra/upload_file/remote_agent_adapter.py`；`evidence-agent/core/data_plane_auth.py` | 沒有設定帳密的端點 |
| 檢測工具頁 | `tool-plugin-manage` enable=0（FR-166 隱藏）；`ToolPluginManage.vue` 766 行 | `scripts/sql/2026-10-28-fr166-hide-tool-plugin-manage-menu.sql` | 翻回並改用途 |
| 主系統稽核管線 | `logger.info(extra={event_code})` → `CustomFormatter` 補操作者 → `DBLogHandler` 只放行帶事件碼 → `public.system_logs`；套件 `audit()` helper | jedi-common 1.3.6 `logger/`；`jedi_remote_agent/common/audit.py` | — |
| Agent 稽核 | `audit.record(action, actor, **fields)` 寫 jsonl；`REPORTED_ACTIONS` 11 種隨心跳回傳；ISO 微秒游標；`_MASKED_KEYS` 自遮；41 個呼叫點手傳 actor | `evidence-agent/core/console/audit.py` 155；logging 只有 `basicConfig` | 整段換管線 |
| Agent SQLite | jedi-common `init_db`，WAL＋busy_timeout 5s，`local_db` 版本表 v2 四張表 | `core/app_factory.py` 38、91；`core/local_db/migrations.py` | v3 加 `system_logs` |
| jedi-common 0.0.31 | `DBLogHandler.emit` 直接讀 `record.user_uid`／`event_code`、不過濾；`CreateSystemLogDTO`／`system_log_service.add_system_log`（`@transaction`）現成；`session_context` 是 ContextVar，listener 執行緒自開 scope 沒問題 | Agent venv `jedi_common/logger/db_log/` | 過濾與補欄要子類 |
| 主控台語言 | 登入頁有 `toggleLocale`；殼只有深淺色與登出；翻譯檔中英齊 | `console/Login.vue` 200、`console/Shell.vue` 55 | 殼沒鈕、沒記住 |
| 主站日誌頁（範本） | `UserLogs.vue` 讀 api_logs：act_time／level／user_name／url／method／source_ip | `src/views/action-log/UserLogs.vue` | — |

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

### 5.1 下載權限點（T1：A1、A1b、A2、A2b、B1、B4、B5、A5、F2、F6）

**票分用途。** `GET /file/access-token/<uid>?action=preview|download`，省略＝preview（舊前端不壞）。票的 scope 字串在既有 `file_access_scope` 後接 `:download`，下載端點 `file_access_guard` 只收帶 `:download` 的票，pdf-preview 端點兩種都收。簽 download 票前先跑 `adapters.download_guard`（宿主注入，None 時不擋）。

**認領者各看自己那顆。** 登記表 `FILE_ACTION_DOWNLOAD`；主系統 `core/plugins/_file_ownership.py::assert_download_capability` 一支共用：上傳者本人直接放行，否則查該模組的 download 能力點；9 類認領者各自呼叫它。能力點 migration 照 FR-166 形狀但第三段「預設角色授權」**整段不寫**。匯出端點照既有 `@require_capability("<模組>.export")` 形狀補 2 顆。

**Agent 端**：票範圍 `download-files`；`files_download`、netdiag 報告、runs 報告三條下載都改這個範圍；新增 `GET /console/files/<uid>/preview` 走 `inline_policy`。主系統「Agent 掃描執行紀錄」報告下載端點 `_ADMIN_CAPABILITIES` 改 `detection.remote-agent.download-files`。

**前端**：`signedDownloadUtil.fileDownloadUrl` 帶 `action=download`；下載鈕顯示條件＝「持該模組 download 能力點，或是自己上傳的」，各頁帶各自那顆；共用預覽框由呼叫端給能力點；匯出鈕看各自 export 顆；主控台檔案頁改「預覽」為主、下載鈕看 `me.scope` 含 `download-files`。

**陷阱**：Agent 驗票見到不認得的範圍整張拒收，所以 Agent（A2 套件＋A5）要先認得 `download-files`，主系統才可以簽；升級順序 Agent 先。

### 5.2 離線剩餘天數（T2：F1）

總覽卡：`剩餘 = offline_login_days − offline_days`，只在 `net === OFFLINE` 顯示；≤0 紅字＋「只剩本機管理員可登入」。資料心跳已下發、Agent `/console/state` 已回，純畫面。到期後離線密碼與 AD 一起鎖（D9）：Agent `operator_session_valid` 不分登入方式都看 `offline_login_disabled()`，登入當下與每個請求重驗走同一支；登入頁兩個分頁一起灰。

### 5.3 profile 同步（T3：A4、A7、B2、F4）

照 `vulndb_sync.py` 逐段對應：

| vulndb | profile |
|---|---|
| 心跳回應 `vulndb:{bundle_date,sha256,size}` | `profiles:[{uid,version,sha256,file_uid}]`（只含 `is_current`） |
| `observe()` 比日期 | `observe()` 逐支比 `profile_cache.is_cache_valid(cache_dir, sha256)` |
| 背景執行緒下載＋驗章＋原子切換 | 背景執行緒呼叫既有 `profile_cache._populate_cache`（抽成公開介面，**不複製**） |
| 回報 `vulndb_date`／`vulndb_sync_error` | payload `profiles:[{uid,version,sha256,state,error}]` |
| 主系統 `remote_agents.vulndb_*` 三欄 | 新表 `agent_profile_states`（一台多支） |
| 主控台 `POST /vulndb/check` 清冷卻 | `POST /profiles/check` 同款；主系統「補更新」＝心跳回應帶 `profiles_check:true` 旗標（照 restart 旗標形狀） |

失敗冷卻同 vulndb，避免每次心跳重拉。

**主控台總覽「檢測基準」卡**：Agent `profile_sync.summary()` 回 `{total, ok, error, last_synced_at, listed}`，取得時間用快取完成標記檔的 mtime（重啟不丟）；卡片顯示「N 天前更新／M 支基準 K 支失敗」，主系統沒下發清單時顯示「主系統未提供」；「立即檢查更新」與弱點資料卡共用 `useSyncCheck`；健康區 `profiles` 燈超過 7 天或有失敗轉黃。

### 5.4 Agent 掃描工具設定（T4：A9、B3、F5、F8，直連改寫）

**頁面**：主系統「合規與稽核 → Agent 掃描工具設定」（route `agent-tool-credentials`，門檻 `detection.remote-agent.configure`）。左欄選 Agent（連線中／離線、已設定 n／N 支、最近心跳）；右欄是那台每支工具一張卡：已設定／未設定、設定時間、設定方式（由主系統設定／Agent 主控台）、最近測試結果。只列租戶啟用、要帳密的工具。卡片標題列一顆「⋮」功能選單：

| 動作 | 彈窗 | 走哪條路 | 能力點 |
|---|---|---|---|
| 設定 | 照工具 schema 出表單，按「儲存」；已設定時提示會整組覆蓋 | `POST /remote-agents/{uid}/tool-credentials` → 主系統直推 Agent `PUT /detection/credentials` | `detection.remote-agent.configure` |
| 測試連線 | 要連目標主機的工具從設備清冊挑；有兩組憑證的工具先選組 | 既有 `POST /detection-tools/configs/{uid}/test-connection` 帶 `agent_uid`，Agent 用本機帳密測、結果記回心跳 | `detection.plugin.update` |
| 清除 | 確認窗說明之後派工會因未設定而失敗 | `DELETE /remote-agents/{uid}/tool-credentials` → 直推 `DELETE /detection/credentials` | `detection.remote-agent.configure` |

頁頂「管理啟用的工具」對話框是租戶層開關：關掉的工具整個停用，這頁不列、任務設定也選不到。

**直推流程**：
1. `is_online` false → 409 `FILE_AGENT_409003`，不打 Agent。工具目錄不認得 → 404。
2. `infra/remote_agent/agent_credential_client.py` 以 mTLS＋短效 JWT 打 Agent；帶帳密的 PUT 在非 `full` 模式直接拒（不走裸 http）；回應比對 `X-Agent-Fingerprint`，不符拒用並稽核 6081。
3. Agent 端 `/detection/credentials` 走 `credential_handover.store`（照工具目錄 schema 用 `secret_form.validate` 驗、寫 `credential_store`、稽核 6086／6087 method=handover）；欄位不合回 400，訊息只有欄名。
4. 主系統把回來的 `status` 寫回 `remote_agents.configured_tools`（`assign_fields`），畫面當場翻面；下一次心跳照常以 Agent 自報值覆蓋。
5. 主系統稽核 `AGENT_CREDENTIAL_SET`／`AGENT_CREDENTIAL_CLEARED` method=remote，只帶工具代號。

**離線與舊版 Agent**：右欄整頁鎖（離線說明連不上、舊版說明請先升級），每 30 秒重抓 Agent，連回自動解鎖；填到一半斷線會關彈窗。

### 5.5 Agent 稽核管線（T5：A3、A6、F3）

```
業務碼 ──audit(logger,"AGENT_X",AgentEventCode.AGENT_X,**fields)──▶ logger "agent"
                                                                     │
                                       ConsoleActorFilter（補 user_uid/user_nickname/login_method/source_ip；無 request→system）
                                       SecretMaskingFilter（jedi-common）
                                                                     │
                                                              QueueHandler（請求不等 DB）
                                                                     │  專屬執行緒
                                                              QueueListener ──▶ AgentDBLogHandler(DBLogHandler)
                                                                                  只放行 event_code≠None/''/'-'；缺欄補 None
                                                                                  system_log_service.add_system_log（@transaction 自開 scope）
                                                                                       │
                                                                                 SQLite system_logs
                                                                                       │
                                                        心跳 console_events：select id>cursor and event_code in REPORTED
                                                        ack → cursor=max(id)（自增 id，重啟不重送）
```

- **呼叫點**：41 處從 `audit("credential_set", g.console_session.sub, tool=code)` 改成 `audit(logger, "AGENT_CREDENTIAL_SET", AgentEventCode.AGENT_CREDENTIAL_SET, tool=code)`。`actor` 參數消失。
- **哪些回傳主系統**：事件碼表的屬性（套件 `AgentEventCode` 旁一個 `REPORTED` 集合），不再是 audit.py 另一個集合。
- **新事件碼**（A2 在套件加、B1 鏡像）：`AGENT_LOGOUT`、`AGENT_CREDENTIAL_TESTED`、`AGENT_PASSWORD_CHANGED`、`AGENT_HEARTBEAT_LOST`、`AGENT_HEARTBEAT_RESTORED`、`AGENT_VULNDB_UPDATED`、`AGENT_PROFILE_UPDATED`、`AGENT_LOCAL_SCAN_REFUSED`、`AGENT_LDAP_CHANGED`、`AGENT_STORAGE_CHANGED`、`AGENT_SUPPORT_BUNDLE_DOWNLOADED`，從 6207 起。
- **保留期** 180 天，心跳每日一次 `delete where act_time < now-180d`。
- **覆蓋率（D10）**：`core/console/audit_import.py` 啟動時把舊 `console-audit.jsonl`（含 `.1`）照時間排序一次寫進表，35 個舊動作名對到事件碼、對不到的記 6235；同一交易寫一列 6234 當冪等標記，寫完才把檔改名 `.imported`；`act_time` 原樣保留到微秒，主系統以 `at` 去重不會重複。`audit_once` 同一 session 同一對象在窗內只記第一筆（只在真的記了才更新時刻）。
- **匯出（D10）**：`GET /console/api/logs/export` 同一組篩選、不分頁、上限 50,000 列，CSV 帶 BOM、以 `= + - @` 開頭的儲存格加單引號；守「看日誌」範圍＋`download-files` 兩層；走 `file_protect.send_protected`（FR-169）。
- **過渡層**：`AgentDBLogHandler` 只存在因為 0.0.31 的 `emit` 不過濾且直讀 `record.user_uid`；檔頭註解只寫這一句與「升 jedi-common ≥1.3.6 即刪」。
- **A3 開工第一件事**：實測 `QueueListener` 執行緒上呼叫 `add_system_log`，`@transaction` 能自開 `session_scope`（`session_context` 是 ContextVar，新執行緒為空，應可）；不行就在 handler 內顯式 `with session_scope()`。

### 5.6 語言切換（T6：F1）

`Login.vue` 的 `toggleLocale` 抽成 `useLocale.js`（localStorage 記住，照 `theme.js` 形狀），殼頂欄與登入頁共用。

### 5.7 安全規則對照

- 下載票分用途後，preview 票打 download 端點必 403（A1 突變測試）。
- 7＋2 顆能力點出貨零角色；`gen_permission_matrix.py` 重產矩陣讓 FE 角色矩陣多一欄「下載／匯出」。
- 設定帳密：fields 只在這次直推的 request body 過手；主系統 log、稽核 message、回應 body 都不得帶值；帶帳密只走 mTLS 通道；回應比對 Agent 指紋。
- 所有下載出口走 FR-169 `send_protected`：主控台檔案、網路除錯報告、診斷包、日誌匯出，主系統 Agent 執行紀錄報告；預覽不加密。
- 入場券帶 Agent 不認得的範圍時回 `AGENT_CONSOLE_TICKET_SCOPE_UNKNOWN`（請升級 Agent），不混進「無效或過期」。
- 稽核 message 經 `SecretMaskingFilter`；A3 突變：把 filter 拿掉，含 `password=` 的測試要紅。
- 日誌端點唯讀、四種範圍任一可看；不回 `path`／`line_no` 這種程式座標給使用者。

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

**批（Stage）＝工作坊同時開幾個小弟一起跑的那一組**：同一批不改同一個既有檔、不依賴對方產出。底表在 [`handoff/cards.py`](handoff/cards.py)，兩個斷言已過。

**登記型檔只讓一張卡碰**：`manifest.tsv`／`event_code.py`／`remote_agent.py` adapters（B1、B2、B3 各在不同批）；`console/routes.py`（A6）；`enroll.py`（A7）；`Shell.vue` 與 i18n key（F1 先登記「日誌」項，F3 只新開 view）；`capabilities.js` 由 B1 重產、F2 commit。

**套件異動**：jedi-file-upload 1.4.0、jedi-remote-agent 1.9.0、jedi-detection 1.7.0、jedi-issue 1.4.0（意見回饋附件認領者），皆已發版、BE 已釘。批 10 與手測直修只動 Agent、BE、FE，沒有新套件版本。

**表中 B3／F5 兩列寫的是開卡範圍**（派工單＋等心跳）；行為以 D5 直連為準，見 §5.4。

| 批 | 主題 | 卡 | repo | reuse／extend／new＋理由 | 改哪些既有檔 | 新開哪些檔 | 依賴 | 驗收 |
|---|---|---|---|---|---|---|---|---|
| 1 | 下載權限點 | **A1** jedi-file-upload：簽票分 preview／download 兩種用途、下載端點只收 download 票、宿主可注入下載守門 | jedi 套件 | extend：`UploadFileAccessTokenRoute` 加 `?action=preview|download`（預設 preview，舊前端不壞）；`FileUploadAdapters` 加一欄 `download_guard`（宿主注入，None＝不擋）；`UploadFileDownloadRoute` 驗票時只收 action=download 的票；不另開第二條下載端點 | `jedi_file_upload/api/routes/upload_file_route.py 141（簽票）、160（下載）`、`jedi_file_upload/plugin/contract.py 125（FileUploadAdapters）`、`jedi_file_upload/api/guards.py 42（file_access_guard 傳 action）` |   | — | 套件 tests：簽 preview 票打 download 端點 403；簽 download 票先過 `download_guard`（注入一支會拒的 guard → 簽票 403）；沒注入 guard 行為與今天相同；pdf-preview 端點不受影響 |
| 1 | 下載權限點 | **A2** jedi-remote-agent：主控台票新範圍 download-files（A2b 對到 `detection.remote-agent.download-files`）；AgentEventCode 補本機事件碼；console_event 收 event_code | jedi 套件 | extend：`plugin/contract.py` 的 `CONSOLE_SCOPE_CAPABILITIES` 與 `OFFLINE_OPERATOR_SCOPE_CAPABILITIES` 各加一列；`common/code.py` `AgentEventCode` 從 6207 起加碼；`heartbeat_console.ingest_console_events` 收的事件多一個 `event_code` 鍵（沒帶仍照 action 對照，舊 Agent 不壞） | `jedi_remote_agent/plugin/contract.py 44、52`、`jedi_remote_agent/common/code.py 89`、`jedi_remote_agent/app/service/heartbeat_console.py 87` |   | — | tests：持有 各模組 download 能力點 的票帶 `download-files`；離線操作者範圍含它；`ingest_console_events` 帶 event_code 的事件原碼寫入、只帶 action 的照舊；新碼 6207～ 不與主專案 `event_code.py` 撞號（開卡已查最大 6206） |
| 1 | Agent 日誌 | **A3** Agent 稽核改走 logging 管線：system_logs（SQLite）＋QueueHandler＋操作者過濾器＋DB handler 過渡子類；41 個呼叫點改用套件 audit() | evidence-agent | reuse：入口用 jedi-remote-agent `common/audit.py::audit(logger, event_name, event_code, **fields)`；表／DTO／service 用 jedi-common 0.0.31 `logger/db_log/system_log`；標準庫 `QueueHandler`＋`QueueListener`。new（過渡層，升 jedi-common 即刪）：`DBLogHandler` 薄子類（只放行帶 event_code、缺欄補 None）。new：`ConsoleActorFilter`（從 `flask.g.console_session` 補 user_uid／user_nickname／login_method／ip，背景執行緒補 system）。刪：`audit.py` 的 jsonl 寫入、ISO 游標、`local_rows`、`REPORTED_ACTIONS`／`_MASKED_KEYS` | `core/console/audit.py 155（整支改寫：record→委派 audit()；pending／ack 改讀表、游標改自增 id）`、`core/local_db/migrations.py（v3：system_logs 表，欄位照 jedi-common model）`、`core/app_factory.py 24（logging 接線一處）`、`core/console/heartbeat.py 51`、`core/console/{routes,ops_routes,scan_routes,netdiag_routes,tools,files,runs_routes,ldap_routes,local_auth,local_auth_routes}.py、core/{console_allow_cidr,local_state_cli,ldap_source}.py（41 處呼叫改簽名、拿掉 actor 參數）` | `core/audit_pipeline.py（filter、queue、handler 子類、install()）` | — | 登入／設帳密／手動掃描後 `sqlite3 data/agent.db 'select event_code,user_name,message from system_logs'` 有列且密碼欄為 `***`；心跳 `console_events` 帶 `event_code`、ack 後游標前進（重啟不重送）；背景執行緒事件 user_name=system；把 filter 拿掉 → 測試紅（突變）；`grep -rn 'console-audit.jsonl' core/` 為 0；寫表失敗只進 app log、請求不失敗；180 天清理在心跳跑一次／天 |
| 1 | profile 同步 | **A4** jedi-detection：Agent×profile 取得狀態表、心跳附加欄位 provider、代設帳密的 handover 單（overwrite） | jedi 套件 | extend：新表 `agent_profile_states(agent_id, profile_version_id, sha256, synced_at, error)` 照套件既有 migration 形狀；`credential_handover_service.start_for_agent` 加 `tools=`／`overwrite=` 參數（代設走同一支，不另開 service）；心跳附加欄位 `profiles` 的 provider 放套件（主專案接線） | `jedi_detection/app/service/credential_handover_service.py 91、116`、`jedi_detection/migrations/（新號）` | `jedi_detection/domain/detection_tools/entity/agent_profile_state_entity.py`、`jedi_detection/infra/detection_tools/model+repo（agent_profile_state）`、`jedi_detection/app/service/agent_profile_sync_service.py（current_profiles_for_heartbeat()、record_agent_state()）` | — | DEV 套套件 migration 後 `\d` 新表有 RLS 四指令與 `(agent_id, profile_version_id)` 唯一鍵；`current_profiles_for_heartbeat()` 只回 `is_current` 版本的 `{uid, version, sha256, file_uid}`；`start_for_agent(tools=[x], overwrite=True)` 建的單 params 帶 overwrite；既有 handover tests 綠 |
| 1 | 語言切換 | **F1** 主控台殼層：登入後語言切換、總覽「離線登入剩餘天數」卡、選單登記「日誌」分頁（先指向佔位） | FE | extend：`Shell.vue` 頂欄把 `Login.vue` 既有的 `toggleLocale` 搬成共用（`state.js` 或新 `useLocale.js`，localStorage 記住，照 `theme.js` 形狀）；`Overview.vue` 的 banner 既有 `offline_days`／`offline_login_days` 算剩餘天數加一張卡（照 `overviewHealth.js` 既有卡形狀）；**登記型檔集中**：`Shell.vue` 選單本卡一次加「日誌」項並佔 i18n key，F3 只新開 view | `console/Shell.vue 201`、`console/Login.vue 200（toggleLocale 改呼叫共用）`、`console/views/Overview.vue 141`、`console/router.js（logs 路由指向 Placeholder.vue）`、`i18n zh-tw/en agent-console.json` | `console/useLocale.js` | — | 登入後右上角有地球鈕，切換後重整仍是該語言；總覽有「離線登入還能用 N 天」卡：N=上限−已離線天數、≤0 紅字並寫「只剩本機管理員與 AD 可登」、在線時卡不顯示；側欄多「日誌」項（點進去是佔位）；`grep border-left console/` 為 0 |
| 2 | 下載權限點 | **A5** Agent 端：檔案與報告下載只認 download-files 範圍、檔案頁補預覽端點、不能 inline 的格式不給看 | evidence-agent | extend：`files.py` 的 `files_download` 改 `require_session('download-files')`、新增 `files_preview` 走既有 `inline_policy`（照 `runs_routes` 預覽端點形狀，不複製）；`netdiag_routes` 報告下載同樣改範圍；`local_login.py` 認得新範圍（票上帶不認得的範圍整張拒收，先認再用） | `core/console/files.py 169`、`core/console/netdiag_routes.py（report 下載）`、`core/console/runs_routes.py（報告下載）`、`core/console/local_login.py（範圍白名單）` |   | A2、A3 | curl 8444：票無 `download-files` 時 `GET /console/files/<uid>/download` 403、`/preview` 200 且 HTML 回應帶 `Content-Security-Policy: sandbox`；svg 預覽 415；主系統票帶 `download-files` 時 200；tests 含突變（把範圍檢查拿掉要紅） |
| 2 | Agent 日誌 | **A6** Agent 日誌查詢 API：依人／事件碼／日期分頁讀本機 system_logs | evidence-agent | reuse：jedi-common `system_log_service.get_system_log_list(curr_page, page_size, **filters)` 已有分頁與篩選，端點只做參數解析與序列化；守門 `require_session(('configure','files','run-local-scan','net-diag'))` 唯讀；**登記型檔**：`routes.py` 只加一行掛 blueprint | `core/console/routes.py 212（掛一行）` | `core/console/log_routes.py` | A3 | `GET /console/logs?page=1&size=50&user=local-admin&event_code=6093&from=…&to=…` 回 `{items, total}`，欄位 act_time／user_name／event_code／message／method；無 session 401；message 裡沒有密碼（A3 已遮） |
| 2 | profile 同步 | **A7** Agent profile 同步：心跳收 profiles 清單比對本機快取、背景補拉、回報每支狀態；主控台「立即檢查」 | evidence-agent | reuse：取檔與快取用既有 `core/profile_cache.py`（`_populate_cache`、`is_cache_valid`），**不另寫下載**；流程照 `core/vulndb_sync.py` 形狀（observe→比對→背景執行緒→回報→冷卻），`request_check()` 同款；**登記型檔**：`enroll.py` 掛一個 observer＋payload 加一欄 | `core/enroll.py 287（payload 加 `profiles`）、362（observer 一行）`、`core/console/ops_routes.py（`POST /console/profiles/check` 照 vulndb/check 形狀）` | `core/profile_sync.py` | A4 | 心跳回應帶 `profiles:[{uid,version,sha256,file_uid}]` 時，本機沒有的在背景拉、拉完 marker sha256 一致；下次心跳 payload `profiles:[{uid,version,sha256,state:ok|error,error}]`；`POST /console/profiles/check` 清冷卻；拉失敗回報 error 不重試到死（冷卻同 vulndb）；稽核 `AGENT_PROFILE_UPDATED` 一筆 |
| 2 | 代設工具帳密 | **A9** Agent 收代設帳密：handover 單帶 overwrite 時蓋掉本機既有值 | evidence-agent | extend：`credential_handover.store` 讀 `task.get('overwrite')`，True 走 `credential_store.put`（既有）而非 `put_if_absent`；回報 summary 多 `overwritten` | `core/credential_handover.py 20` |   | A4 | tests：overwrite=False 既有不蓋（stored/skipped 同今天）；overwrite=True 蓋掉並回 overwritten；log 只帶工具代號；稽核 `AGENT_CREDENTIAL_SET` method=handover |
| 2 | 下載權限點 | **B1** 主系統：EventCode 鏡像 6207～6217、console sink 收 event_code（下載守門改由 B4 分模組做） | BE | extend：migration 照 `2026-10-27-fr166-agent-standalone-capabilities.sql` 形狀但**不綁預設角色**；`core/plugins/file_upload.py` adapters 多注入 `download_guard=require_capability('file.download')`；`core/plugins/remote_agent.py` `_ADMIN_CAPABILITIES` 的 `RemoteAgentLocalRunReportRoute.get` 改 各模組 download 能力點；`common/enum/event_code.py` 鏡像 A2 新碼；`core/plugins/_agent_console_audit.py` 收 event_code；跑 `scripts/deliverables/gen_permission_matrix.py` 重產 FE `capabilities.js`（FE 檔由 F2 commit） | `core/plugins/file_upload.py 351`、`core/plugins/remote_agent.py 186`、`common/enum/event_code.py 198`、`core/plugins/_agent_console_audit.py 25`、`scripts/sql/manifest.tsv` | `scripts/sql/2026-10-29-fr166-1-file-download-capability.sql` | A1、A2 | DEV 套 migration：`select count(*) from role_capabilities rc join capabilities c on c.id=rc.capability_id where c.name like '%.download'` 為 0（Administrator 靠 `*.*.*`）；無此能力點的帳號 `GET /file/access-token/<uid>?action=download` 403、`?action=preview` 200；Agent 執行紀錄報告下載端點同樣 403／200；BE 重啟實打；守衛測試綠；回寫明講「出貨基線待重產」 |
| 4 | profile 同步 | **B2** 主系統 profile 同步接線：心跳附加 profiles、心跳回報入表、profile 頁查每台 Agent 取得狀態、補更新端點 | BE | extend：`core/plugins/remote_agent.py` `heartbeat_extras` 加 A4 的 provider（照 `_agent_vulndb.py` 形狀新開 `_agent_profile_sync.py`）；心跳 payload `profiles` 入表走套件 service；新端點 `GET /detection-profiles/<uid>/agents`（每台 Agent：version、sha256、synced_at、error）與 `POST /remote-agents/<uid>/profiles/check`（經心跳要求 Agent 立即檢查，照 restart 旗標形狀）；守門 `detection.profile.read`／`detection.remote-agent.configure` | `core/plugins/remote_agent.py 319（heartbeat_extras 加一項）、186（兩端點登記）` | `core/plugins/_agent_profile_sync.py`、`api/remote_agent/routes/agent_profile_sync_route.py` | A4、A7 | DEV：上傳新版 profile → 123 Agent 下次心跳拉到 → `agent_profile_states` 有列、`GET …/agents` 看得到 ok；按補更新 → 心跳回應帶旗標 → Agent 清冷卻；BE 重啟實打；守衛測試綠 |
| 3 | 下載權限點 | **A1b** jedi-file-upload：登記表加 `FILE_ACTION_DOWNLOAD`、拆掉全局 `download_guard` 插槽 | jedi 套件 | extend：`domain/ports.py` 多一個動作常數；`FileUploadAdapters` 拿掉 A1 的全局插槽（守門責任下放認領者） | `jedi_file_upload/domain/ports.py`、`plugin/contract.py`、`api/routes/upload_file_route.py` |   | A1 | tests：download 票簽發前呼叫認領者的 `assert_can_access(file, FILE_ACTION_DOWNLOAD)`；沒有認領者的檔 404 |
| 3 | 下載權限點 | **A2b** jedi-remote-agent：`download-files` 範圍對到 `detection.remote-agent.download-files`、`CAPABILITIES` 申報它 | jedi 套件 | extend：`plugin/contract.py` 對照表改一列、能力點清單加一顆（含 home_route） | `jedi_remote_agent/plugin/contract.py` |   | A2 | tests：持該顆者票帶 `download-files`；守衛測試「套件宣告的能力點都在 seed」綠 |
| 6 | 下載權限點 | **B4** 主系統：7 顆客戶資料下載能力點 migration、9 類認領者下載時看自己那顆、上傳者本人免、範本 SSP 佐證只鎖下載 | BE | new：`core/plugins/_file_ownership.py::assert_download_capability` 一支共用；extend：9 支認領者各加一行呼叫；`event_code.py` 鏡像 6218～6227 | `core/plugins/file_upload.py`、各模組 ownership 檔、`common/enum/event_code.py`、`scripts/sql/manifest.tsv` | `scripts/sql/2026-10-29-fr166-1-download-capabilities.sql` | A1b、A2b、B1 | 無能力點帳號對他人證據 `?action=download` 403、`preview` 200；對自己上傳的 download 200；`role_capabilities` 對 7 顆為 0 |
| 6 | 下載權限點 | **A2c** jedi-remote-agent：`AgentEventCode` 補登 6218～6227 十個 Agent 本機事件碼 | jedi 套件 | extend：`common/code.py` 加十列（A3 暫定的碼正式登記） | `jedi_remote_agent/common/code.py` |   | A3 | tests：碼不撞、主專案鏡像一致 |
| 7 | 下載權限點 | **B5** 主系統匯出端點補 export 能力點：稽核計畫、POA&M、風險總表、原始碼掃描檔原檔 | BE | extend：各端點 `@require_capability("<模組>.export")`；migration 2 顆 | `api/project/routes/ap_export_route.py`、`api/project/routes/poam_objective_route.py`、`api/governance/routes/risk_register_route.py`、`manifest.tsv` | `scripts/sql/2026-10-29-fr166-1-export-capabilities.sql` | B4 | 無 `grc.risk.export` 帳號 `POST /governance/risks/export` 403 `GRC_403022`；管理員 200 xlsx |
| 8 | 下載權限點 | **F6** 主站匯出鈕看各自 export 能力點（稽核計畫／POA&M／風險總表） | FE | extend：三處匯出鈕加 `v-can`；`capabilities.js` 重產 | 三個 view、`src/config/capabilities.js` |   | B5 | 無能力點看不到匯出鈕；有則照常 |
| 7 | 下載權限點 | **F2** 全系統下載鈕分模組看各自 download 能力點、上傳者本人免：主站 10 處＋Agent 執行紀錄頁＋主控台檔案頁改預覽、執行紀錄對話框下載鈕；commit 重產的 capabilities.js | FE | extend：主站 `signedDownloadUtil.fileDownloadUrl` 簽票帶 `action=download`、`filePreviewUrl` 帶 preview；10 處下載鈕統一 `v-can="各模組 CAP.<模組>_DOWNLOAD"`（PrimeVue Menu 項用既有 `useMenuItemsCan`）；主控台 `Files.vue` 下載鈕改看 `me.scope` 含 `download-files`、加「預覽」鈕 reuse `ReportPreviewDialog`（svg／xhtml 不給看、顯示「此格式僅能下載」）；`Runs.vue` 對話框下載鈕同範圍；`runs.js canDownload` 改認 `download-files` | `src/utils/signedDownloadUtil.js 44`、`src/components/grc/JobExecutionDrawer.vue 1434（超標 3179 行：只改一行加 v-can）`、`src/components/grc/evidence-view/RoundEvidenceView.vue 80`、`src/views/feedback/system/SystemFeedbackView.vue 60`、`src/views/feedback/system/SystemFeedbackForm.vue 240`、`src/views/evidence-classification/components/EvidenceFilePreviewDialog.vue 135`、`src/composables/useSbomViewer.js 26`、`src/composables/useScanFileDownload.js 19`、`src/views/remote-agent/AgentLocalRunsView.vue 213`、`src/config/capabilities.js（B1 重產，本卡 commit）`、`console/views/Files.vue 90`、`console/views/Runs.vue 185`、`console/runs.js 82` |   | B1、A5 | 無 各模組 download 能力點 的角色：主站 10 處下載鈕全部不見、預覽照常；主控台檔案頁只有預覽、報告對話框無下載；有能力點的角色全部回來；`grep -rn 'DOWNLOAD_FILE\|openFileDownload\|fileDownloadUrl' src/ | grep -v v-can` 逐處說明；`npm run build` 與 `build:CONSOLE` 過；`grep border-left` 為 0 |
| 4 | Agent 日誌 | **F3** 主控台「日誌」分頁：依人／事件／日期篩，欄位比照主站使用者日誌頁 | FE | reuse：表格與篩選列照主站 `src/views/action-log/UserLogs.vue` 的欄位與寫法，分段用 SegmentButtons；資料來源 A6 端點；事件碼顯示用既有 i18n `lang.event_code.*`（grep 確認主站有）；只新開 view，不碰 `Shell.vue`（F1 已登記） | `console/router.js（logs 指向新 view）` | `console/views/Logs.vue`、`console/logsApi.js` | A6、F1 | 分頁對 123 實際有列；篩人／事件／日期區間有效；message 不顯示密碼；`grep border-left` 為 0；表格一格一值 |
| 5 | 代設工具帳密 | **B3** 主系統代設 Agent 工具帳密端點：在線才收、建 overwrite handover 單、主系統不落地；檢測工具頁 route 翻回並改名 | BE | extend：新端點 `POST /remote-agents/<uid>/tool-credentials {tool, fields}` 走 A4 `start_for_agent(tools=[tool], overwrite=True)`，fields 只進派工單 payload（既有 `build_payload` 從雲端密文讀 → 本卡加「一次性 fields 直接進單」分支，不存 `tenant_detection_tool_configs`）；Agent 離線（`is_online` 為 false）回 409 `GRC_409xxx`；守門 `detection.remote-agent.configure`；稽核 `AGENT_CREDENTIAL_SET` method=remote；migration 把 `tool-plugin-manage` 翻回 enable=1、名稱改「Agent 掃描工具設定」、綁 configure 能力點 | `core/plugins/remote_agent.py 186（端點登記）`、`common/code/grc_error_code.py`、`scripts/sql/manifest.tsv` | `api/remote_agent/routes/agent_tool_credentials_route.py`、`scripts/sql/2026-10-29-fr166-1-tool-page-agent-credentials.sql` | A4、A9 | 在線 Agent：POST 後 123 下次心跳收到單、本機值被蓋、回執 overwritten；離線 Agent 409；`select * from tenant_detection_tool_configs` 沒新增列；`system_logs` 有 6086 method=remote 且 message 無帳密；選單翻回且改名；守衛測試綠；回寫明講「出貨基線待重產」 |
| 5 | profile 同步 | **F4** 主站 profile 頁：每個版本展開「Agent 取得狀態」表＋「補更新」鈕 | FE | extend：`ScanToolResourcesView.vue`（82 行）加一個子元件掛點；新元件 `ProfileAgentStates.vue` 讀 B2 端點，表格一格一值（Agent 名、版本、同步時間、狀態 Tag），補更新鈕 `v-can="CAP.DETECTION_REMOTE_AGENT_CONFIGURE"` | `src/views/plugin/ScanToolResourcesView.vue 82`、`src/config/api/api.js`、`i18n` | `src/views/plugin/components/ProfileAgentStates.vue` | B2 | 上傳新版後表格顯示 123 從「待更新」變「已取得」；按補更新有 toast、Agent 下次心跳拉；離線 Agent 顯示「離線」不報錯；`grep border-left` 為 0 |
| 8 | 代設工具帳密 | **F5** 主站「Agent 掃描工具設定」頁：選 Agent、選工具、填帳密、送出；離線直接說連不上 | FE | extend：`ToolPluginManage.vue` 766 行**超標**：只留殼與路由，新邏輯進新元件 `AgentCredentialSetter.vue`；工具表單 reuse 既有 `tool_schemas`／`config_field_schema` 的表單渲染（grep 主控台 `SecretForm.vue` 與主站既有工具表單，二擇一抽共用、不第三份）；送出後顯示「已送出，等 Agent 下次心跳（最多 N 秒）」、輪詢 handover 進度（既有 `credential_handover` 欄） | `src/views/plugin/ToolPluginManage.vue 766（淨減）`、`src/config/api/api.js`、`i18n`、`src/router（頁名）` | `src/views/plugin/components/AgentCredentialSetter.vue` | B3、F2 | 選離線 Agent 送出 → 409 toast「這台現在連不上」；在線送出 → 幾十秒內狀態變「Agent 已收」；主系統 DB 無帳密；`wc -l ToolPluginManage.vue` 比 766 小；表單不自刻第三份；`grep border-left` 為 0 |
| 9 | 文件 | **D1** SPEC 與手冊補到現況：下載權限點、profile 同步、代設帳密、Agent 日誌、剩餘天數、語言切換；FE 角色矩陣多一顆 | BE | extend：`docs/spec-site/current/evidence/agent-console.md`、`remote-agent-manage.md`、`scan-tool-resources`（profile 頁）對應頁各加段＋檔頭變更紀錄一行；手冊 `docs/user-manual/agent/13-standalone-operation.md` 13.x 補操作；`docs/claude/reference_permission_system_sop` 若列能力點清單補 各模組 download 能力點 | `docs/spec-site/current/evidence/agent-console.md`、`docs/spec-site/current/evidence/remote-agent-manage.md`、`docs/user-manual/agent/13-standalone-operation.md` |   | F2、F3、F4、F5 | 六個主題各有一段；`grep -n '原本\|第 [0-9] 棒\|這次'` 命中 0；SPEC 頁 build 過 |
| 10 | 檢測基準＋AD 鎖 | **A10** Agent：總覽「檢測基準」摘要與燈；離線超過天數 AD 登入一併鎖（D9） | evidence-agent | extend：`profile_sync.summary()`、`overview._profiles_lamp`；`local_auth.operator_session_valid` 拿掉 AD 例外 | `core/profile_sync.py`、`core/profile_cache.py`、`core/console/overview.py`、`core/console/local_auth.py`、`core/heartbeat_status.py` |   | — | AD 登入過期後 401、已登入被踢；local-admin 照登；總覽 `info.profiles` 與 `profiles` 燈有值 |
| 10 | 檢測基準卡 | **F7** 主控台：總覽「檢測基準」卡＋立即檢查＋燈；離線鎖定時 AD 分頁一起灰 | FE | extend：`useVulndbCheck` 泛化成 `useSyncCheck` 兩張卡共用 | `console/views/Overview.vue`、`console/Login.vue`、`console/opsApi.js`、`console/overviewHealth.js`、i18n | `console/useSyncCheck.js`（改名） | A10 | 卡片顯示 N 天前／M 支 K 失敗；「主系統未提供」；文案不再說 AD 不受限 |
| 10 | Agent 日誌 | **A11** Agent：日誌覆蓋率——舊 jsonl 一次匯入、預覽記 6230、進頁／開工具表單／開執行紀錄詳情 6231～6233（D10） | evidence-agent | new：`audit_import.py`；extend：`audit.audit_once`、各分頁 API 掛 `page_viewed` | `core/console/audit.py`、`files.py`、`tools.py`、`runs_routes.py`、`scan_routes.py`、`log_routes.py`…、`core/app_factory.py` | `core/console/audit_import.py` | — | 123 升級後多 61 筆舊紀錄、jsonl 改名 `.imported`；走一圈每步一筆 |
| 10 | 工具設定頁 | **F8** 主站「Agent 掃描工具設定」頁重做：左選 Agent、右列工具卡、離線整頁鎖、拆舊目錄 | FE | new：`AgentPicker`、`AgentToolCard`、`EnabledToolsDialog`、`agentToolCards.js`；刪 `DetectionToolCards.vue` 等五檔 | `AgentCredentialSetter.vue`、`ToolPluginManage.vue`、i18n | 見左 | — | 版面照雛形；離線鎖；舊目錄不見 |

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

| # | 步驟 | 期望 |
|---|---|---|
| 1 | 用沒有任何 download／export 能力點的角色登主站 | 他人上傳的證據、SSP 佐證、SBOM、掃描檔、意見回饋、Agent 執行紀錄的下載鈕全部不見，自己上傳的仍可下載；預覽照常；稽核計畫／POA&M／風險總表匯出鈕不見 |
| 2 | 同角色跳轉 Agent 主控台檔案頁 | 只有預覽；svg 顯示「此格式僅能下載」且無下載鈕 |
| 3 | 給該角色對應模組的 download／export 顆 | 該模組的下載鈕回來；直接打 `?action=download` 簽票 200；拿 preview 票打下載端點 403 用途不符 |
| 4 | 123 拔網路 | 總覽「離線登入還能用 N 天」卡出現，N＝租戶設定−已離線天數；超過天數後離線密碼與 AD 分頁一起灰、AD 登入 401，本機管理員照登 |
| 5 | 主站上傳新版 profile | profile 頁該版本展開：123「待更新」→ 下次心跳後「已取得」；按補更新 → Agent 立即檢查；主控台總覽「檢測基準」卡顯示幾天前更新 |
| 6 | 主站「Agent 掃描工具設定」對 123 的 Nmap「⋮ → 設定」、儲存、再「⋮ → 測試連線」 | 儲存當場成功、卡片翻成已設定；欄位不合當場報哪一欄；測試連線結果寫進「最後測試」；123 主控台工具頁看到同一設定時間；主系統 DB 無帳密 |
| 7 | 123 離線時打開那台 | 右欄整頁鎖並說明連不上；連回 30 秒內自動解鎖 |
| 8 | 做完 1～7 開主控台「日誌」 | 每一步都有列（含預覽、進頁、開工具表單）、操作者正確、密碼欄 `***`；升級前的舊紀錄也在；「匯出 CSV」跳一次性密碼、下載加密檔；主站使用者日誌頁看到回傳的那幾筆 |
| 9 | 登入後切英文、重整 | 仍是英文 |
| 10 | 主系統升級後、Agent 未升時從主系統開主控台 | 登入頁說「這台 Agent 版本比主系統舊，請升級」，不是「入場券無效」 |

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

- **升級順序 Agent 先**：主系統簽的票帶 `download-files`，舊 Agent 整張拒收，持有者登不進主控台。與 FR-166 P3-0b 同一個坑。
- **票改用途會不會壞第三方呼叫**：`action` 預設 preview，只有 FE 的 `fileDownloadUrl` 一處改帶 download；其他簽票呼叫者行為不變。
- **profile 的 cache 路徑是凍結契約** `/data/content/cache/`：正式版靜 compose 掛可寫 volume；用源碼直接跑的開發機要自己把這個目錄給 Agent 帳號寫入權，否則每支 profile 回報 `PermissionError`。
- **QueueListener 執行緒上的 session scope**：A3 第一件事實測；不行就 handler 內顯式開 scope。
- **SQLite 寫表失敗不得讓請求失敗**：`DBLogHandler.emit` 整支 try/except 走 `handleError`，1.3.6 有、0.0.31 沒有，子類要補。
- **設定帳密走直推**：主系統要連得到 Agent 的 8443。落地版同內網沒問題；SaaS 版要客戶開放 8443 進來，否則設定、測試連線、取證據檔一起不能用（同一個前提）。
- **主系統送來的欄位一定照工具 schema 驗**：曾有人直打 API 存進錯的欄位名，卡片顯示已設定、掃描時才報缺少憑證。
- **profile 冷卻**：拉失敗每次心跳重拉會把頻寬吃光（vulndb 已踩過），冷卻照抄。
- **`JobExecutionDrawer.vue` 3179 行**：F2 只改一行加 `v-can`，不碰其他。

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

設計決策與拆卡；開工後現況看 SPEC 與手冊；每棒做了什麼看 `handoff/FR-166.1-LOG.md`。
