FR-157 · 設計文件 v1 · 2026-10-22(D1~D11 全數定案)

工具帳密留在客戶機房,存了什麼檔自己看得到

從主系統一鍵跳進 Agent 機上的主控台:填檢測工具帳密、瀏覽這台存的證據檔;兩個能力點決定看得到哪些分頁

管理員在主系統 Agent 清單按一下,新分頁直接進到 Agent 機上的主控台(免再登入)。檢測工具的連線帳密在那裡填、加密存 Agent 本機,主系統從此不存;派工時 Agent 用自己機上那份。同一個主控台有「檔案」分頁,列這台 Agent 實際存在 SeaweedFS 或磁碟上的證據檔,可查可下載。權限拆成「Agent 設定」與「Agent 檔案」兩個能力點,稽核人員只勾檔案就能進去看檔、看不到帳密分頁。本文件把 11 項決策展開到「照做就能交」的程度,拆成五個子需求、10 張實作卡。

D1~D11 全數定案 10 張實作卡、5 批 跨 4 repo:BE/FE/jedi-python-package/evidence-agent 推翻 FR-056 D9:帳密不再存雲端 陷阱:既有客戶的帳密要搬家,升級不能壞 陷阱:檔案頁權限是租戶層,看得到這台全部檔

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

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

變更紀錄

日期 變更 對應
2026-10-22 初版設計定案(D1~D11),拆 5 子需求 10 張實作卡 母卡 CM-3026,子卡 CM-3027~CM-3036

1. 需求背景與端到端

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 瀏覽器門。

做完之後

%%{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
圖 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'}}}%%
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 — 帳密的存放與使用

2. 分工概述(30 秒版)

整案一句話:使用者在主系統 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)

# 決策 定案 理由 被排除的方案
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 私鑰派生

D10 的限制要寫進手冊

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

UI 設計約束(沿用 FR-134/FR-156)

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


4. 現況接入點盤點

元件 現況座標 現況 本案動作 卡
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. 詳細設計

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 張實作卡)

排序以「決策者何時看得到畫面」為準:先讓主控台開得出來、檔案頁有真資料(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. 端到端驗收

# 步驟 期望
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. 風險與陷阱

風險 怎麼擋
瀏覽器連不到 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. 這份文件的定位

你想知道 看
PM 說什麼、選項怎麼推、D10 為什麼改 討論稿 §6.4
畫面長什麼樣、兩種身分差在哪 雛形(右上切身分)
每個決策為什麼 本文件 §3
開工要改哪些檔 本文件 §4、§6
入場券與 8444 門怎麼做的 FR-156 design E1
被推翻的帳密存雲端設計 FR-056 design D9
做到哪了 README 總進度、handoff/(開工後)