---
title: "Agent 獨立作業主控台 — 需求討論稿 (FR-166)"
brand: "Guidant AI · **FR-166** Agent 獨立作業主控台"
eyebrow: "FR-166 · 需求討論稿 v4 · 2026-10-26（D1～D16 已裁；§13 全案重新檢視；§14 全量工作清單 37 項給 PM 分階段）"
h1: "主系統不在，Agent 也要能自己站著"
subtitle: "直接打 Agent 網址就能登入、不靠任務就能按掃描工具、現場查連線問題、斷線不停工且復線自動補傳"
lede: "Agent 現在是主系統的手腳：所有事都要主系統發令才動，主系統斷線它就只剩心跳在等。老闆要它變成**能自己站著的工具箱**——客戶的 Agent 跟台北主系統斷了線，現場的人照樣能登入 Agent、手動按掃描、查「為什麼連不到那台設備」；未來高雄、台南、日本、新加坡各裝一台，平時自己用、報告再回傳總部。本稿分析五件事：怎麼登入（業界常見的五種模式比較）、掃描怎麼不經任務就能按、網路除錯要包哪些工具（有些公司看到 traceroute 會跳告警，要讓使用者自己勾）、斷線期間任務怎麼續做與補傳、以及這樣開門後怎麼不被人當跳板或駭客工具。"
chips: [
  {text: "D1～D16 已裁（2026-10-26）", kind: ok},
  {text: "§14：37 工項／約 50 卡／五階段，最小可出貨 28 卡，等 PM 砍", kind: warn},
  {text: "§13 全案重新檢視：18 條死結推演、15 條新情境、2 條資安規則例外", kind: accent},
  {text: "沿用 FR-157 主控台殼、FR-156 入場券與 8444 門", kind: accent},
  {text: "跨 4 repo：BE／FE／jedi-python-package／evidence-agent", kind: accent},
  {text: "陷阱：獨立掃描不可放寬授權目標清冊", kind: crit},
  {text: "陷阱：除錯工具是雙面刃，不包的東西比包的重要", kind: crit}
]
footer: "FR-166 · 需求討論稿 · 2026-10-26 v1 · 前作：[FR-157](../FR-157-2610-agent-local-settings-console/) 主控台殼與救援碼、[FR-156](../FR-156-2610-agent-side-sbom-scan-and-vulndb-sync/) 入場券與 8444 門、[FR-056](../FR-056-2607-detection-tool-integration/) 派工骨架、[FR-039](../FR-039-2606-distributed-file-agent/) Agent 本體 · 雛形：[prototype/index.html](prototype/index.html) · 現況描述來源：evidence-agent `core/`、jedi-remote-agent、DEV 本機 DB 唯讀查詢"
---

> 狀態：**v5**｜日期：2026-10-26｜已裁：D1～D23（D20～D22 是決策者看過十二幕雛形後補裁；D23 是手測時改裁）｜LDAP 三選一納入、憑證改首次信任釘住（推翻 CM-2239）｜§13 全案重新檢視｜§14 全量工作清單給 PM（§9 彙整）｜定案後落 design.md 再開卡
> 雛形：[`prototype/index.html`](./prototype/index.html)（六幕：直接打網址的登入頁、總覽（含離線橫幅）、獨立掃描、網路除錯、執行紀錄與待回傳、主系統端的離線操作者授權與自主執行紀錄；右上可切「在線／離線／未同步」看三種狀態的差別）

---

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

### 1.1 老闆與決策者說了什麼

| 日期 | 誰 | 說什麼 | 白話翻譯 | 本稿對應 |
|---|---|---|---|---|
| 2026-10-26 | 老闆 | Agent 跟主系統離線了，還是要能被手動觸發掃描或用 Agent 內其他功能 | Agent 要能自己站著，不是主系統的手腳 | §3 登入、§4 獨立掃描 |
| 2026-10-26 | 老闆 | 主系統在台北，會開放高雄、台南、日本、新加坡的點安裝，各區掃描與報告回傳，平時也能用我們的工具 | 多據點各自作業、報告彙整回總部 | §7 離線續傳、§8 主系統端（據點） |
| 2026-10-26 | 老闆 | 要有網路連線除錯工具（ping、traceroute 等），跟設備離線時能查原因，回報給我們或客戶網管 | Agent 要能當現場的網路診斷箱 | §5 網路除錯 |
| 2026-10-26 | 決策者 | 除錯時要讓使用者選要用哪些工具，有些公司會針對這些動作跳告警 | 診斷動作不能全自動一次全開 | §5.3 勾選與噪音標示 |
| 2026-10-26 | 決策者 | 不能被人當作跳板或駭客工具，設計時資安細節都要注意 | 開門的同時要把不該開的鎖死 | §6 資安守門 |
| 2026-10-26 | 決策者 | 登入方式提供幾種模式，最好是業界常見的 | 不自創登入機制 | §3.2 五種模式比較（D1） |
| 2026-10-26 | 決策者 | 萬一主系統沒先給、根本沒設過，這 100% 會發生；要防的是被當跳板 | 「誰能登入」與「能掃誰」是兩道門，後者不看身分 | §3.3 三層身分、§6.1 三道門（D1、D12） |
| 2026-10-26 | 決策者 | 都沒同步過就預設沒完成設定、拒絕用工具；用 Agent 管理員登進來查連線 | 未同步＝只准主系統連線診斷 | D12（已裁） |
| 2026-10-26 | 決策者 | 離線超過 30 天掃描也停 | 清冊可能早過期，只剩診斷 | D2（已裁） |
| 2026-10-26 | 決策者 | 是不是乾脆 Agent 也起一個 Postgres？ | 分析後裁不起，本機狀態收進同一個 SQLite | D11（已裁） |
| 2026-10-26 | 決策者 | 據點不需要呀，不就是 Agent 名稱就好 | 名稱本來就由管理員取，「高雄機房 Agent-01」一看就知道在哪 | D8（已裁：不做據點欄） |
| 2026-10-26 | 決策者 | 第二期的東西這次就要做，除非太龐大再討論；「看自己這台」做成 UI | mtr 式監測、網卡狀態、NetworkManager 狀態都進第一版；本機狀態是儀表板不是勾選項 | D5（已裁） |
| 2026-10-26 | 決策者 | tcpdump 呢？ | 分析後裁不包：偷聽整條線、EDR 必告警、擷取檔是外洩點；替代由埠測試／TLS／MTU 補 | D10（已裁） |
| 2026-10-26 | 決策者 | 有 UI 嗎？會知道斷在哪嗎？安裝時就連不通怎麼知道本體在哪？ | 結果頁＝路徑圖＋判讀卡；D6 補未同步只准三項 | §5.4、D6（已裁） |
| 2026-10-26 | 決策者 | 順便加重啟 Agent；從主系統發動為什麼不行？他一樣可以開視窗連進來 | 首腦改口：跳轉進主控台按重啟本來就是從主系統發動；另加心跳旗標 | D14（已裁） |
| 2026-10-26 | 決策者 | 白名單也是從主系統同步進來？ | 兩種白名單都從主系統來；CIDR 安裝時設底線、主系統只能收緊 | D9（已裁） |
| 2026-10-26 | 決策者 | 整個重新分析：死結、沒想到的情境、既有機制、開發與資安規範 | §13 | §13 |
| 2026-10-26 | 決策者 | LDAP 老闆會要，有現成的對不對？順便檢查那個功能有沒有缺漏 | 有（jedi-iam），登入本體對，但三個洞未修 | §3.5、§13.8 |
| 2026-10-26 | 決策者 | 要讓他選從主機同步還是自己設，各區可能不同 | LDAP 三選一：同步／自設／不接 | D17（已裁） |
| 2026-10-26 | 決策者 | CM-2239 說不勾的是憑證嗎？使用者不會有這個憑證，要嘛我們自動信任，這太工程師了 | 改「首次信任、之後釘住」，拿掉勾選框 | D18（已裁，推翻 CM-2239） |
| 2026-10-26 | 決策者 | 抽出來是可以被復用的嗎？要可以復用抽出來才有意義；輪子不要一直重造 | 抽 `jedi-ldap` 零相依套件，三個使用者 | D19（已裁） |
| 2026-10-26 | 決策者 | 先用最嚴謹的設計看有多少工，再跟 PM 討論分階段或剔除；切階段切內容要分清楚，不能戰線太長沒辦法銜接 | §14 全量工作清單 | §14 |
| 2026-10-26 | 決策者 | 第三方元件掃描也是 call Agent 執行，應該整個在 Agent 上執行，只是看使用者要用哪一台 | 實查它本來就全在 Agent 跑，本案只補 Agent 自己的入口；結果走既有 `source_scans` | §4.3b |
| 2026-10-26 | 決策者 | 所有的掃描工具都要可以在 Agent 上獨立執行，這個是不是沒放進去 | 七支有、ZAP 被首腦以清冊為由排除——改為把網址型目標納清冊，八支全開 | D3 修正 |

### 1.2 現況哪裡痛

實查 evidence-agent（主控台 FR-157 已上到 F2）與 jedi-remote-agent 後，五件事各自的現況：

| 需求 | 現況 | 差距 |
|---|---|---|
| 直接連到 Agent 操作 | 8444 主控台已存在，但進門只有兩條路：主系統簽五分鐘入場券跳轉、或登上 Agent 機下 `sudo guidant-agent-ctl console-login` 印一次性網址。直接打網址只看到「請從主系統開啟」指路頁 | 主系統斷線＝第一條路斷；第二條要能 SSH 進機器，現場網管多半做不到 |
| 離線時的身分 | Agent **不存任何人**：驗票用雲端公鑰，session 存記憶體，沒有帳號表、沒有密碼 | 離線登入要先回答「Agent 怎麼知道你是誰、你能做什麼」 |
| 獨立觸發掃描 | 八支工具（OpenVAS、OpenSCAP、Nmap、CINC Auditor、ZAP、SonarQube、第三方元件掃描、SSH 主機金鑰）全由心跳領 `pending_tasks` 再開執行緒跑；主控台「檢測工具」頁只有帳密設定與「測試連線」 | 沒有「選工具→填目標→按執行」的入口；執行結果的落點（任務證據）也不適用於手動執行 |
| 網路除錯 | 無。最接近的是各 connector 的「測試連線」（只測工具本身） | 整塊新做 |
| 斷線續傳 | 任務執行緒不受斷線影響會跑完；報告本來就存 Agent 自己的儲存（`_upload_report` 走本機 `file_upload_service`）；但**結果回報 `POST /result` 失敗只記 log**，程式註解寫「本階段無自動重派邏輯，留待觀察」。主系統端任務停在 `dispatched`，畫面不會告訴使用者 Agent 離線了 | 要補「回報失敗進待傳佇列、復線重送」與主系統端「等待 Agent 回傳」的狀態顯示 |

### 1.3 已有的零件（本案不用重做）

| 零件 | 在哪 | 本案怎麼用 |
|---|---|---|
| 8444 瀏覽器門＋主控台殼 | evidence-agent `core/browser_plane_server.py`、`core/console/`（session、routes、overview、tools、files、audit、heartbeat） | 登入頁、掃描頁、除錯頁、紀錄頁全掛在這套殼上，加分頁不加門 |
| 入場券跳轉 | jedi-remote-agent `agent_ticket_service`＋Agent `verify_console_ticket` | 在線時的首選登入路，照舊 |
| 機上救援碼 | `core/console/local_login.py`（sha256 落地、一次性認領、五分鐘） | 保留為「什麼都不行時」的最後一條路 |
| 授權目標清冊快取 | `core/allowed_targets.py`（心跳下發、持久化、從未收到＝一律拒） | 獨立掃描與網路除錯的目標**都走它**，離線沿用快取 |
| 八支 connector | `core/task_executor_connectors/*` | 獨立掃描直接呼叫同一支 connector，不另寫執行路徑 |
| 本機帳密儲存 | `core/credential_store.py`（Fernet、`secrets.key`） | 獨立掃描用本機帳密，與派工同一份 |
| 本機稽核＋心跳回傳 | `core/console/audit.py`、`core/console/heartbeat.py`（游標＋ack） | 所有新操作走同一條稽核路；離線時自然累積、復線補傳 |
| Agent SQLite | `upload_files` 一張表（FR-066 D2 起單檔） | 新增「本機執行紀錄」「待傳佇列」「本機操作者」三張表，同一個檔 |
| FE 主控台入口 | `compliance-manager-fe/console/`（Vite 第二入口，三個 view） | 加 view 不加入口 |
| 主系統 Agent 管理頁 | FE `RemoteAgentManage.vue`、jedi-remote-agent `remote_agents`（已有 `name`、`base_url`、`capabilities`、在線判定＝心跳間隔×3） | 加「據點」欄、「離線操作者」設定、「自主執行紀錄」分頁 |

### 1.4 不做什麼（本稿先劃線，決策者可改）

| 不做 | 原因 |
|---|---|
| Agent 自己建帳號、自己管密碼政策、自己管角色表 | 身分來源仍是主系統；Agent 只有一個固定的本機管理員（安裝時設）與主系統下發的離線操作者名單，**沒有「新增使用者」功能** |
| Agent 起 Postgres | 本機資料單機、單進程、幾千筆以內，SQLite 甜蜜點；FR-066 D2 拔掉 PG 的理由一條沒變；業界邊緣節點（Nessus scanner、Wazuh agent）都用嵌入式 DB（D11 已裁） |
| 多 Agent 之間同步設定或帳號 | 會經雲端，與 FR-157 D9 一致不做 |
| 獨立掃描放寬授權目標清冊 | 這條是防跳板的核心；離線不是放寬的理由（§6） |
| 包 nmap 全埠掃描、封包擷取、任意命令列、SSH 跳轉到別台 | 包了就是駭客工具（§5.4、§6） |
| Agent 本體離線升級 | FR-156 D4 已裁不做；弱點資料離線包走 FR-165 |
| 多因子（TOTP） | 決策者裁「第二期的這次就做、太龐大再議」——本項屬**太龐大**：要客戶每人裝驗證器、要備援碼流程、要主系統端綁定頁，是一張完整 FR 的量；且離線登入已有三層＋清冊門，多因子的邊際效益低。**列為明確不做，決策者可反對** |
| 自主執行紀錄掛成任務證據 | 牽動任務抽屜與證據生命週期（FR-163／164 剛改完）；本案先把紀錄與報告做出來，掛證據等決策者看過紀錄頁再決定形狀 |
| Agent 自簽憑證換客戶憑證 | FR-156 D9 範圍，不在本案重複 |
| 「據點」欄位或「區域」實體 | Agent 名稱本來就是管理員取的、一台 Agent 通常就是一個點；自主執行紀錄與清單照 Agent 名稱篩（D8 已裁） |

---

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

整案：**Agent 主控台多三個分頁（掃描工具、網路除錯、執行紀錄）與一頁登入，主系統連得上時照舊跳轉進去，連不上時用主系統事先授權的本機帳號登入；掃描與除錯直接在 Agent 上按、結果先存 Agent，連回主系統後自動補傳到「Agent 自主執行紀錄」。**

| 子需求 | 做什麼 | 產出 | 完成怎麼判定（決策者檢查法） |
|---|---|---|---|
| **A 登入與離線身分** | 直接打 Agent 網址有登入頁；主系統授權「誰能離線登入哪台」，在線跳轉時順手設本機密碼；離線用帳號＋本機密碼；救援碼保留；鎖定與有效期 | 登入頁＋主系統授權設定＋Agent 本機操作者表 | 把 Agent 機的網路對主系統切斷（拔 VPN 或改 hosts），瀏覽器直接打 `https://<agent>:8444/console` 看到登入頁，用帳號＋本機密碼登得進去，畫面上方有「主系統連線中斷」橫幅；沒被授權的帳號登不進去；連續打錯五次鎖十五分鐘 |
| **B 獨立掃描** | 主控台「掃描工具」頁：選工具→從清冊挑目標→填參數→執行；結果存 Agent 本機紀錄；復線回傳主系統 | 掃描工具頁＋本機執行紀錄 | 離線狀態下對清冊內一台主機跑 OpenSCAP，看到進度與完成、能下載報告；對清冊外 IP 執行被擋且訊息寫「不在授權目標內」；接回主系統後在 Agent 管理頁「自主執行紀錄」看到這筆並能下載同一份報告 |
| **C 網路除錯** | 主控台「網路除錯」頁：挑目標、勾工具（每項標噪音等級，預設只勾安靜的）、執行、看結果、匯出或回傳 | 網路除錯頁＋診斷報告 | 對一台拔了網路線的設備跑 ping＋traceroute，結果顯示在哪一跳斷；traceroute 旁有「可能觸發 IDS 告警」標示且預設不勾；報告匯出 PDF／文字，接回主系統後在自主執行紀錄看得到 |
| **D 離線續傳** | 任務結果與報告回報失敗進 Agent 待傳佇列，心跳成功後依序重送；主系統端收重複回報不炸；任務畫面顯示「Agent 離線，等待回傳」 | 待傳佇列＋主系統端狀態顯示 | 主系統派一支檢測給 Agent 後立刻切斷 Agent 對主系統的網路，等掃描跑完，主系統任務顯示「Agent 離線、等待回傳」不是失敗；接回網路兩個心跳內任務變成功、報告與證據都在 |
| **E 主系統端配合** | 離線操作者授權頁；「自主執行紀錄」分頁（可下載報告、可篩 Agent／工具／人）；Agent 清單顯示待傳件數與「離線操作者 0 位」警示 | 主系統三處改版 | 授權頁能勾人並選範圍；自主執行紀錄能依 Agent 名稱篩到「高雄機房 Agent-01」的掃描；清單對沒勾人的 Agent 顯示紅字 |
| **F 文件與出貨** | SPEC、手冊（含「哪些除錯動作可能觸發客戶告警」一節）、安裝文件（CIDR 白名單） | 文件改成現況 | 手冊「Agent 獨立作業」一章有登入三條路、離線能做什麼不能做什麼、除錯工具噪音表 |

資安守門（§6）不是獨立一棒，是 A～E 每棒的驗收條件；收口時 ddd-compliance-reviewer 與資安規則檔一起掃。依賴：FR-157 主控台殼（已上到 F2）、FR-156 E1（已 Done）。**決策者功課**：D1 登入模式、D5 除錯工具清單要拍板。

---

## 3. 直接進門與登入 {#login nav="登入"}

### 3.1 現況的門怎麼開、哪裡斷

FR-157 D1 裁「Agent 不開登入表單」，前提是**主系統一定連得到**。本案的前提剛好相反：主系統連不到是常態之一。FR-157 的三條路在離線時：

| 路 | 離線時 |
|---|---|
| 主系統簽入場券跳轉 | 斷（主系統本身連不到） |
| 機上 `console-login` 救援碼 | 可用，但要能 SSH 上 Agent 機且有 sudo；現場網管多半沒有 |
| 直接打網址 | 只看到指路頁 |

所以「離線登入」要新開一條路，而這條路的核心問題不是表單怎麼畫，是 **Agent 離線時憑什麼認得你**。

### 3.2 業界常見的五種模式（D1）

| | 模式 | 業界例子 | 離線能用 | 帳密存哪 | 主系統撤銷後多久失效 | 優 | 劣 |
|---|---|---|---|---|---|---|---|
| **A** | 主系統跳轉（現況） | Okta／AD 的 SSO 跳轉 | ✗ | 不存 | 即時 | 零新攻擊面、不用記網址 | 主系統斷就進不去 |
| **B** | Agent 自己的本機帳號 | Nessus、Wazuh、防火牆與交換器的管理頁 | ✓ | Agent（雜湊） | 不會——主系統不知道這些帳號存在 | 最單純、離線完全獨立 | 第二套帳號表：誰有帳號主系統不知道、離職沒人刪、密碼政策各管各的 |
| **C** | **主系統授權的本機帳號（快取式）** | Windows 網域帳號的「快取登入」（筆電離開公司網路仍能登）、1Password／Bitwarden 離線解鎖 | ✓ | Agent 只存「主系統授權過的人」名單＋各人**另設的本機密碼**雜湊；名單隨心跳同步 | 在線時下一次心跳即時；離線時靠有效期自動到期（D2） | 名單只在主系統一處管；離線能用；授權到期自動失效；不把主系統密碼雜湊送出去 | 使用者要在線時先設一次本機密碼（跳轉進來時順手設）；離線期間撤銷不了 |
| **D** | 接客戶的 AD／LDAP | Nessus、GitLab、Jenkins 的 LDAP 登入 | ✓（AD 通常與 Agent 同內網） | 不存 | AD 停用帳號即生效 | 客戶 IT 最習慣；離職流程走 AD 就好 | 要設定（LDAP 網址、bind 帳號、群組對應）；小客戶沒 AD；Agent 多一個對外連線與一組帳密要存 |
| **E** | 一次性救援碼（現況 CLI） | break-glass、設備的 console 口 | ✓ | 不存 | 一次性 | 零設定 | 要登機器下 sudo；不適合日常 |

還有兩個常被問到的，本稿先排除：**硬體金鑰／客戶端憑證**（瀏覽器匯入憑證才能進——落地版客戶會抱怨麻煩，且撤銷要管 CRL）、**Agent 自己接 OIDC 到雲端 IdP**（離線時一樣斷，等於 A）。

**多因子（TOTP 驗證碼 App）**：B／C／D 都能疊。業界獨立管理台（Nessus、pfSense）多半有，但要客戶每個人裝驗證器 App。本稿建議列第二期選配，不綁第一版。

### 3.3 定案：三層身分（D1 已裁）

決策者點出的關鍵：**「主系統沒先給、根本沒設過」100% 會發生**。這種情境下 Agent 只有兩種選擇——只認安裝時設的那組密碼，或自己放寬讓任何主系統帳號試（離線時驗不了主系統密碼，等於讓人猜，就是跳板的開端）。沒有第三條路；業界所有有管理頁的設備（Nessus scanner、FortiGate、UniFi）都走第一種。所以登入分三層，從「最後保障」到「日常」：

| 層 | 帳號 | 怎麼來的 | 能做什麼 | 什麼時候用 | 過期／撤銷 |
|---|---|---|---|---|---|
| **① 本機管理員** | `local-admin`（固定一個） | **安裝時強制設密碼**，不設不給裝完；既有客戶升級時互動終端照問、無人值守不強制改紅字提醒（D13）；12 碼以上 | 全部分頁；**但掃描範圍照樣過清冊（§6.1 門 2），未同步時只准主系統連線診斷（D12）** | 斷線、剛裝好、名單裡沒人、其他路都不通 | **永不過期、不受離線停用**；改密碼在主控台內或機上 `guidant-agent-ctl reset-admin-password`（sudo） |
| **② 主系統授權的離線操作者** | 主系統帳號 | 管理員在主系統勾人＋範圍＋到期日，心跳同步名單到 Agent；本人在線時進主控台設一組本機密碼（不是主系統密碼，只存雜湊在這台） | 名單上寫的範圍 | 日常離線操作（各據點網管） | 到期日自動失效；離線 30 天停用；在線時撤銷下次心跳生效 |
| **③ 主系統跳轉** | 主系統帳號 | 入場券（FR-157 現況） | 票上的範圍 | **在線時的首選** | 即時 |

救援：機上 `sudo guidant-agent-ctl console-login` 一次性網址（現況已有），給「本機管理員密碼也忘了」的情況。忘密碼的恢復路徑是「有機器 sudo 權限的人」，與所有設備忘密碼要接 console 口同理。

```{.mermaid cap="圖 1 — 三層登入：在線走跳轉（順手設離線密碼），離線用離線密碼或本機管理員，什麼都不行用救援碼"}
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
sequenceDiagram
    participant I as 安裝者
    participant U as 使用者瀏覽器
    participant M as 主系統
    participant AG as Agent 8444
    Note over I,AG: 安裝時
    I->>AG: install.sh 強制設 local-admin 密碼（只存雜湊）
    Note over M,AG: 在線時（平常）
    M->>AG: 心跳回應帶「離線操作者名單」：誰、範圍、到期日（沒有主系統密碼）
    U->>M: Agent 管理頁按「開啟主控台」
    M-->>U: 五分鐘入場券
    U->>AG: 帶票進門
    AG-->>U: 發 session；在名單且未設離線密碼 → 提示設定
    U->>AG: 設離線密碼（只存雜湊）
    Note over U,AG: 離線時
    U->>AG: 直接打 https://agent:8444/console
    AG-->>U: 登入頁（橫幅：主系統連線中斷）
    U->>AG: 主系統帳號＋離線密碼，或 local-admin＋管理員密碼
    AG->>AG: 名單內？未到期？密碼對？沒被鎖？（local-admin 只驗後兩項）
    AG-->>U: 發 session；掃描範圍仍照清冊，未同步只准診斷
    Note over U,AG: 什麼都不行時
    U->>AG: 機上 sudo console-login 印一次性網址
```

設計點：

- **名單由主系統下發，Agent 只做快取**，名單只有帳號、顯示名、範圍、到期日，**沒有主系統密碼雜湊**。Agent 端沒有任何「新增使用者」功能，①只有一個固定帳號——Agent 永遠不會長出主系統看不見的身分。
- **離線密碼是另一組**：主系統密碼雜湊不該離開主系統（FR-157 的動機）；一人授權多台各設一組可接受（與 FR-157 D9 同理）；在線跳轉進任一台都能重設該台密碼。
- **登入頁版面照主系統 `Login.vue`**（左 44% 表單、右品牌面板、底線式分頁），改 Agent 模式：分頁依連線狀態變——在線＝主系統登入（首選，跳回主系統驗證）／AD／離線密碼；離線＝AD／離線密碼／本機管理員；未同步＝只有本機管理員可按。右面板主角是「這台 Agent 卡」（名稱、IP、版本、角色、連線狀態）加一句狀態提示，不放 hero 標語；拿掉記住我、忘記密碼、captcha。FE console 的 `Login.vue` 直接從主站複製版面與樣式再刪分流邏輯。
- **主控台殼改左側邊選單，結構照主系統、身分靠四個訊號分**（決策者 2026-10-26 裁：「結構一樣、身分明顯不一樣」）。FR-157 的頂部 TabView 在三頁時合理，七頁不夠放。側欄 14rem、頂欄 4rem、麵包屑、側欄底部使用者區，照主站 `AppSidebar`／`AppTopbar` 複製版面（console 不載 router 與 Pinia，選單項寫死在 console 自己的清單）。分組：總覽／作業（掃描工具、網路除錯、執行紀錄）／設定（檢測工具、身分來源、檔案）／維運（服務與重啟）；未同步時作業與設定項反灰。四個環境訊號：①側欄用主站現成的 teal 主題色（`--menu-bg: #00695C`），主站側欄跟亮暗走、Agent 固定 teal；②側欄頂「AGENT」字樣＋這台的名稱與狀態點（主站同位置是租戶／專案）；③內容區頂部常駐狀態列「AGENT · 高雄機房 Agent-01 · 主系統連線正常／中斷／未同步」，不是只有異常才出現；④瀏覽器分頁標題「Guidant AI Agent · 高雄機房 Agent-01」加 favicon 角標。同一套產品的 prod／staging 就是這樣分，不靠版面不同辨識。
- **總覽＝dashboard，不另開 Dashboard 頁**：要處理的事放最上面（主系統連線、待回傳積壓、磁碟低於門檻、清冊過舊、帳密儲存損壞、有掃描被拒絕），其他正常收成一行；網路一行四個點（網卡、閘道、DNS、主系統）；正在執行；最近五筆；這台的靜態資訊放最下。主系統端 Agent 清單本身就是跨台 dashboard，不另做「Agent 總覽儀表板」；跨據點統計圖列第二期報表。
- **「沒設過」不能是靜默狀態**：主系統 Agent 清單對「離線操作者 0 位」顯示紅字警示；安裝完成畫面與手冊要求把 local-admin 密碼交給現場負責人保管。
- **local-admin 不是萬能帳號**：它只過門 1；門 2（清冊）與門 3（工具清單）不看身分（§6.1）。攻擊者拿到 local-admin 能做的是對清冊內已授權設備跑既有工具，不能掃任意 IP、不能下指令；未同步過的 Agent 連掃描都跑不了。

::: {.callout .decided}
**D1 離線登入模式（已裁 2026-10-26）**

三層：安裝時強制設的本機管理員 `local-admin`（永不過期、不受離線停用、仍過清冊）＋主系統授權的離線操作者（C 模式，日常）＋入場券跳轉（在線首選）；救援碼保留。LDAP／AD 列第二期選配；純本機多帳號不做；多因子第二期。
:::

::: {.callout .decided}
**D2 有效期、鎖定與離線停用（已裁 2026-10-26）**

離線操作者授權預設 90 天、可選 30／180／不限；**離線超過 30 天**（最後一次心跳成功起算）離線操作者登入與**獨立掃描一併停用**（清冊可能早過期），只剩 local-admin 可登、只准網路除錯與主系統連線診斷；主系統派的任務若復線後才領到照常跑。登入失敗 5 次鎖 15 分鐘並寫稽核；密碼最短 12 碼；local-admin 永不過期。
:::

### 3.4 登入後能做什麼：在線、離線、未同步三種狀態

「未同步」＝這台 Agent **從未收過**授權清冊或離線操作者名單（剛裝好還沒註冊、或註冊後第一次心跳還沒成功）。決策者裁：未同步就預設沒完成設定，拒絕用工具，只准 local-admin 登進來查連線（D12）。

| 功能 | 在線 | 離線（30 天內） | 離線超過 30 天 | 未同步 |
|---|---|---|---|---|
| 登入 | 跳轉（首選）／離線密碼／local-admin | 離線密碼／local-admin | **只有 local-admin** | **只有 local-admin** |
| 總覽、檔案、工具帳密（FR-157 既有） | ✓ | ✓ | ✓ | 總覽只顯示註冊狀態與診斷入口 |
| 獨立掃描（§4） | ✓ | ✓（清冊用快取） | **✗ 停用** | **✗**（從未收到清冊＝一律拒，既有行為） |
| 網路除錯（§5） | ✓ | ✓ | ✓ | **只准主系統連線診斷**（心跳端點、憑證、時鐘、閘道、DNS） |
| 看主系統派的任務進度 | ✓ | ✓ | ✓ | — |
| 執行紀錄回傳主系統 | 即時 | 進待傳佇列 | 進待傳佇列 | — |
| 清冊、名單、工具定義更新 | 心跳即時 | 用快取；畫面標版本與日期 | 用快取 | 無 |
| 弱點資料更新 | 心跳得知自取 | 不更新 | 不更新 | 無 |

::: {.callout .decided}
**D12 未同步狀態（已裁 2026-10-26）**

沒收過清冊或名單＝未完成設定：只有 local-admin 能登，只准主系統連線診斷，畫面寫「尚未完成與主系統的同步，請先連上主系統」。**這是防跳板的核心**——沒被主系統認可的 Agent 不管誰登入都掃不了任何東西。
:::

### 3.5 第四層：接客戶 AD／LDAP（D17、D18、D19 已裁）

決策者判斷老闆會要。實查現況與缺漏在 §13.8；這裡講設計。

**三選一**（各區可能不同 AD——總部一個網域、新加坡子公司自己的、日本點用 Google Workspace 沒有 AD）。主控台「檢測工具」旁同形狀多一張「身分來源」卡：

| 選項 | 適合誰 | 設定存哪 | Agent 回報 |
|---|---|---|---|
| **跟主系統同步（預設）** | 總部與主系統同一個 AD | 主系統心跳下發 `ldap_config`，Agent 不存設定；服務帳密 Fernet 存 `credential_store` | `ldap_source=synced` |
| **這台自己設** | 分公司有自己的 AD | Agent 本機，形狀同工具帳密（FR-157 已做：Fernet 儲存＋測試連線＋釘住指紋） | `ldap_source=local` ＋伺服器位址，主系統清單看得到哪台接了哪個目錄 |
| **不接** | 這個點沒有目錄服務 | — | `ldap_source=none` |

**守住的點**：LDAP 只取代「密碼怎麼驗」那一步，**不決定「誰能登」**——名單仍是主系統下發的離線操作者；名單內的人可選用 AD 密碼登 Agent、不必另設離線密碼；**名單不在、AD 密碼對也不給進**。AD 也連不到時退回離線密碼或 local-admin，登入頁一個帳號欄，Agent 自己判斷。

**憑證（D18）**：客戶多半沒有 CA 憑證可貼，「驗證憑證」勾選框是工程師思維。改成 **首次信任、之後釘住**（SSH `known_hosts` 模式，專案 SSH 連線已是 `RejectPolicy`＋`known_hosts`）：第一次連自動記下伺服器憑證指紋、畫面顯示「已信任 xxx 的憑證（到期 2027-03）」；之後指紋變了就擋並紅字「伺服器憑證變了，若非你們更換憑證請聯絡網管」，管理員按「重新信任」才放行。拿掉勾選框，只留「貼上 CA」當進階選項。加密方式預設 STARTTLS，連不上自動退 389 並黃字「目前是明文連線」。使用者零操作、零憑證知識，中間人攻擊擋得住。**此條推翻 CM-2239**（當時裁預設不勾的理由正是「客戶沒憑證」，釘住模式同時滿足兩邊）。主系統 LDAP 設定頁與 Agent 端都改。

**抽套件（D19）**：`ldap_adapter.py` 兩百多行混了兩種東西——目錄連線（約 180 行，只靠 `ldap3`）與主系統帳號對應（約 30 行，查 `user_auth_providers`／`users` 表，繼承 `BaseRepository`）。Agent 沒那兩張表，裝整支 jedi-iam 要拖 Flask-RESTful 全套且 `jedi-common` 版本線衝突（Agent 釘 0.0.31、jedi-iam 要 ≥1.3.4——jedi-masking 抽成零相依套件的理由相同）。

```
jedi-ldap（新，零相依只靠 ldap3）
  LdapDirectory(config).verify_user(username, pw) → DirectoryUser(uid, name, email, groups)
  LdapDirectory(config).test_connect() → 結構化結果（連不到／憑證變了／帳號格式／密碼錯）
  LdapConfig dataclass；首次信任釘住（指紋算與比對；存哪由呼叫端給 store）
  加密預設 STARTTLS；不丟 Flask 例外
jedi-iam.LdapAdapter  → 瘦成 30 行：呼叫 jedi-ldap 拿 DirectoryUser，再查綁定表；失敗計數補 LDAP 路徑
evidence-agent        → 裝 jedi-ldap，不裝 jedi-iam
```

**復用性**（決策者：「要可以復用抽出來才有意義」）：判準是不綁任何一張表、不綁 Flask、只靠 ldap3。現在就有三個使用者——主系統登入（jedi-iam 瘦身後）、Agent 離線登入（本案）、未來任何「用客戶 AD 驗身分」的地方（FR-132 存取管理若做 AD 群組對應角色、License Center 若接客戶目錄）。C11 標的「跳脫寫在兩處」「鎖定不計入」「加密預設無」三個洞抽出來後只修一處。

::: {.callout .decided}
**D17 Agent LDAP 來源三選一（已裁 2026-10-26）**

同步（預設）／自設／不接；名單仍由主系統決定，LDAP 只驗密碼；自設的回報主系統清單可見。
:::

::: {.callout .decided}
**D18 LDAP 憑證改首次信任釘住、加密預設 STARTTLS（已裁 2026-10-26，推翻 CM-2239）**

拿掉「驗證憑證」勾選框；第一次連自動記指紋、變了就擋並要人「重新信任」；「貼上 CA」留進階。主系統設定頁與 Agent 端同步改。
:::

::: {.callout .decided}
**D19 抽 `jedi-ldap` 零相依套件（已裁 2026-10-26）**

目錄連線段抽出；jedi-iam 瘦身呼叫它；Agent 裝它不裝 jedi-iam。套件異動清單見 §14.3 P0-1～P0-3。
:::

---

## 4. 獨立掃描（不經任務） {#scan nav="獨立掃描"}

### 4.1 老闆說的「像第三方元件掃描的模式」是什麼

FR-134／156 的第三方元件掃描是主系統側選單一個獨立模組：使用者挑 Agent、上傳源碼或 SBOM、按掃描，**不必先有稽核任務**，結果自己一張表（`compliance.source_scans`）、有自己的報表頁，之後要當證據再掛到任務。本案要把這種「工具自己能按」的形狀搬到 Agent 上，而且對八支工具通用。

### 4.2 畫面長什麼樣（雛形幕 3）

主控台「掃描工具」分頁：

1. **工具卡**：八支工具一人一卡，顯示「已設定帳密／未設定」「這台能跑」（capabilities）；未設定的卡按下去先導去帳密分頁。
2. **目標**：從授權清冊挑（設備名＋IP，清冊版本與更新時間標在旁邊）；允許手打 IP 或網段，但**必須落在授權網段內**，否則當場擋（D3）。
3. **參數**：照工具目錄的 `param_schema` 長表單（與主系統派工表單同一份定義，心跳下發、Agent 快取——FR-157 D3 同一機制）。
4. **執行**：按下去進本機執行紀錄，進度與取消比照派工（同一支 connector、同一個 cancel_event）。
5. **完成**：摘要（發現幾項）、報告下載、「回傳主系統」狀態三種：已回傳（附時間）／待回傳／回傳失敗（附原因，例如主系統回 413）。在線時可按「立即重試」不等下一次心跳；離線時按鈕反灰、連回後自動補傳，急著要報告用「匯出 PDF」帶走。主系統端「手動匯入 Agent 報告」列第二期。畫面上來源一律寫「Agent 手動」（不寫「本機」，使用者分不出什麼本機）。

### 4.3 Agent 內部怎麼跑

不另寫執行路徑：手動執行＝自己造一張「本機任務」，走 `task_executor._run_one` 同一條（ack 與 result 的「雲端」改成本機紀錄表）。差異只有三處：任務來源標 `local`、沒有 `job_execution_uid`、結果回報對象是本機紀錄＋待傳佇列。

兩支工具的範圍（§13.1 實查）：**Nmap 的執行主機**走 `params.hosts`，`_run_one` 呼叫 connector 前對 `params` 整包 `allowed_targets.check()`，執行主機**本來就過清冊**，手動模式沿用同一條不必另做。**ZAP 的網址**是 FR-129 刻意留的缺口（`allowed_targets.py` 檔頭明寫網址不在授權範圍）；手動模式若照現況開 ZAP 等於開一條對任意網址發掃描的路。決策者裁「所有掃描工具都要能在 Agent 獨立執行」，所以**不是拔掉 ZAP，是把網址型目標納進清冊規則**：Agent 端 `allowed_targets.py`（203 行）把 ZAP 的網址欄位加進比對欄位，`urlsplit` 取 hostname → `getaddrinfo` 解成 IP → 過同一張清冊；解不到或不在網段內就拒。主系統清冊不用改（網址最後仍落到 IP）。主系統派工的 ZAP 也一併受這條管（現況不過清冊）。並發上限：同一台 Agent 同時最多 2 個掃描、1 個診斷，超過排隊；執行不綁 session，session 過期掃描繼續。

本機紀錄表（Agent SQLite 新增 `local_runs`）：`uid`、`kind`（scan／diag）、`tool`、`targets`、`params`（不含帳密）、`started_by`（本機操作者帳號）、`status`、`summary`、`report_file_uid`（走既有 `upload_files`）、`created_at`、`finished_at`、`synced_at`、`cloud_uid`。

### 4.3b 八支工具都在 Agent 執行，差別只在入口與結果落點

決策者點出：第三方元件掃描「也是 call Agent 執行，應該整個在 Agent 上執行，只是看使用者要用哪一台」。實查現況確實如此——FR-156 之後它的執行就全在 Agent（主系統只剩開單、簽直傳票、收結果出報表），**不存在「移過來」的問題**；本案要補的是**Agent 自己的入口**。這與其他七支工具是同一個形狀：

| | 主系統入口（在線） | Agent 入口（本案新增，離線也能） | 執行在哪 | 結果回主系統落哪 |
|---|---|---|---|---|
| OpenVAS／OpenSCAP／Nmap／CINC／SonarQube／SSH 金鑰 | 派稽核任務 | 掃描工具頁 | Agent | `agent_local_runs`（主系統沒有這些工具的獨立報表頁，派工結果本來是任務證據） |
| 第三方元件掃描 | 側選單獨立模組（選 Agent→直傳源碼→掃） | 掃描工具頁（源碼直接丟這台 8444） | Agent | **`source_scans` 既有收件路徑**（主系統已有它的結果表與報表頁，台北在既有的報表頁就看得到高雄手動掃的那次） |
| ZAP | 派稽核任務 | 掃描工具頁（網址解析成 IP 過清冊） | Agent | `agent_local_runs` |

給 runner：**八支都在 Agent 執行、都有兩個入口；差別只在結果回傳落哪張表**。第三方元件掃描手動模式不是第二套，是在 `source_scans` 多一個「來源＝Agent 手動」的值。

### 4.4 結果回主系統要落在哪（D4）

| 選項 | 做法 | 優 | 劣 |
|---|---|---|---|
| **a. 主系統新表「Agent 自主執行紀錄」＋Agent 管理頁一個分頁（建議）** | 心跳或復線時 Agent 把 `local_runs` 推上去，主系統存 `compliance.agent_local_runs`（含報告檔參照，報告本體仍在 Agent）；畫面在 Agent 管理頁「自主執行紀錄」分頁，可篩 Agent／工具／人／日期、下載報告 | 與任務體系分開、不污染證據；多台 Agent 彙整一頁看完 | 要掛成任務證據要再做一步 |
| b. 直接塞進 `detection_executions`（任務型檢測結果表） | 補 `job_execution_uid` 可空 | 報表頁現成 | 那張表與任務一對一，到處假設有任務；空值會讓既有頁面炸 |
| c. 只留 Agent 本機、不回傳 | 最省 | — | 老闆明說要回傳 |

「掛成證據」：有了 a 之後，第二期在任務抽屜加「從 Agent 自主執行紀錄挑一份當證據」，複製參照即可。第一版不做。

::: {.callout .decided}
**D3 獨立掃描的目標範圍（已裁 2026-10-26，同日修正）**

與派工同一條規矩——只能掃授權清冊（設備清冊 IP＋授權網段）內的目標，離線沿用快取清冊，不因離線放寬；手打目標必須落在授權網段內；要掃新設備到主系統清冊加、等下一次心跳。**八支工具全部可在 Agent 手動執行**（決策者：所有掃描工具都要能在 Agent 獨立執行）；ZAP 的網址解析成 IP 後過同一張清冊，Agent 端 `allowed_targets.py` 補這段。
:::

::: {.callout .decided}
**D4 獨立掃描結果落點（已裁 2026-10-26）**

a：主系統新表 `compliance.agent_local_runs`（jedi-remote-agent）＋Agent 管理頁「自主執行紀錄」分頁，報告本體留 Agent、主系統只存參照；掛成任務證據列第二期。
:::

---

## 5. 網路連線除錯 {#diag nav="網路除錯"}

### 5.1 要回答的問題

「Agent 跟某幾台設備連不到，原因是什麼？」拆開來是四個層次：這台 Agent 自己的網路正不正常（介面、閘道、DNS）→ 到目標的路通不通（ping、traceroute）→ 目標的服務開不開（連接埠、TLS、HTTP）→ 跟主系統之間通不通（心跳端點、憑證、時鐘）。工具就照這四層挑。

### 5.2 兩區：這台 Agent 的網路（儀表板）＋對目標做診斷（勾選）

決策者裁：「看自己這台」做成 UI，不是勾選項。網路除錯分頁分上下兩區。

**上半區「這台 Agent 的網路」**：進頁自動載入、不用按、不對外送任何封包、無噪音。右上「重新整理」「複製成文字」（貼給網管）。

| 卡片 | 顯示什麼 | 來源 | 容器形態 |
|---|---|---|---|
| 網卡 | 每張一卡：IP／遮罩／MAC／速率／up-down（綠灰點） | `ip addr`、`ethtool`（速率） | `ethtool` 要 `network_mode: host` 或掛 `/sys/class/net`，讀不到標「此部署形態不可用」 |
| 路由表 | 目的／閘道／介面，預設路由標示 | `ip route` | ✓ |
| DNS | 用哪幾台、順序 | `/etc/resolv.conf`、`resolvectl` | ✓ |
| 鄰居表 | 閘道 MAC 學到沒（沒有＝紅字「二層不通」） | `ip neigh`（只讀） | ✓ |
| 這台在聽的埠 | 8443／8444 兩列，綠＝在聽 | `ss -tuln` 只列 Agent 自己的 | ✓ |
| 時鐘 | 本機時間、與主系統偏移秒數（既有 `clock_skew`）、NTP 狀態 | `timedatectl` | 容器內 NTP 狀態不可用 |
| 代理 | 有無、指向哪 | 環境變數 | ✓ |
| 網路服務 | NetworkManager／systemd-networkd 狀態 | `nmcli`／`networkctl` | 原生可讀，容器標不可用 |
| 主系統連線 | 最後心跳、狀態（正常／連不到／連得到但被拒）、清冊版本 | 既有 `heartbeat_status`、`allowed_targets` | ✓ |

**下半區「對目標做診斷」**：選目標 → 勾項目 → 執行。每項標噪音等級（**安靜**＝正常應用程式天天在做；**中**＝某些 IDS 規則會記；**高**＝多數監控當掃描告警）。

| 項目 | 回答什麼 | 噪音 | 預設 | 實作與逾時 |
|---|---|---|---|---|
| ping | 通不通、延遲、掉包 | 安靜 | ✓ | `ping -c 4`，上限 10 次；10s |
| DNS 解析 | 名稱解成什麼、哪台 DNS 回、多久 | 安靜 | ✓ | `getaddrinfo`＋指定 DNS；5s |
| 指定埠測試 | 埠開不開；**逾時（封包被丟）／拒絕（到了但沒服務）分得出** | 安靜 | ✓ | `connect_ex`，一次最多 5 個指定埠（D20），**不提供範圍**；每埠 3s |
| TLS 憑證檢查 | 誰發的、到期、鏈完整；**刻意要連自簽目標**：用預設驗證 context，把 `SSLCertVerificationError` 當結果，程式不得出現 `CERT_NONE` | 安靜 | ✓ | `ssl`；10s |
| HTTP(S) 回應 | 回幾號、轉址、耗時；只取標頭 | 安靜 | ✓ | `httpx`；不跟隨跨 host 轉址、最多 5 跳、大小上限；10s |
| SSH 握手 | TCP 連上＋拿到 SSH 版本 banner，**不登入**（OpenSCAP／Nmap／CINC 都靠 SSH，先分「網路不通」還是「帳密錯」） | 安靜 | ✓ | socket 讀 banner；10s |
| MTU 測試 | VPN 下「ping 通但大檔卡」 | 安靜 | ✓ | `ping -s 1472 -M do`；10s |
| 主系統連線診斷 | 固定序列：DNS → 閘道 ping → 主系統 ping → 443 → TLS（用既有 `describe_tls_failure`）→ 心跳端點 HTTP → 時鐘偏移 → 代理 | 安靜 | ✓ | 既有 `cloud_trust`、`clock_skew`；總 60s |
| traceroute | 在哪一跳斷 | **中** | ✗ 要勾，執行前確認 | `traceroute`，ICMP／UDP 可選；60s；需 `CAP_NET_RAW`，沒有時該項標「此環境不可用」 |
| 持續路徑監測（mtr 式） | 一段時間內每跳掉包 | **高** | ✗ 要勾並確認 | 在 traceroute 上疊迴圈與統計；上限 60s |

規則：一次只查一台、不並發；同一目標每分鐘最多 3 輪、每 session 最多 20 個目標；上次勾的組合記本機。

**噪音以總量計，不只看單項（D20）**：決策者問「全勾不就一堆，不算攻擊嗎」。答：單項各只做一次正常連線，但八項全勾對同一台二十秒內二十幾次連線，敏感的 IDS 可能記為探測。所以按「開始診斷」前先算這次**總共幾次連線**（勾幾項加總：ping 4、DNS 1、埠 N、TLS 1、HTTP 1、SSH 1、MTU 1、主系統序列 5、traceroute 30、持續監測 60）與約幾秒，顯示噪音條；**總連線數超過 10、或含中高項目**，執行前跳確認框列出每個原因。連線之間固定隔 1 秒、不連發。不採「只能勾一項」——判讀卡要多項結果合起來才講得出「斷在第幾跳」。門檻、每項噪音等級、埠上限、間隔都寫死在程式，不能被參數改。

### 5.3 結果怎麼呈現：結論在上、證據在下（雛形幕 4）

決策者問「會知道斷在哪嗎」。結果區三層：

**第一層：路徑圖**。每一跳一個節點、綠勾紅叉，紅叉那跳就是斷點；只跑 ping 沒跑 traceroute 時兩端之間標「未探測（勾 traceroute 可看每一跳）」。

**第二層：判讀卡**，固定三句——結論／依據／建議動作。判讀規則是確定性的，不做 AI 推測：

| 症狀組合 | 結論 |
|---|---|
| 閘道都 ping 不到 | 問題在本機網段或這台 Agent 的網卡 |
| 閘道通、目標 DNS 解不到 | DNS 問題，不是路不通 |
| 閘道通、路徑中途斷 | 斷在那一跳之後 |
| 路徑全通、埠「逾時」 | 封包到了目標附近被防火牆丟掉 |
| 路徑全通、埠「拒絕」 | 到了目標，該服務沒開 |
| 埠通、TLS 失敗 | 憑證或 TLS 設定問題 |
| 埠通、SSH 握手失敗 | SSH 服務設定，非網路 |
| 小封包通、MTU 測試失敗 | VPN／隧道 MTU 問題 |
| 主系統：DNS 解不到 | 「這台的 DNS 解不到主系統名稱，請確認內網 DNS」 |
| 主系統：443 逾時 | 「路通但 443 被擋，請放行 Agent → 主系統 443」 |
| 主系統：TLS 失敗 | 「憑證驗不過：自簽未裝 CA，或時鐘差太多」 |
| 主系統：心跳 401／403 | 「**連得到但身分被拒**：註冊 token 錯或已被停用」（既有心跳迴圈已分 401／403／404） |
| 主系統：時鐘偏移 > 5 分 | 「時鐘差 N 秒，JWT 與憑證驗證會失敗，請對時」 |

**第三層：原始輸出**，摺疊，等寬字，給網管複製。

每次執行產一份診斷報告：可匯出文字／PDF（`html.escape` 走既有 WeasyPrint 路徑）、可「回傳主系統」（走 outbox，落自主執行紀錄 `kind=diag`）。報告含內網拓撲，下載過 session scope；回傳主系統後下載過能力點。

### 5.4 安裝後連不上主系統（雛形幕 7）

決策者問「一開始安裝就連不通，怎麼知道本體在哪」。未同步狀態下 local-admin 登入 → 總覽紅橫幅「尚未同步」→ 按「診斷主系統連線」→ 跑 §5.2 的固定序列 → 路徑圖＋判讀。這條序列同時是手冊「安裝後連不上」一節的排查順序，UI 與手冊同一份邏輯。

::: {.callout .decided}
**D5 網路除錯工具（已裁 2026-10-26）**

上半區本機儀表板（資訊與連線狀態分開、無噪音、自動載入）；下半區十項診斷，安靜的預設勾，traceroute 與持續路徑監測要勾並確認；埠測試只給指定埠；全部進第一版，容器形態讀不到的標「此部署形態不可用」。噪音總量門檻見 D20。
:::

::: {.callout .decided}
**D6 網路除錯的目標範圍（已裁 2026-10-26）**

四類：授權清冊內設備（已同步才有）、主系統網址（註冊設定就有）、本機預設閘道、本機 DNS（讀本機就有）。手打 IP 必須落在授權網段。**未同步狀態只准後三項**（對應 D12）。清冊外一律拒，連 local-admin 也拒。traceroute 經過的中間節點是被動出現的，允許。
:::

---

## 6. 資安守門：開了門之後怎麼不被當跳板 {#security nav="資安"}

### 6.1 三道門：「誰能登入」與「能掃誰」是兩件事

決策者擔心的是「有登入口＝有跳板」。答案是掃描範圍由主系統決定、**與登入身分無關**，三道門各管各的：

```{.mermaid cap="圖 2 — 三道門：登入只過門 1；門 2、門 3 不看你是誰，local-admin 也一樣被擋"}
%%{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'}}}%%
flowchart LR
  A[任何人] --> G1
  subgraph G1[門 1 · 誰能登入]
    direction TB
    L1[local-admin 安裝時設]
    L2[離線操作者 主系統下發名單]
    L3[入場券 主系統簽]
  end
  G1 --> G2
  subgraph G2[門 2 · 能對誰動手]
    direction TB
    C1[授權目標清冊 主系統下發 Agent 只快取]
    C2[從未收到清冊 = 一律拒]
    C3[Nmap 執行主機 ZAP 網址解析 IP 也過]
  end
  G2 --> G3
  subgraph G3[門 3 · 能用什麼]
    direction TB
    T1[八支工具 只接表單定義參數]
    T2[除錯清單 不包埠範圍 nmap 封包擷取]
    T3[沒有命令列 subprocess 傳陣列不經 shell]
  end
  G3 --> R[對清冊內設備跑既有工具 僅此而已]
```

推論：攻擊者拿到 local-admin 密碼，能做的是對清冊內那幾十台已授權設備跑 OpenSCAP；不能掃 192.168.77.5、不能掃任意網段、不能下指令。未同步過的 Agent（D12）連門 2 都過不了，什麼都掃不了。守衛測試固定一條：**用 local-admin session 打清冊外目標必須 403**。

### 6.2 逐威脅配鎖

本案把 Agent 從「只聽主系統」變成「現場也能操作」，攻擊面變大是事實；下面是每一道新門配的鎖。這些是 A～E 每棒的驗收條件，不是另一棒。

| 威脅 | 鎖 | 落在哪 |
|---|---|---|
| 外人從網路連到 8444 猜密碼 | 登入失敗 5 次鎖 15 分鐘（D2）；本機密碼最短 12 碼；session 30 分鐘；可選 CIDR 白名單只准內網某段連（D9）；稽核記來源 IP | A |
| local-admin 密碼被現場多人共用、稽核分不出誰 | 已知限制：手冊要求平常走離線操作者、local-admin 只當救援；稽核記來源 IP；主系統對「離線操作者 0 位」紅字警示 | A、E |
| Agent 換租戶重註冊、舊租戶資料留在機上 | 重註冊時清名單、清冊、執行紀錄、待傳佇列；佇列有未回傳先問「有 N 筆未回傳，確定丟棄？」；退役提供 `guidant-agent-ctl wipe` | A、D |
| 離職或撤權的人離線期間還能登 | 授權有效期（D2）；離線超過 30 天停用；在線時心跳即時同步名單 | A |
| 被偷走或拔線搬走的 Agent 永遠獨立運作 | 離線 30 天停用本機登入與獨立掃描；救援碼只能看狀態 | A |
| 拿 Agent 掃清冊外的主機（跳板） | 獨立掃描與除錯都走 `allowed_targets.check()`，從未收到清冊＝一律拒（既有，D12）；手打目標限授權網段（D3／D6）；Nmap 執行主機與 ZAP 網址解析 IP 也過清冊 | B、C |
| 注入指令 | 工具只接表單定義的參數（`param_schema`），目標只接 IP／hostname／CIDR 格式驗證，一律 `subprocess` 傳陣列不經 shell；不提供任何自由命令框 | B、C |
| 拿除錯頁做掃描 | 不包埠範圍、nmap、arp-scan、封包擷取；traceroute 要人勾並確認；單目標序列執行；速率上限 | C |
| Nmap 手動模式被拿來掃任意網段 | 手動模式與派工同一支 connector：仍只能 SSH 到**已設定的執行主機**、目標仍過清冊 | B |
| 操作沒人知道 | 所有登入、執行、匯出寫本機稽核並回主系統（FR-157 D8 既有通道）；離線累積復線補傳；報告含執行者 | A～E |
| 診斷報告外洩內網拓撲 | 報告下載需 session＋範圍；回傳主系統走既有 mTLS 資料面；本機存放在資料目錄 0600 | C、D |
| 待傳佇列被塞爆磁碟 | 佇列只存指標與摘要（報告本體走 `upload_files`）；佇列上限 10,000 筆，超過最舊先丟並寫稽核 | D |
| 主系統端被重送轟炸 | 回報端點冪等（同 uid 同狀態回 200 不重寫）；每心跳最多帶 50 筆 | D |
| 本機密碼雜湊被拿走 | argon2id；存 SQLite 的資料目錄 0600；Agent 本身已有防竄改（FR-064） | A |

守衛測試（runner 每卡 commit 前跑）：① 除錯與獨立掃描的所有入口都呼叫 `allowed_targets.check()`（grep 斷言）；② 沒有任何 `shell=True`；③ 工具清單裡沒有 nmap／tcpdump／iperf 字樣；④ 回報端點對重複 uid 回 200；⑤ local-admin session 打清冊外目標 403；⑥ 未同步狀態下所有掃描與除錯入口（主系統連線診斷除外）403；⑦ Agent 端 grep 不到任何「新增使用者」路由。

::: {.callout .decided}
**D9 主控台 8444 對誰開（已裁 2026-10-26）**

現況綁 `0.0.0.0`，維持預設不限。**安裝時問一次**「允許哪些網段連到主控台（留空＝不限）」寫本機設定 `AGENT_CONSOLE_ALLOW_CIDR`，這是底線、未同步時就靠它；**主系統 Agent 管理頁可再設**，心跳下發後**取交集**（主系統只能收緊、不能放寬）。逃生門（§13.2 #10、#11）：白名單永遠隱含放行 loopback；主系統端擋「與安裝設定無交集」；Agent 收到空交集忽略主系統那份並寫稽核；CLI `console-allow-cidr reset`。
:::

::: {.callout .decided}
**D10 明確不包的東西（已裁 2026-10-26）**

寫進 design 並加守衛測試：任意命令列、SSH 跳轉到別台、埠範圍掃描、nmap 當除錯工具、ARP 掃描、**封包擷取（tcpdump：偷聽整條線能看到別人帳密、EDR 必告警、擷取檔是外洩點，業界管理台皆不內建）**、頻寬測試（iperf）、從 Agent 刪主系統證據、Agent 端新增使用者。需要封包層級分析時由客戶網管在網路設備端擷取，手冊寫一句。
:::

---

## 7. 離線續傳 {#resume nav="離線續傳"}

### 7.1 現況哪裡會掉

```{.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'}}}%%
sequenceDiagram
    participant M as 主系統
    participant AG as Agent
    participant Q as Agent 待傳佇列（SQLite）
    M->>AG: 心跳回應帶 pending_tasks
    AG->>M: ack（pending→dispatched）
    Note over M,AG: 此時斷線
    AG->>AG: 掃描照跑（執行緒不受影響）
    AG->>AG: 報告存本機 upload_files（本來就在這台）
    AG--xM: POST result 失敗
    AG->>Q: 入列：task_result{uid, status, summary, result_ref}
    AG->>Q: 入列：稽核事件、local_runs 摘要
    Note over M,AG: 主系統畫面：「Agent 離線中，掃描結果等待回傳」
    Note over M,AG: 復線
    AG->>M: 心跳成功
    AG->>Q: 依序取出
    AG->>M: POST result（冪等：已是終態回 200）
    AG->>M: POST local-runs（批次 50 筆）
    AG->>Q: 成功才刪；失敗留著下次再送
    M->>M: 任務→succeeded、證據照既有流程產生
```

實查結果：

- **執行不會停**：`dispatch_tasks` 每筆開 daemon thread，與心跳無關；報告 `_upload_report` 存 Agent 自己的儲存，不經主系統。這兩段**已經是對的**。
- **會掉的只有兩處**：① `_result()` 的 POST 失敗只記 log（註解「留待 FR-056.4 觀察」）；② 主控台稽核事件走心跳游標，離線期間累積在本機但沒有上限控管。
- **主系統端**：任務停在 `dispatched`，Agent 在線判定＝心跳間隔×3 沒心跳就算離線（`is_online`）。畫面目前只會看到「執行中」轉圈。

### 7.2 做法（D7）

- **Agent 待傳佇列**：SQLite 新表 `outbox`（`id`、`kind`：task_result／local_run／audit、`payload`、`idempotency_key`、`attempts`、`next_retry_at`、`created_at`）。`_result()` 失敗即入列；心跳成功後 `drain()` 依 `created_at` 序送，每次心跳最多 50 筆，失敗退避（1、2、4、8 分鐘，上限 1 小時）；送成功才刪，重啟中途可能重送，靠主系統終態冪等吸收。**上限 10,000 筆，丟棄順序：先稽核事件、再 local_runs 摘要、任務結果永不丟**（一台一天不會超過幾十筆）。
- **主系統端冪等**：實查 `agent_task_service.receive_result` **只對 `cancelled` 短路**，`succeeded` 再收一次會再跑 `write_success` 與 listener（證據再產一次）。改成「任何終態（succeeded／failed／cancelled）＝回 200 記 log 不重做」；新端點 `POST /agents/local-runs`（批次 50 筆）以 `(agent_id, local_uid)` 去重，`agent_id` 從通行證取、body 帶的無效。
- **主系統端顯示**：任務卡片與檢測執行頁，當 `agent_tasks.status in (dispatched, running)` 且該 Agent `is_online=false` → 顯示「Agent 離線中，結果等待回傳（最後心跳 10/24 14:31）」，不計入失敗、不觸發逾時失敗；Agent 清單加「待傳 N 筆」欄（心跳帶 `outbox_size`）。
- **逾時與收尾（D15）**：實查主系統**沒有任何派工逾時機制**（`dispatched_at` 只是記錄欄位，jedi-remote-agent 與 jedi-detection 都沒拿它判過期）。所以離線不會被誤判失敗，但永久離線（機器報廢）的任務會永遠「執行中」無人收尾。補：主系統既有背景排程加一條「`dispatched`／`running` 且該 Agent 離線超過 7 天 → `failed`，原因 `agent_unreachable`」，`system_context("agent_task_offline_reaper")` 具名、看到 0 筆不動；Agent 復線後回傳已 failed 的單走終態冪等（記錄不重做）。
- **離線判定加遲滯**（斷斷續續的線路）：Agent 端連續 3 次心跳失敗才切「離線」、連續 2 次成功才切「在線」，避免畫面跳動與佇列反覆送一半；佇列每筆獨立冪等，送一半重來不重複。
- **「連得到但被拒」與「連不到」分開**：主系統整台重裝、憑證對不上時心跳是 401／403 不是逾時；總覽與診斷頁顯示「連得到主系統但身分被拒（憑證或註冊問題）」，與「連不到」是兩種狀態、兩種處置。
- **離線期間任務被主系統取消**：Agent 不知道、跑完回傳；主系統收到已取消任務的結果記錄但不產證據、不炸。
- **Agent 重開機（斷電）**：記憶體 session 全失效、正在跑的掃描中斷——執行紀錄標「中斷」；待傳佇列在 SQLite 不丟；開機後自動重連補傳。
- **磁碟空間**：低於門檻（既有 `disk_space.py`）拒絕新掃描、總覽紅字；佇列本體只存指標與摘要。

::: {.callout .decided}
**D7 離線續傳機制（已裁 2026-10-26）**

Agent 端 outbox＋心跳後 drain（丟棄順序任務結果永不丟）；主系統端回報端點改**任何終態冪等**、`local-runs` 批次端點、已刪除或停用的 Agent 回傳拒收；「Agent 離線等待回傳」狀態顯示；離線判定遲滯。不做：Agent 之間代送、主系統主動連 Agent 拉結果、復線自動推報告副本。
:::

::: {.callout .decided}
**D15 永久離線任務的收尾（已裁 2026-10-26，§13.2 #3）**

主系統派工單 Agent 離線超過 7 天自動 `failed`（原因 `agent_unreachable`），既有背景排程具名 `system_context` 跑；7 天內照 D7 等。
:::

### 7.3 本機狀態放哪（D11 已裁：同一個 SQLite，不起 Postgres）

實查 Agent 本機現在存的東西：檔案索引 `upload_files`（SQLite 單檔，FR-066 D2 從 Postgres 換來）、工具帳密（Fernet 加密檔）、清冊與工具表單定義（JSON 檔）、主控台稽核（JSONL＋游標）、救援碼（一碼一檔）。本案要加：離線操作者名單、local-admin 與離線密碼雜湊、本機執行紀錄 `local_runs`、待傳佇列 `outbox`。全部是**單機、單進程、幾千筆以內、無並發寫入**的資料。

| 起 Postgres 換到什麼 | 代價 |
|---|---|
| 正規的表與交易 | SQLite 單一寫入者下交易語意相同 |
| 多進程並發 | Agent 沒這需求 |
| 「看起來比較正式」 | 多一個容器要起停、備份、密碼輪換、升級；原生安裝版要 bundle 整套 PG；客戶升級多一個會壞的點；FR-066 D2 拔掉 PG 的理由一條沒變 |

業界邊緣節點（Nessus scanner、Wazuh agent、FortiGate）都用嵌入式 DB，沒有一家為本機帳號起 Postgres。做法：四張新表進既有的 SQLite 檔；清冊與工具定義的 JSON 順手收進去；稽核 JSONL 與帳密加密檔維持（帳密有獨立金鑰檔的理由，FR-157 D11）。反悔條件：Agent 要跑多 worker 同時寫、或本機資料要給第三方報表工具直接查。

::: {.callout .decided}
**D11 本機狀態存放（已裁 2026-10-26）**

不起 Postgres；離線操作者、本機密碼雜湊、執行紀錄、待傳佇列四張表進既有 SQLite 單檔，清冊與工具定義 JSON 順手收進同一檔。
:::

---

## 8. 主系統端配合 {#mainsys nav="主系統端"}

### 8.1 多據點怎麼表達（D8 已裁：用 Agent 名稱，不加據點欄）

老闆的情境是「台北主系統，高雄、台南、日本、新加坡各一台」。決策者裁：Agent 名稱本來就是管理員取的，「高雄機房 Agent-01」一看就知道在哪，一台 Agent 通常就是一個點，不另加「據點」欄位。自主執行紀錄與 Agent 清單照 Agent 名稱篩；手冊寫命名建議「地點＋用途＋編號」。

::: {.callout .decided}
**D8 多據點（已裁 2026-10-26）**

不做據點欄位、不做區域實體；靠 Agent 名稱。
:::

### 8.2 離線操作者授權頁（雛形幕 6）

Agent 管理頁每台 Agent 多一個「離線操作者」設定：勾人（租戶內使用者）、每人選範圍（Agent 設定／Agent 檔案／獨立掃描／網路除錯四個能力點的子集，**不能超過這個人在主系統本來就有的能力點**）、授權到期日（預設 90 天）。存 `remote_agent_offline_operators`（`agent_id`、`user_id`、`scope`、`expires_at`、`local_password_set_at`、`revoked_at`）。心跳回應帶給 Agent；撤銷即從名單拿掉，Agent 下次心跳刪快取。

新增兩個能力點：`detection.remote-agent.run-local-scan`（獨立掃描）、`detection.remote-agent.net-diag`（網路除錯），照 FR-157 D10 形狀；入場券的 `scope` 也帶這兩個。

Agent 清單對「離線操作者 0 位」顯示紅字警示（提醒要嘛勾人、要嘛確認現場有 local-admin 密碼）；名單內的人若還沒設離線密碼，授權頁標「離線密碼：未設定」；有撤銷但該台離線還沒同步到的，標「有待同步的撤銷」。離職流程手冊寫明：把人從離線操作者移除，離線中的那台等它上線才生效。

::: {.callout .decided}
**D13 安裝與升級流程（已裁 2026-10-26；§13.2 #2 修正升級段）**

**新裝**：`install.sh` 強制問 local-admin 密碼（12 碼以上），不設不給裝完；安裝完成畫面大字要求交給現場負責人保管。**升級**：`upgrade` 可能無人值守，非互動終端（`[ -t 0 ]` 為假）**不強制**；啟動後主控台總覽紅字「尚未設定本機管理員密碼」、心跳回報 `local_admin_set=false` 讓主系統清單同樣紅字，在線跳轉進去的 IT 管理員（`configure` scope）可設；互動終端升級時照新裝問。新增 CLI：`reset-admin-password`、`wipe`（先清 SQLite 再刪檔案目錄）、`console-allow-cidr reset`；`re-enroll` 既有子命令擴充「清名單、清冊、執行紀錄、outbox，有未回傳先問」。
:::

### 8.5 重啟 Agent（D14）

兩種部署形態都有外部拉起機制（systemd `Restart=on-failure`、compose `restart: unless-stopped`），主控台重啟＝Agent 自己優雅結束（取消執行緒、關 DB、`exit 0`）由外部重拉，**不是** Agent 去呼叫 `systemctl` 或 docker API（要特權、又多一條能被濫用的路）。

| 入口 | 情境 | 連線是誰發的 |
|---|---|---|
| 主系統 Agent 管理頁 → 開啟主控台 → 維運 → 重啟 | 在線時台北管理員遠端處理 | 管理員瀏覽器 → Agent（入場券） |
| 直接打 Agent 網址 → 登入 → 維運 → 重啟 | 離線時現場人員 | 現場瀏覽器 → Agent |
| 主系統 Agent 清單「要求重啟」→ 心跳帶旗標 → Agent 下次心跳執行 | Agent 在線但 8444 被防火牆擋、只有 8443 心跳通 | 仍是 Agent 來拉，不違反發起方原則 |

流程：確認框列出「正在跑的掃描 N 個會中斷並標 `cancelled`；待回傳佇列不受影響；約 10 秒後恢復」→ 寫本機稽核、進 outbox → 回「已送出」→ 前端倒數 15 秒打 `/api/me`（401）→ 顯示「Agent 已重啟，請重新登入」。**旗標分兩種**（§13.2 #8）：`restart_when_idle`（預設，等沒任務）、`restart_now`；主系統按鈕預設前者、勾「立即」才後者。總覽加 `uptime_sec` 與上次重啟原因（手動／崩潰重拉／主系統要求）。

同區塊：服務狀態（uptime）、**下載支援診斷包**（Agent 側新做，不是 FR-071 的主系統包：log 尾段、去敏設定、本機儀表板快照、最近診斷結果）。權限：local-admin 與 `configure` scope。

::: {.callout .decided}
**D14 重啟 Agent（已裁 2026-10-26）**

主控台維運區「重啟」按鈕（跳轉與離線登入兩條入口共用）＋主系統「要求重啟」心跳旗標（`when_idle`／`now`）；Agent 優雅結束由 systemd／compose 重拉；加 uptime、支援診斷包。
:::

### 8.3 自主執行紀錄分頁

Agent 管理頁「自主執行紀錄」：一列一次執行——Agent、種類（掃描／除錯）、工具、目標、執行者、時間、狀態、報告下載（經 Agent 資料面取檔，Agent 離線時標「報告在離線的 Agent 上」）。篩 Agent／工具／人／日期。

### 8.4 心跳新欄位

Agent → 主系統：`outbox_size`、`local_login_enabled`（離線超過 30 天停用時回 false）、`local_admin_set`、`console_allow_cidr_installed`（安裝時設的，供主系統驗交集）、`uptime_sec`、`last_restart_reason`。主系統 → Agent：`offline_operators[]`（含撤銷→Agent 踢 session）、`console_allow_cidr`（只能收緊）、`restart`（`when_idle`／`now`／空）。全部照既有 observer 形狀掛進 `_heartbeat_loop`。

---

## 9. 決策彙整 {#decisions nav="決策"}

| 編號 | 題目 | 建議 | 替代 | 狀態 |
|---|---|---|---|---|
| D1 | 離線登入模式 | 三層：安裝時本機管理員 local-admin＋主系統授權離線操作者＋入場券跳轉；救援碼保留；LDAP 第二期 | 純本機多帳號 | **已裁** |
| D2 | 有效期與鎖定 | 授權預設 90 天可選；離線 30 天停用離線操作者**與獨立掃描**；5 次鎖 15 分；密碼 12 碼；local-admin 永不過期 | 不限期 | **已裁** |
| D3 | 獨立掃描目標範圍 | 仍限授權清冊，離線用快取不放寬；手打限授權網段；八支全開，ZAP 網址解析後過清冊 | 離線放寬／拔 ZAP | **已裁** |
| D4 | 獨立掃描結果落點 | 主系統新表 `agent_local_runs`＋管理頁分頁；掛證據第二期 | 塞 `detection_executions` | **已裁** |
| D5 | 除錯工具 | 本機儀表板九卡＋十項診斷全進第一版；安靜預設勾，traceroute／持續監測要勾並確認 | 分期 | **已裁** |
| D6 | 除錯目標範圍 | 清冊＋主系統＋閘道＋DNS；手打限授權網段；未同步只准後三項 | 自由輸入 | **已裁** |
| D7 | 離線續傳 | Agent outbox（任務結果永不丟）＋心跳 drain；主系統任何終態冪等＋「離線等待回傳」顯示＋遲滯 | 主系統主動拉 | **已裁** |
| D8 | 多據點 | 不加據點欄，靠 Agent 名稱；手冊寫命名建議 | 據點欄／區域實體 | **已裁** |
| D9 | 8444 對誰開 | 安裝時設底線 CIDR、主系統只能收緊取交集；隱含 loopback；交集非空驗證；CLI reset | 預設只內網 | **已裁** |
| D10 | 明確不包的東西 | 命令列、SSH 跳轉、埠範圍、nmap 探測、ARP 掃描、tcpdump、iperf、刪證據、Agent 端新增使用者——守衛測試 | — | **已裁** |
| D11 | 本機狀態存放 | 不起 Postgres；四張新表進既有 SQLite | 起 Postgres | **已裁** |
| D12 | 未同步狀態 | 沒收過清冊或名單＝只有 local-admin 可登、只准主系統連線診斷 | 放寬 | **已裁** |
| D13 | 安裝與升級 | 新裝強制設 local-admin 密碼；升級非互動不強制改紅字提醒；加 reset-admin-password／wipe／console-allow-cidr reset；re-enroll 擴充清本機 | 升級也強制 | **已裁** |
| D14 | 重啟 Agent | 主控台維運「重啟」（兩入口共用）＋主系統心跳旗標 when_idle／now；優雅結束由 systemd／compose 重拉；uptime、支援診斷包 | Agent 呼叫 systemctl | **已裁** |
| D15 | 永久離線任務收尾 | 主系統派工 Agent 離線 7 天自動 failed（agent_unreachable），具名 system_context 排程 | 永遠等 | **已裁** |
| D16 | Agent 本機 DB 版本機制 | 最小 `schema_version`＋有序 migration list（§13.4） | 每表 checkfirst／alembic | **已裁** |
| D17 | Agent LDAP 來源 | 三選一：同步（預設）／自設／不接；名單仍主系統決定 | 只同步 | **已裁** |
| D18 | LDAP 憑證與加密預設 | 首次信任釘住、拿掉勾選框、預設 STARTTLS；推翻 CM-2239 | 維持勾選框 | **已裁** |
| D19 | LDAP 實作怎麼共用 | 抽 `jedi-ldap` 零相依套件，jedi-iam 瘦身呼叫 | 複製一份進 Agent／Agent 裝 jedi-iam | **已裁** |
| D20 | 除錯噪音怎麼算 | 以**這次總連線數**計：執行前算給使用者看；超過 10 次或含中高項目跳確認框列原因；埠一次上限 5、連線間隔 1 秒；門檻寫死 | 只能勾一項執行 | **已裁** |
| D21 | 主控台檔案頁權限 | 維持 FR-157 D7 **租戶層**（有「Agent 檔案」＝看得到這台全部檔案）；離線操作者授權頁與手冊寫明這一句；專案層另開 FR | 本案提前做專案層 | **已裁** |
| D22 | 安裝題目搬進主控台 | 只搬**證據檔儲存位置**進「服務與重啟」頁，且主控台**只能選內建 SeaweedFS**（客戶不能把證據指到別處）；自有 SeaweedFS／MinIO／本機磁碟只能用 CLI `configure-storage`；重新註冊與升級留 CLI | 主控台四種型別全開／全搬／不搬 | **已裁** |
| D23 | 離線操作者的權限從哪來 | **跟主系統同一份**：離線名單只管「誰能離線登入、到期日」，不另勾範圍；每次心跳依該使用者當下的 Agent 能力點算出範圍下發（`restart` 照 D14 不下放）。主系統改角色，下一跳名單自動跟著變。主系統**沒有任何 Agent 能力點**的人不能加入名單（離線進去也什麼都做不了） | 名單逐人勾範圍（上限為本人能力點） | **已裁** |

---

## 10. 拆棒草案 {#breakdown nav="拆棒"}

排序以「決策者何時看得到畫面」為準：先讓登入頁與離線橫幅出現，再長功能。同一批彼此不改同一檔；登記型共用檔（能力點 migration、`ROUTE_TABLE`、心跳 payload 組裝、FE `console/` 路由）分批。

| 批 | 棒 | 做什麼 | repo | 看得到什麼 |
|---|---|---|---|---|
| 0 | J1 jedi-remote-agent 套件異動 | 兩張表（`remote_agent_offline_operators`、`agent_local_runs`，RLS）、心跳下發 `offline_operators`／`console_allow_cidr`／`restart`、心跳收 `outbox_size`／`local_admin_set`／`uptime`、`local-runs` 批次端點、`receive_result` 終態冪等、三個能力點、離線 7 天 reaper、事件碼。**異動前列清單等決策者點頭**；dev 走 path dependency | jedi-python-package | 套件測試過；主專案接線後心跳 payload 多欄位 |
| 0 | G1 Agent 本機 DB 版本機制 | D16：`core/local_db/`、`schema_version`、v1＝既有 upload_files、v2＝四張表 | evidence-agent | 既有客戶升級後 SQLite 多四張表、資料不丟 |
| 1 | A1 主系統離線操作者授權（BE） | `core/plugins/remote_agent.py` 接線、能力點 migration、`_ADMIN_CAPABILITIES`、CIDR 交集驗證 | BE | curl 能建授權、心跳回應帶名單 |
| 1 | A1-FE 主系統離線操作者授權頁 | **新開子元件** `OfflineOperatorsDialog.vue`，不往 `RemoteAgentManage.vue`（766 行）加；「0 位」紅字 | FE 主站 | 幕 6 授權表 |
| 1 | B1 Agent 本機執行紀錄與手動掃描 API | `local_runs` 表用、`_run_one` 的 ack／result 改可注入 sink、清冊 check、並發上限、取消；新檔 `core/console/local_runs.py` | evidence-agent | curl 起一支本機掃描並查進度 |
| 1 | C1 網路除錯執行器 | 新套件 `core/netdiag/`：九卡儀表板、十項診斷、參數驗證、逾時、速率、判讀規則、報告；容器形態偵測 | evidence-agent | curl 跑 ping＋traceroute 拿報告含判讀 |
| 2 | A2 Agent 登入與本機密碼 | 名單快取 observer、local-admin／離線密碼 argon2id（單調時鐘鎖定）、`POST /console/local-session`（四種失敗同一回應）、有效期、離線 30 天停用、未同步狀態、`revoke_by_sub`、在線提示設密碼、`heartbeat_status.last_error_kind`；新檔 `core/console/local_auth.py` | evidence-agent | curl 離線密碼登入拿 session |
| 2 | A2-FE 登入頁與三態 | `Login.vue` 取代 `Blocked.vue`、`phase='login'`、三態橫幅、設離線密碼對話框 | FE console | **直接打網址看到登入頁、離線登得進去** |
| 2 | B2 掃描工具分頁 | `views/Scan.vue`：工具卡、清冊目標挑選、`tool_schemas` 表單、進度、取消 | FE console | 幕 3 |
| 2 | C2 網路除錯分頁 | `views/NetDiag.vue`：儀表板九卡、目標、勾選與噪音、確認、路徑圖、判讀卡、匯出 | FE console | 幕 4、幕 7 |
| 2 | D1 Agent outbox | `outbox` 表、`_result` 失敗入列、心跳後 drain、退避、丟棄順序、離線判定遲滯 | evidence-agent | 斷線掃完、復線結果到主系統 |
| 2 | E2 Agent 維運區 | 重啟（優雅結束）、心跳旗標 observer、uptime、支援診斷包；新檔 `core/console/ops.py` | evidence-agent | curl 重啟後 Agent 回來 |
| 3 | D2 主系統離線顯示（BE） | 任務頁「Agent 離線等待回傳」資料、清單待傳欄、「要求重啟」端點 | BE | 幕 6 任務橫幅 API |
| 3 | D2-FE 主系統離線顯示 | 任務頁橫幅、清單欄、「要求重啟」按鈕（子元件） | FE 主站 | 幕 6 |
| 3 | E1 自主執行紀錄（BE） | `local-runs` 接線、報告下載走資料面 JWT、nickname enrich | BE | API |
| 3 | E1-FE 自主執行紀錄分頁 | **新 view** `AgentLocalRunsView.vue`，篩 Agent／工具／人／日期 | FE 主站 | 幕 6 紀錄表 |
| 3 | E3 執行紀錄分頁與維運分頁 | `views/Runs.vue`、`views/Ops.vue` | FE console | 幕 5、維運區 |
| 3 | A3 安裝、升級與 CLI | 新裝強制問密碼、升級非互動不強制、`reset-admin-password`／`wipe`／`console-allow-cidr reset`、`re-enroll` 擴充、`upgrade` 前問執行中任務、CIDR 安裝問句；**子命令進 `scripts/install/lib/`**（install.sh 2,800 行） | evidence-agent | 裝機不設密碼裝不完 |
| 4 | F1 文件與出貨 | SPEC、手冊（獨立作業一章、除錯噪音表、安裝後連不上排查、local-admin 保管）、安裝文件、資安例外登記兩條、守衛測試收口 | BE docs＋evidence-agent | 手冊 |

批 0 兩張是地基（套件與 DB 機制），批 1 起才能並行；批 1 內 A1／B1／C1 改不同檔；批 2 的 A2 與 D1 都動 `enroll.py` 的 observer 掛點（登記型）→ A2 先、D1 排批 2 尾或改批 3，開卡時定。

跨 repo 的棒（A1、D2、E1）各拆成「BE＋套件」與「FE 主站」兩張卡，一卡一 repo。

---

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

| 風險 | 說明 | 對策 |
|---|---|---|
| 離線期間撤銷不了 | 離線操作者模式的本質 | 有效期＋離線 30 天停用；主系統標「有待同步的撤銷」；手冊寫明 |
| local-admin 密碼沒人記得 | 裝機的人離職 | 安裝完成畫面要求交給現場負責人；機上 sudo `reset-admin-password`；主系統「離線操作者 0 位」紅字 |
| 清冊內設備換 IP | 新 IP 不在清冊掃不了 | 診斷頁 DNS 項看得出；手冊「到主系統更新清冊」 |
| 本機密碼變成第二套密碼 | 使用者忘了、跨台不同 | 在線跳轉時可重設；救援碼可重設；多台各設一次已是 FR-157 既定 |
| 清冊快取過期 | 離線一個月後清冊早變了 | 畫面標清冊版本與日期；離線 30 天停用 |
| 除錯工具在某些客戶環境本身就觸發告警 | 連 ping 都有客戶擋 | 噪音標示＋預設只勾安靜＋手冊「先跟網管講」一節 |
| traceroute 需要 raw socket | 容器內要 `CAP_NET_RAW`；原生安裝需 setcap | 安裝文件補；沒有權限時該項顯示「此環境不可用」而不是失敗 |
| 待傳佇列累積很大 | 離線一個月的稽核事件 | 佇列上限、報告本體不進佇列、每心跳 50 筆分批 |
| 主系統端回報端點改冪等會影響既有流程 | 既有「同 uid 第二次回報」行為可能被某處依賴 | 改之前 grep 所有呼叫端；守衛測試 |
| jedi-remote-agent 異動量大 | 兩張表、六個心跳欄位、三個端點、一條 reaper | 批 0 先做、套件測試齊、主專案接線後實打心跳 |
| 資安規則兩條例外（SR-08.2、SR-04.9） | reviewer 會標 | design 明列例外登記＋`docs/security-report/requirements/` 補註記 |
| 無人值守升級 | 不能強制互動 | D13 分新裝／升級 |
| 自簽憑證警告 | 直接打網址比跳轉更常撞到 | FR-156 D9 已列第二期「客戶自備憑證」；本案手冊再提一次 |
| FE `console/` 入口長大 | 現在三個 view，本案加四個 | 體積上限盯著；共用元件抽出 |

---

## 13. 全案重新檢視：死結、漏掉的情境、既有機制、規範對照 {#review nav="全案檢視"}

決策者要求在定案前把整案重走一遍：會不會有死結、有沒有情境沒想到、有沒有既有機制沒注意到、有沒有違反開發與資安規範。本節是逐項實查（開檔看程式、不憑印象）後的結果，**實查推翻本稿前面說法的地方標 🔁，前面各節已同步改正**。

### 13.1 既有機制盤點：哪些本來就有、本案直接接上

這張表的目的是「不重複造輪子」。每一列都是開檔確認過的。

| 需求 | 既有機制（檔案） | 本案怎麼用 | 🔁 本稿前面說錯的 |
|---|---|---|---|
| 主控台 session | `core/console/session.py`：記憶體 `SessionStore`、滑動 30 分鐘、`secrets.token_urlsafe(32)` | 離線登入、local-admin 登入發的 session **走同一個 store**，不另做 | — |
| 路由守門 | `core/console/routes.py:require_session(scope)`：無 session 401、scope 不在票上 403 | 新路由全掛它；新 scope `run-local-scan`、`net-diag` 加進 `agent_auth.CONSOLE_SCOPES` | — |
| 本機管理員身分 | `core/console/local_login.py` 已有 `LOCAL_ADMIN_SUB="local-admin"`、`LOCAL_ADMIN_DISPLAY_NAME`；`_create_local_session` 發全 scope session | **local-admin 不是新身分，是既有救援碼身分加一組密碼**；D1 的①沿用這個 sub，密碼登入與救援碼登入進同一個 `_create_local_session` | 前面把 local-admin 寫成「新增帳號」，實際是既有 sub 加密碼路徑 |
| CSP 與安全標頭 | `routes.py` 頁面帶 nonce CSP、API 帶 `nosniff` | 登入頁、新分頁照用，不另設 | — |
| 本機稽核與回傳 | `core/console/audit.py`：JSONL、游標嚴格遞增、`REPORTED_ACTIONS` 白名單、`_REPORT_KEYS` 欄位白名單、10MB 輪替；`core/console/heartbeat.py:_console_events()` 隨心跳帶、ack 游標 | 新動作（`local_login`、`local_scan_started`、`net_diag_run`、`restart_requested`、`report_exported`）加進 `REPORTED_ACTIONS`；主系統端對應事件碼加進 jedi-remote-agent 的事件表 | — |
| 授權目標清冊 | `core/allowed_targets.py`：心跳下發、持久化 `allowed_targets.json`、`check(params)` 從 `params` 的 `hosts`／`targets` 等欄位抽目標比對、從未收到＝一律拒（`degraded_reason=cloud_too_old`）；主系統端 `core/plugins/remote_agent.py:AllowedTargetsProvider` 實作 `IAllowedTargetsProvider` | 獨立掃描與除錯都呼叫同一支 `check()`；未同步狀態就是 `received=False` | 🔁 **Nmap 執行主機已經過清冊**：`task_executor._run_one` 在呼叫 connector 前對 `params` 整包 `check()`，Nmap 的 `params.hosts`（執行主機）本來就在被比對的欄位裡。本稿 §4.3 說「執行主機不過清冊」是錯的，已改 |
| ZAP 網址 | `allowed_targets.py:30` 檔頭明寫「ZAP 的網址不在授權範圍」，`_target_networks` 會 `urlsplit` 取 hostname 但 ZAP 的欄位不在 `_CONNECT_FIELDS` | 這是**既有的刻意缺口**（FR-129 當時裁網址型目標不管）。本案手動模式若開放 ZAP，等於開一條「對任意網址發掃描」的路 → D3 補一條：**手動模式第一版不開 ZAP**，等 FR-129 把網址型目標納清冊再開 | 前面說「ZAP 網址解析後 IP 也過清冊」是要改 `allowed_targets.py` 的既有決策，屬 FR-129 範圍，不在本案偷改 |
| 任務執行骨架 | `core/task_executor.py:_run_one`：ack → 清冊 check → 帳密解析 → connector.run → 上傳報告 → result；`_RUNNING_TASKS` 取消登記 | 手動執行造一張本機任務走同一條；`_ack`／`_result` 改成可注入的 sink（雲端 sink／本機 sink），不複製整支 | — |
| 心跳迴圈 | `core/enroll.py:_heartbeat_loop`：每跳依序 `skew_monitor`、`allowed_targets`、`error_tracking`、`vulndb_sync`、`tool_schemas`、`console_heartbeat.observe(data)`、`dispatch_tasks`；失敗只 log、磁碟檢查不依賴雲端 | 新增 `offline_operators.observe(data)`、`outbox.drain()`（成功後）、`restart_flag.observe(data)` 三個 observer，形狀照既有 | — |
| 心跳失敗判定 | 同上：401／404 自動重註冊、403 不重註冊、其他只 warning | **「連得到但被拒」已經分得出**（401／403／404 各走不同分支），本案只是把這個狀態露到主控台總覽（`heartbeat_status` 加 `last_error_kind`） | — |
| 時鐘偏移、磁碟 | `core/clock_skew.py`、`core/disk_space.py:DiskSpaceMonitor` | 本機儀表板直接讀；磁碟門檻沿用 `AGENT_DISK_MIN_FREE_PERCENT` | — |
| 雲端 TLS 診斷 | `core/cloud_trust.py:describe_tls_failure` 已會把 TLS 失敗翻成白話提示 | 主系統連線診斷的 TLS 那一步直接用它 | — |
| 本機帳密 | `core/credential_store.py`（Fernet、`secrets/secrets.key` 0600） | 手動掃描帳密從這裡拿，與派工同一份（`credential_resolver`） | — |
| 工具表單定義 | `core/tool_schemas.py` 心跳下發快取 `tool-schemas.json` | 掃描工具頁的參數表單從這裡長 | — |
| 檔案索引 | Agent SQLite `upload_files`（`core/app_factory.py` 以 `checkfirst=True` 自建單表） | 報告本體照走 `file_upload_service`，`local_runs.report_file_uid` 指向它 | 🔁 **Agent 沒有 migration 機制**（FR-066 D7 明寫「無 DB migration 負擔」）；本案加四張表要補一個最小的「建表＋版本欄」機制，見 13.4 |
| 維運 CLI | `install.sh` 子命令：`status`／`start`／`stop`／`restart`／`logs`／`fingerprint`／`console-login`／`configure-storage`／`re-enroll`／`upgrade`／`uninstall` | `reset-admin-password` 與 `wipe` 加進同一個 `case`；**`re-enroll` 已存在**，重註冊清本機狀態的鉤子掛在它裡面 | 前面寫「重註冊時清本機狀態」沒指出掛點，實際是擴充 `cmd_re_enroll` |
| 入場券 | jedi-remote-agent `agent_ticket_service`（`purpose=console_login`、`scope`、`jti` 一次性） | 兩個新 scope 走同一張票 | — |
| 主系統能力點 | FR-132 已有 `detection.remote-agent.{read,create,update,delete}`，FR-157 加 `configure`、`browse-files` | 加 `run-local-scan`、`net-diag`、`restart`，照 FR-157 A1 的 migration 形狀 | — |
| LDAP 登入 | jedi-iam `ldap_adapter.py`（295 行）：兩段式 bind、AD NTLM、跳脫、TLS 驗證；主系統登入頁有密碼／LDAP 切換；LDAP 設定頁 `LdapConfigForm.vue`；帳號要先由管理員「綁定」到 LDAP 身分 | 目錄連線段抽成 `jedi-ldap`（D19），Agent 與 jedi-iam 共用 | 🔁 不能直接用：adapter 同時繼承 `BaseRepository`（查 `user_auth_providers`／`users` 表），Agent 沒這兩張表；整支 jedi-iam 拖 Flask-RESTful 全套且釘 `jedi-common>=1.3.4` 與 Agent 的 `0.0.31` 衝突 |
| 主系統 Agent 管理頁 | FE `src/views/remote-agent/RemoteAgentManage.vue` 766 行 | 🔴 **已逼近 800 行上限**：離線操作者授權、自主執行紀錄一律新開子元件或新 view，不往裡加 | 前面拆棒沒提這件事 |
| 主控台 FE | `compliance-manager-fe/console/`：`App.vue` 用 `phase`（loading／blocked／ok）切頁、`api.js` 401 觸發 `onSessionLost`、`Blocked.vue` | 登入頁取代 `Blocked.vue` 成為 `phase='login'`；`api.js` 不動 | — |
| 支援診斷包 | FR-071 是**主系統端**的診斷包，Agent 沒有對應物 | 本案「下載支援診斷包」是 Agent 側新做，不能說沿用 FR-071；第一版內容限 Agent log 尾段、去敏設定、本機儀表板、最近診斷結果 | 前面說「FR-071 有腳本」誤導 |
| 主系統端派工逾時 | 🔁 **沒有**。`agent_tasks.dispatched_at` 只是記錄欄位，jedi-remote-agent 與 jedi-detection 都沒有任何以它判過期的程式；`source_scans` 另有 `expire_stale_uploads` 但那是等上傳的狀態 | 離線續傳不需要「暫停逾時」（本來就沒有逾時）；但反過來，**永遠回不來的任務現在無人收尾**，本案要補一條（13.2 死結 3） | 前面寫「離線期間暫停逾時」是對不存在的機制做假設 |
| 結果回報冪等 | `agent_task_service.receive_result`：歸屬比對 → 已 cancelled 短路回 200 → 其他狀態直接寫 | 🔁 **只對 cancelled 冪等**。`succeeded` 任務再收一次 `succeeded` 會再走一次 `write_success` 與 listener（證據再產一次）。本案要把短路條件擴成「任何終態＝回 200 不重做」 | 前面寫「冪等端點」當成已有，實際要改 |

### 13.2 死結推演

逐條想「這個情境會不會卡住、誰來解」。

| # | 情境 | 會不會死結 | 解法（已寫進對應 D 項） |
|---|---|---|---|
| 1 | 剛裝好就斷線，沒人設過離線密碼 | 不會：local-admin 安裝時強制設（D13） | 若裝機的人也忘了：機上 sudo `reset-admin-password`；再沒有＝沒有機器管理權，本來就不該進得去 |
| 2 | 既有客戶升級，升級腳本要求設 local-admin，但升級是無人值守的（`upgrade` 子命令被排程跑） | **會**：強制互動會讓無人值守升級卡住 | D13 修正：升級時若非互動終端（`[ -t 0 ]` 為假）**不強制**，改為啟動後主控台總覽紅字「尚未設定本機管理員密碼」、心跳回報 `local_admin_set=false` 讓主系統清單也紅字；在線跳轉進去的人可設。只有「新裝」才強制 |
| 3 | 主系統派工後 Agent 永久離線（機器報廢），任務永遠 `dispatched` | **會**：主系統沒有逾時機制，任務永遠「執行中」 | 新增 D15：主系統派工單加「Agent 離線超過 N 天（預設 7）自動標 `failed`，原因 `agent_unreachable`」，由既有背景排程跑；Agent 復線後回傳已 failed 的單走 13.1 的終態冪等（記錄但不重做）。這與 D7「離線不算失敗」不衝突：離線 7 天內等，超過才收尾 |
| 4 | 離線操作者名單從未同步（未同步狀態），local-admin 登入後要「設離線密碼」給別人 | 不會：D1 明定 Agent 端沒有新增使用者；local-admin 只能改自己的密碼 | — |
| 5 | 離線 30 天停用掃描後，主系統派的任務復線領到 | 不會：D2 明定主系統派的照跑（停用的是手動） | — |
| 6 | Agent 重啟中（維運按重啟）同時心跳正在 drain outbox | **可能重送**：drain 送出去、主系統收到、Agent 退出前沒來得及刪佇列 → 重啟後再送一次 | outbox 每筆帶 `idempotency_key`，主系統端終態冪等（13.1）；Agent 端 drain 採「送成功才刪」本就允許重送，靠冪等吸收 |
| 7 | 重啟按鈕按下、session 失效、使用者不知道有沒有重啟成功 | 不會死結但體驗差 | 前端倒數 15 秒後打 `/console/api/me`（會 401）→ 顯示「Agent 已重啟，請重新登入」；總覽加 `uptime_sec`，重登後看得到剛重啟 |
| 8 | 主系統經心跳下「請重啟」旗標，但 Agent 正在跑一個 4 小時的 OpenVAS | **會中斷任務** | 旗標分兩種：`restart_when_idle`（預設，等沒任務再重啟）、`restart_now`；主系統按鈕預設前者，勾「立即」才後者；中斷的任務走既有 cancel 流程標 `cancelled`，不是 failed |
| 9 | 離線期間，離線操作者在主系統被撤銷，同時他正在 Agent 上跑掃描 | 不會死結：session 到 30 分鐘過期；撤銷同步到 Agent 時 `SessionStore` 把該 sub 的 session 全部 revoke（要加 `revoke_by_sub`） | — |
| 10 | 8444 CIDR 白名單設錯把自己鎖在外面 | **會**：連 local-admin 都連不到 | 機上 CLI `console-login` 印的網址**不受 CIDR 限制**嗎？不對，CIDR 是連線層擋。解法：CIDR 白名單**永遠隱含放行 loopback**，手冊寫「鎖住時 SSH 到機器用 `curl` 或 port-forward 從 127.0.0.1 進」；另加 CLI `console-allow-cidr reset` |
| 11 | 主系統 CIDR 與安裝時 CIDR 取交集後為空 | **會**：誰都連不到 | 主系統端下發前驗證交集非空（Agent 心跳回報安裝時 CIDR，主系統 UI 擋「與安裝設定無交集」）；Agent 端收到空交集時忽略主系統那份並寫稽核 |
| 12 | outbox 滿 10,000 筆最舊先丟，丟掉的是任務結果 | **會丟資料** | 丟棄順序改為：先丟稽核事件、再丟 local_runs 摘要、**任務結果永不丟**（任務結果一台 Agent 一天不會超過幾十筆，不可能撐滿） |
| 13 | 離線操作者離線密碼忘了，Agent 又離線 | 不會死結：local-admin 可登；但 local-admin 不能幫他重設（只能改自己的） | 可接受：他等復線後從主系統跳轉進來重設。手冊寫明 |
| 14 | 兩個人同時用 local-admin 登入 | 不會：session 各自一個；稽核分不出誰（已知限制） | — |
| 15 | 心跳帶回 `offline_operators` 名單時 Agent 磁碟滿、SQLite 寫不進 | 不會卡心跳（observer 照既有「拿不到只略過」原則），但名單停在舊版 | 總覽磁碟紅字已涵蓋；手冊 |
| 16 | 未同步狀態，local-admin 跑「主系統連線診斷」要對主系統網址 DNS 解析，但 Agent 連 DNS 都沒設 | 不會死結：診斷結果就是「DNS 未設定」，這正是它要回答的 | — |
| 17 | 手動掃描跑到一半 Agent 升級（`current` symlink 切換、重啟） | 任務中斷 | `upgrade` 子命令已有停服務步驟；本案在 `cmd_upgrade` 前加「有執行中任務先問」（非互動時直接中斷並標 `cancelled`） |
| 18 | 主系統與 Agent 對「離線」的判定不一致（主系統 ×3 心跳、Agent 連續 3 次失敗） | 不會死結，但畫面可能一邊說在線一邊說離線 | 兩邊都是「3 個心跳間隔」，只差相位；可接受，手冊說明 |

### 13.3 前面沒想到的情境（本輪新增）

| 情境 | 影響 | 併入 |
|---|---|---|
| **無人值守升級**不能強制互動 | D13 要分新裝／升級 | D13 修正（13.2 #2） |
| **主系統從未有逾時**，永久離線的 Agent 讓任務永遠執行中 | 要補收尾機制 | 新 D15 |
| **結果回報只對 cancelled 冪等** | 重送會重做證據 | D7 修正：終態冪等 |
| **手動模式開 ZAP 等於繞過清冊** | 清冊設計刻意不管網址 | D3：把網址型目標納清冊（hostname 解析成 IP 比對），八支全開 |
| **Agent 沒有 migration 機制** | 四張新表怎麼進既有客戶的 SQLite | 新 D16（13.4） |
| **`RemoteAgentManage.vue` 766 行** | 再加就破 800 | 拆棒 E1／A1 明定新開子元件 |
| **重啟與長任務衝突** | 要分 when_idle／now | D14 修正 |
| **CIDR 把自己鎖外面** | 要有逃生門 | D9 修正：隱含 loopback＋CLI reset＋交集非空驗證 |
| **outbox 丟棄順序** | 不能丟任務結果 | D7 修正 |
| **撤銷要踢掉現有 session** | `SessionStore` 加 `revoke_by_sub` | A2 |
| **local-admin 密碼也要能從主系統跳轉時改** | 在線時 IT 管理員進主控台可改 local-admin 密碼（要 `configure` scope） | A2 |
| **診斷報告含內網拓撲、回傳主系統後誰能看** | 自主執行紀錄分頁的報告下載要過 `net-diag` 或 `read` 能力點；報告本體在 Agent，下載走資料面 JWT（既有 `data_plane_auth`） | E1 |
| **手動掃描的帳密在 Agent 本機**，`credential_store` 解不開（金鑰壞）時 | 既有 `_configured_tools` 已處理成「全部未設定」；手動掃描頁照此顯示「帳密儲存損壞，請重設」 | B2 |
| **Agent 時鐘偏移大**，local-admin 密碼的鎖定計時與授權到期判定都會歪 | 鎖定用單調時鐘（`time.monotonic`）；到期日判定用系統時鐘但總覽紅字提醒偏移 | A2 |
| **同一瀏覽器同時開主系統跳轉 session 與離線密碼 session** | 同一個 cookie 名，後者覆蓋前者 | 可接受；登入頁若已有有效 session 直接進總覽 |

### 13.4 Agent 本機 DB 的版本機制（新 D16）

現況 `app_factory.py` 只對 `upload_files` 一張表 `create(checkfirst=True)`，再加 `_heal_upload_files_columns` 補欄位——這是「沒有 migration 的 migration」。本案四張新表若也照這個做法，每張表各寫一支 heal，下一個 FR 再加就第三套。

| 選項 | 做法 | 優 | 劣 |
|---|---|---|---|
| **a. 最小版本表（建議）** | SQLite 內加 `schema_version(version INTEGER)` 一列；`core/local_db/migrations.py` 一個有序 list，每項 `(version, sql)`；啟動時從當前版本往後套、每步一個交易；首版把既有 `upload_files` 當 v1、本案四張表 v2 | 之後任何 FR 加表只加一項；與主系統 `schema_migrations` 概念對齊但不共用（主系統那套是 PostgreSQL） | 要寫一次 |
| b. 每表各自 `checkfirst` | 照現況 | 零新機制 | 加欄位要各寫 heal；沒有版本概念，回滾不可能 |
| c. 引入 alembic | 正規 | 重；Nuitka 打包多一個套件；單檔 SQLite 不值得 |

::: {.callout .decided}
**D16 Agent 本機 DB 版本機制（已裁 2026-10-26）**

a：最小 `schema_version`＋有序 migration list，30 行；本案四張表當 v2；既有 `_heal_upload_files_columns` 是 v1 的一部分不動。決策者問「不留技術債選哪種」——不是 alembic（Nuitka 多帶套件、單檔用不到），是把現況的「每表各自補丁」收斂成一張清單。
:::

### 13.5 開發規範對照

| 規範 | 本案怎麼守 | 要注意的點 |
|---|---|---|
| DDD 分層（主系統端） | 新表 `agent_local_runs`、`remote_agent_offline_operators` 走 jedi-remote-agent 套件（domain entity／repo interface／infra impl／app service），主專案只在 `core/plugins/remote_agent.py` 接線；route 不碰 session | 兩張表都有 `tenant_id` → RLS 四種指令（13.6） |
| 套件異動規範 | jedi-remote-agent 要加：兩張表、`offline_operators` 心跳下發、`local-runs` 批次端點、`restart` 旗標、終態冪等、離線逾時收尾。**異動前列清單等決策者點頭**；dev 走 poetry path dependency，發版等令 | 這是本案最大的套件異動，拆棒時 jedi 的卡要先行 |
| 檔案體積 | `RemoteAgentManage.vue` 766、`task_executor.py` 300 餘、`routes.py`（console）200 餘、`install.sh` 2,800 餘 | FE 新開子元件；Agent 新路由進 `core/console/` 新檔（`local_auth.py`、`local_runs.py`、`netdiag.py`、`ops.py`）；`install.sh` 已嚴重超標，新子命令進 `scripts/install/lib/`（若無則本案開第一支）並在 `case` 一行呼叫 |
| 下拉值進 `system_menus` | 本案沒有新下拉（據點已裁不做；噪音等級是程式列舉、屬例外） | — |
| 新 env var 先查既有 | `AGENT_CONSOLE_ALLOW_CIDR` 不存在，但命名照既有 `AGENT_CONSOLE_ASSETS_DIR` 族；`AGENT_DISK_MIN_FREE_PERCENT` 沿用 | `.env` sample 與 `deployment-env.md`（agent 版）同步補 |
| 註解只寫為什麼與陷阱 | 本案新檔案多，runner 要守 | reviewer 掃 |
| 測試政策 | 本案改到 `common/authz/`（能力點）、migration、jedi 套件 → 當卡就跑 reviewer；守衛測試 7 條（§6.2）屬「核心共用邏輯」要寫 | 突變測試紀律適用 |
| 審計欄位 | `agent_local_runs.started_by` 是主系統帳號 login_name → 回應要 enrich nickname（`enrich_audit_nickname_objs`） | E1 |
| 錯誤碼 | Agent 端 `common/code/agent_error_code.py` 族（`AGENT_CONSOLE_4xxNNN`）；主系統端 jedi-remote-agent 的 error code 檔；FE `error-code.json` 同步 | 登記型共用檔分批 |

### 13.6 資安規範對照（`.claude/rules/security/` 八支逐條）

| 規則 | SR | 本案落點 | 怎麼守 |
|---|---|---|---|
| 不需登入的入口回應不帶祕密、成敗一致 | SR-01.1 | Agent 登入頁 `POST /console/local-session` | 帳號不存在、密碼錯、被鎖、到期**四種都回同一個 401 同一個 body**；鎖定狀態不在回應透露，只進稽核 |
| 身分檢查前只做便宜的事 | SR-04.1 | 同上 | body 長度上限 1KB 在 `before_request` 擋；argon2 驗證前先查帳號在不在名單（在記憶體） |
| 機器身分只認簽章、回報只動自己名下的單 | SR-01.6、01.7 | `POST /agents/local-runs` 批次端點、重啟旗標 | 走既有 `AUTH_AGENT` 通行證；`local_runs.agent_id` 從證取，body 帶的無效；重複 `(agent_id, local_uid)` 去重 |
| 有能力點的功能 route 掛 `@require_capability` | SR-02.6 | 主系統三個新端點（授權頁 CRUD、自主執行紀錄、要求重啟） | `_ADMIN_CAPABILITIES` 登記；FE `v-can` |
| 帶資源編號先 resolve 再查關係 | SR-02.4 | 授權頁對 `agent_uid`、使用者 `user_id` | agent 屬本租戶、user 屬本租戶、scope 不超過該 user 既有能力點（**三個都核**） |
| 新表掛 RLS 四種指令 | SR-02.1 | `agent_local_runs`、`remote_agent_offline_operators` | `FOR ALL` 一條；`test_rls_all_tenant_tables.py` 自動管 |
| 整合憑證加密、讀取只回已設定 | SR-06.1 | 離線密碼、local-admin 密碼 | argon2id 雜湊（不是加密，更嚴）；任何 API 不回雜湊；主系統端只存「已設定／未設定」布林 |
| 祕密寫出去那一刻就遮 | SR-06.2 | 登入請求 log | `mark_password` 接 Agent 的 request log；密碼欄位名進遮罩清單 |
| 解密後帳密只留記憶體 | SR-06.4 | 手動掃描帶帳密給 connector | 走既有 `credential_resolver`，不進 `local_runs.params` | 
| 對外呼叫帶逾時 | SR-07.3 | 除錯十項工具、主系統連線診斷 | 每項固定逾時（ping 10s、traceroute 60s、埠 3s、HTTP 10s、TLS 10s、SSH 10s、持續監測上限 60s）；總逾時 120s |
| 代連目標來自已登記資料、不收自由文字 | FR-129 D7 | 除錯目標、手動掃描目標 | API 收 `device_uid` 或 `target` 字串，後者**必須**過 `allowed_targets.is_allowed()`；主系統網址／閘道／DNS 三個固定例外由 Agent 自己算、不收 body；守衛 `test_no_freeform_connect_target.py` 的 Agent 版 |
| 目標由呼叫者填的動作：限量、回應只回成敗、稽核記完整目標 | SR-08.2、09.1 | 🔴 **與本案衝突**：除錯頁要逐項回結果（通不通、哪一跳斷），不是只回成敗 | 判斷：SR-08.2 防的是「拿本系統當內網掃描器」，前提是目標自由填。本案目標限清冊＋三例外、單目標序列、速率上限，**回詳細結果是功能本身**。寫進 design「資安規則例外登記」並在 `docs/security-report/requirements/` 補一條 SR 註記；reviewer 看到要能查到理由 |
| 錯誤回應只回代碼 | SR-05.2 | 全部新端點 | ErrorCode 族 |
| 操作日誌是旁路 | SR-08.1 | `audit.record` 已是「寫不進只 log」 | — |
| 外部回來的一切是外部輸入 | SR-04.6 | traceroute／ping 的 stdout 解析、`local-runs` 批次 body | 長度上限、固定格式 regex 解析、不 eval；批次 body 50 筆上限、每筆欄位白名單 schema |
| 交給外部程式執行先看內容、subprocess 不經 shell | SR-04.8、04.10 | ping／traceroute 呼叫 | 參數陣列、目標先過 `ipaddress`／hostname regex、`shell=False`；守衛 grep |
| 回應 schema 白名單 | SR-05.2 | `local_runs` 回主系統、自主執行紀錄回 FE | 逐欄明列 |
| 使用者輸入在出口跳脫 | SR-05.1 | 診斷報告匯出 PDF／文字含目標名、執行者 | PDF 走既有 WeasyPrint 路徑 `html.escape`；文字匯出無格式 |
| 推到第三方最小化 | SR-05.4 | 不適用（本案不推外部） | — |
| socket | 不適用 | — | — |
| TLS 驗憑證、不得關驗證 | SR-03.2 | 除錯的 TLS 檢查**刻意要連自簽憑證的目標**（它就是要報告「憑證驗不過」） | 用 `ssl.create_default_context()` 連，捕捉 `SSLCertVerificationError` 當結果而不是關驗證；程式裡不得出現 `CERT_NONE`；守衛 grep |
| 可選加密預設加密 | SR-03.3 | 不適用 | — |
| 伺服器端抓外部網址走 `safe_http_fetch` | SR-04.9 | HTTP(S) 回應檢查 | 🔴 **衝突**：`safe_http_fetch` 拒內網，而除錯對象就是內網。判斷：它防的是 SSRF（騙伺服器連攻擊者指定位址），本案目標已過清冊。用 `httpx` 直連但套同樣的「不跟隨跨 host 轉址、最多 5 跳、大小上限、只取標頭」；例外登記同上 |
| 檔案存取先問歸屬 | SR-02.8 | 診斷報告與手動掃描報告下載 | Agent 端過 session scope；主系統端過能力點＋`agent_local_runs` 歸屬 |
| 背景工作入列前守門 | SR-02.9 | 離線逾時收尾排程（D15） | `system_context("agent_task_offline_reaper")` 具名；看到 0 筆不動 |
| 刪除先刪紀錄 | SR-08.3 | `wipe` CLI | 先清 SQLite 再刪檔案目錄 |
| 解析前先量大小 | SR-04.2 | `local-runs` 批次、診斷 stdout | 上限常數放 Agent `common/` |

**兩條例外登記**（SR-08.2 逐項回結果、SR-04.9 內網直連）是本案資安面最需要決策者知道的：規則寫給「主系統代人連任意目標」的情境，本案「Agent 對清冊內目標做診斷」是功能本體。例外不是繞過，是把「目標受限」這個前提寫清楚讓規則成立。

決策者問「變例外會不會造成漏洞」——**不會，前提是三個條件都守住**（已裁照此登記）：①目標只能是清冊內設備＋主系統＋閘道＋DNS（探得到的範圍＝主系統本來就授權 Agent 掃的範圍，沒有新增可達性）；②單目標序列、每分鐘 3 輪（掃 42 台要 14 分鐘以上，不是掃描器的形狀）；③結果寫稽核含完整目標。對比：主系統派 Nmap 回來的報告本來就逐台列埠與服務，比除錯頁詳細得多，而那是正常功能，因為目標過清冊；除錯頁的風險上限不會超過它。唯一新增的是三個固定例外，而那是 Agent 本來天天在連的對象。守衛測試「local-admin 打清冊外目標必須 403」就是確保前提永遠成立，**它被改掉例外就失效**，寫進登記。

### 13.8 既有 LDAP 功能的缺漏（決策者要求順便檢查）

登入本體做對三件事（資安連線分析 C11 已驗）：OpenLDAP 兩段式 bind（先用服務帳號查 DN，再拿 DN＋使用者密碼真的登一次目錄；AD 走 NTLM 一步到位）；帳號欄特殊字元跳脫；加密連線可驗憑證可貼 CA。重要設計特性：LDAP 使用者**不自動建帳號**，要先有本地帳號再由管理員綁定（`LOGIN_LDAP_ACCOUNT_NOT_BIND`）——對 Agent 是好消息，「誰能用 LDAP 登」仍是主系統的使用者表。

| # | 缺口 | 嚴重度 | 現況（實查） | 本案落點 |
|---|---|---|---|---|
| 1 | **LDAP 登入失敗不計鎖定** | 🟠 高 | 本地密碼錯 3 次鎖 15 分；LDAP adapter 第二段 bind 失敗直接拋 401，不碰 `login_failed_count`。拿登入頁對客戶目錄猜密碼沒上限，還可能把員工 AD 帳號反鎖 | P0-2 |
| 2 | **加密方式預設「無」** | 🟠 高 | 設定頁下拉預設空值＝明文 389；選「無」連警告都不記 | P0-1（套件預設 STARTTLS）、P0-7（FE） |
| 3 | **驗憑證預設不勾**（CM-2239） | 🟡 中 | 前端新建 `verify_cert: false`，新客戶接上去就是不驗 | D18 釘住，P0-1／P0-2／P0-7 |
| 4 | 服務帳號密碼明文落庫 | 🟡 中 | `THIRD_PARTY_LOGIN` 的 secret 讀取有遮、落庫明文 | P0-3 |
| 5 | 測試連線可帶任意主機 | 🟡 中 | `POST /ldap/connect-test` 帶畫面設定連任何主機，沒有「換主機要重填密碼」 | P0-2 |
| 6 | AD 停用不同步 | 🟡 中 | 綁定後不再問目錄「這個人還在嗎」；AD 停用但本地帳號還在且走密碼登入照樣進；走 LDAP 登入會被目錄擋（這點對） | 手冊寫離職流程；Agent 離線時更問不到，故「到期日＋離線 30 天停用」在 LDAP 模式仍保留 |

### 13.7 本輪修正彙整（對前面各節的影響）

| 項 | 修正 | 已改到哪一節 |
|---|---|---|
| D3 | 八支全開，ZAP 網址型目標納清冊 | §4.3、§9 |
| D7 | 冪等擴成「任何終態」；outbox 丟棄順序任務結果永不丟；刪「暫停逾時」 | §7.2、§9 |
| D9 | 隱含放行 loopback；交集非空驗證；CLI `console-allow-cidr reset` | §6.2、§9 |
| D13 | 升級非互動不強制、改紅字提醒；新裝才強制 | §8.2、§9 |
| D14 | 重啟分 `when_idle`／`now` | §8.5、§9 |
| D15 新 | 主系統派工離線 7 天自動收尾 | §7.2、§9 |
| D16 新 | Agent 本機 DB 版本機制 | 13.4、§9 |
| §4.3 | Nmap 執行主機已過清冊（刪錯誤敘述） | §4.3 |
| 拆棒 | FE 主站新開子元件；Agent 新路由新檔；install.sh 子命令進 lib；jedi 卡先行 | §10 |

---

## 14. 全量工作清單（最嚴謹版，給 PM 分階段與剔除用） {#worklist nav="全量工作"}

決策者要求：「先用最嚴謹的設計模式跟能力來看有多少工要做，再來跟 PM 討論有哪些可以分階段或剔除。切階段、切內容要分清楚，不能因為戰線太長造成東西沒辦法銜接。」本節是這張底表。每一工項都寫到「砍掉會斷什麼」，PM 砍的時候知道後果。

### 14.1 四個原則（切階段的判準）

1. **每個階段結束都是可出貨狀態**：階段 N 做完，客戶升級不會壞、沒做的功能只是「看不到」，不是「按了壞掉」。這是戰線拉長也不會斷的保證。
2. **地基先、畫面後，但地基只做該階段畫面需要的部分**：不為第三階段的畫面先鋪第一階段的表。
3. **登記型共用檔分批**（能力點 contract、事件碼、心跳 payload 組裝、FE 路由、Agent observer 掛點、SQLite 版本清單），同批兩張卡動同一個登記檔必衝突。
4. **套件先於宿主，宿主先於畫面**：jedi-* 改了要發版（或 dev 走 path dependency）宿主才接得上；宿主端點好了 FE 才有東西打。

### 14.2 五個階段

| 階段 | 做什麼 | 做完之後客戶多了什麼 | 停在這裡不往下做，會怎樣 |
|---|---|---|---|
| **P0** | 底層先改好：LDAP 連線的程式抽成獨立套件、Agent 本機資料庫加版本管理、主系統加兩張新表和心跳要帶的欄位、補主系統 LDAP 登入的三個資安漏洞 | 使用者看不到新東西。主系統的 LDAP 登入會變嚴：連錯會鎖、憑證換了會擋 | 不會壞。升級後跟現在一樣 |
| **P1** | 登入：Agent 有登入頁、三種身分、主系統可以勾誰能離線登入、安裝時設本機管理員密碼、限制哪些網段能連主控台 | 直接打 Agent 網址就能登入。管理員在主系統勾離線操作者 | 登進去只有原本的三頁（總覽、檢測工具、檔案）。等於多了一個登入方式，其他沒變 |
| **P2** | 離線能做事：在 Agent 上手動掃描、網路除錯、執行紀錄、重啟、儲存設定 | 主系統連不到也能掃、能查線路、能重啟 | 掃出來的結果只在那台 Agent 上，台北看不到 |
| **P3** | 把結果送回主系統：斷線時先存著、連回自動補送，主系統多一頁看各區的手動執行，任務頁顯示「等 Agent 回傳」，離線超過 7 天的任務自動收掉，主系統可以要求 Agent 重啟 | 連回主系統後結果自動補上。台北看得到高雄、台南、新加坡各自做了什麼。任務不會永遠卡在執行中 | 不會壞。結果留在 Agent 上，想看要到現場 |
| **P4** | Agent 接客戶的 AD、手冊、規格文件、守衛測試、資安例外登記 | 分公司可以用自己的 AD 帳號登 Agent | 不會壞。離線密碼和本機管理員照用 |

**哪些能砍**：P0 不能砍，砍了後面每張卡都要往已經太大的檔案裡硬塞。P1 是老闆要的最低限度。P2 的掃描、除錯、重啟三塊可以各砍各的。P3 可以整段延後，但「離線 7 天自動收掉任務」建議留，它修的是現在就有的問題。P4 的 AD 可以延後，文件不能砍。

### 14.3 全量工項表

欄位：**ID**（階段-序號）／**做什麼**／**repo**／**改哪些既有檔**（行數＝現況，超過 800 標 🔴）／**新開哪些檔**／**依賴**／**可砍？砍了斷什麼**。

#### P0 地基（7 項）

| ID | 做什麼 | repo | 改既有檔 | 新開檔 | 依賴 | 可砍？ |
|---|---|---|---|---|---|---|
| P0-1 | **抽 `jedi-ldap`**：從 `ldap_adapter.py`（295 行）抽目錄連線段成零相依套件（只靠 `ldap3`）：`LdapDirectory(config)`、`verify_user()`、`test_connect()`、`DirectoryUser` DTO、`LdapConfig` dataclass；加**首次信任釘住**（指紋存呼叫端給的 store，套件只算與比對）；加密預設 STARTTLS、退 389 要明示；失敗回傳結構化原因（連不到／憑證變了／密碼錯／帳號格式），不丟 Flask 例外 | jedi-python-package（新） | — | 整支新套件＋tests | 無 | ❌ 不可砍。砍了 Agent 接 LDAP 只能複製一份（兩套真相）或裝整支 jedi-iam（版本線衝突） |
| P0-2 | **jedi-iam 瘦身**：`LdapAdapter` 改呼叫 `jedi-ldap`，只留帳號綁定段（~30 行）；失敗計數補 LDAP 路徑（C11 D5）；`LoginConfigDTO.LDAP_VERIFY_CERT` 退役改釘住指紋欄 `LDAP_TRUSTED_FINGERPRINT`；`/ldap/connect-test` 換主機要重填密碼（SR-06.3） | jedi-iam | `ldap_adapter.py` 295、`login_config_assembler.py` 111、`login_service.py` 147、`user_auth_provider_service.py` 199、`login_error_code.py` | — | P0-1 | ❌ 不可砍（P0-1 沒接上就是兩份） |
| P0-3 | **主系統 LDAP 設定存釘住指紋**：`system-core` 的 `THIRD_PARTY_LOGIN` 設定組加 `trusted_fingerprint`、`trusted_at`；服務帳號密碼改 Fernet（C11 「明文落庫」） | jedi-system-core＋BE migration | system-core 設定 schema、`scripts/sql/` 一支 | — | P0-2 | ⚠️ 指紋欄不可砍（P0-2 要存）；Fernet 可延後但建議一起 |
| P0-4 | **Agent 本機 DB 版本機制（D16）**：`core/local_db/`：`schema_version` 表、有序 migration list、啟動套用；v1＝既有 `upload_files`（含 `_heal_upload_files_columns`）、v2＝`outbox`、`local_runs`、`offline_operators`、`local_credentials`（local-admin／離線密碼雜湊／LDAP 服務帳密指標） | evidence-agent | `core/app_factory.py` 208 | `core/local_db/__init__.py`、`migrations.py` | 無 | ❌ 不可砍（四張表沒地方放） |
| P0-5 | **jedi-remote-agent 表與 port**：`remote_agent_offline_operators`（agent_id、user_id、scope JSONB、expires_at、local_password_set_at、revoked_at；RLS）、`agent_local_runs`（agent_id、local_uid、kind、tool、targets JSONB、started_by、status、summary JSONB、report_ref、started_at、finished_at；RLS；唯一 `(agent_id, local_uid)`）；`remote_agents` 加 `console_allow_cidr`、`outbox_size`、`local_admin_set`、`uptime_sec`、`restart_requested`、`ldap_source`；三個能力點進 `plugin/contract.py`（`run-local-scan`、`net-diag`、`restart`）；domain entity／repo interface／infra impl／app service 照套件形狀 | jedi-remote-agent | `plugin/contract.py`、`infra/remote_agent/model/remote_agent.py` 87、`remote_agent_domain_service.py` 171、套件 migration | `domain/offline_operator/*`、`domain/local_run/*`、`infra/.../offline_operator*`、`infra/.../local_run*`、`app/service/offline_operator_service.py`、`app/service/local_run_service.py` | 無 | ❌ 不可砍（P1 授權頁、P3 紀錄頁都要） |
| P0-6 | **jedi-remote-agent 心跳契約擴充**：回應多 `offline_operators[]`、`console_allow_cidr`、`restart`、`ldap_config`（三選一同步時）；收 `outbox_size`、`local_admin_set`、`uptime_sec`、`last_restart_reason`、`console_allow_cidr_installed`、`sync_state`、`ldap_source`；照 `heartbeat_console.py` 的白名單收法；`receive_result` 改任何終態冪等；新端點 `POST /agents/local-runs`（批次 50、`(agent_id, local_uid)` 去重、`agent_id` 從通行證取）；`POST /remote-agents/{uid}/restart`（`AUTH_ADMIN`）；離線 7 天 reaper `run_daily_tick()`；主控台稽核事件碼加 `local_login`、`local_scan_started`、`net_diag_run`、`restart_requested`、`report_exported` | jedi-remote-agent | `agent_enrollment_service.py` 608、`agent_task_service.py` 170、`api/routing.py` 142、`heartbeat_console.py` | `app/service/heartbeat_offline.py`（新欄位收發）、`app/service/agent_offline_reaper.py`、`api/routes/local_run_route.py`、`api/routes/agent_restart_route.py` | P0-5 | ⚠️ 可拆：`ldap_config` 下發與 `restart` 可延到 P4／P2；終態冪等與 reaper **建議留**（修既有缺口）|
| P0-7 | **三個 LDAP 洞的 FE 面**：`LdapConfigForm.vue`（279 行）拿掉「驗證憑證」勾選、加「已信任憑證 xxx（到期）」顯示與「重新信任」按鈕、加密下拉預設 STARTTLS 並把「無」移末加「僅限測試」；錯誤碼 `error-code.json` 同步 | FE 主站 | `LdapConfigForm.vue` 279、`error-code.json`、`api.js` | — | P0-2 | ⚠️ 可延後，但主系統 LDAP 設定頁與套件行為會不一致（勾選框還在但沒作用） |

#### P1 進得去（8 項）

| ID | 做什麼 | repo | 改既有檔 | 新開檔 | 依賴 | 可砍？ |
|---|---|---|---|---|---|---|
| P1-1 | **主系統接線**：`core/plugins/remote_agent.py`（289 行）接 P0-5／P0-6 的 port；`_ADMIN_CAPABILITIES` 登記三端點；能力點 migration（照 FR-157 A1）；CIDR 交集非空驗證；「離線操作者 0 位」查詢給清單 | BE | `core/plugins/remote_agent.py` 289、`core/host_capabilities.py` 275、`scripts/sql/` 一支 | `core/plugins/_agent_offline_operators.py` | P0-5、P0-6 | ❌ |
| P1-2 | **離線操作者授權頁**：新子元件 `OfflineOperatorsDialog.vue`（勾人、範圍不超過其既有能力點、到期日、離線密碼狀態、撤銷、「有待同步的撤銷」；「Agent 檔案」欄標「勾了＝看得到這台全部檔案」，D21）；清單加「離線操作者」欄與 0 位紅字；**不往 `RemoteAgentManage.vue`（766 行）加** | FE 主站 | `RemoteAgentManage.vue` 766（只加一行掛子元件＋一欄）、`api.js`、i18n | `src/views/remote-agent/OfflineOperatorsDialog.vue` | P1-1 | ⚠️ 可砍→離線只剩 local-admin；老闆「各區自己用」的多人情境做不到 |
| P1-3 | **Agent 名單快取與離線身分**：`offline_operators.observe(data)` 寫 SQLite；撤銷→`SessionStore.revoke_by_sub`；local-admin 密碼（沿用 `LOCAL_ADMIN_SUB`）與離線密碼 argon2id 存 `local_credentials`；鎖定計數單調時鐘；有效期；離線 30 天停用（含掃描旗標）；未同步狀態判定（`allowed_targets.received` 且名單收過）；`heartbeat_status.last_error_kind`（401／403／404 分開） | evidence-agent | `core/enroll.py` 411（加一行 observer）、`core/console/session.py` 75、`core/heartbeat_status.py`、`core/console/heartbeat.py` 76（回報欄位） | `core/console/local_auth.py`（登入邏輯）、`core/offline_operators.py`（快取 observer）、`core/local_credentials.py` | P0-4、P0-6 | ❌ |
| P1-4 | **Agent 登入 API**：`POST /console/local-session`（body ≤1KB、四種失敗同一 401、通過發既有 session）；`POST /console/api/me/password`（改自己的離線密碼或 local-admin 密碼）；`GET /console/state`（在線／離線／未同步／停用，登入頁用，無需 session）；CSP 照既有；稽核 `local_login` | evidence-agent | `core/console/routes.py` 212（只掛 blueprint 一行） | `core/console/local_auth_routes.py` | P1-3 | ❌ |
| P1-5 | **CIDR 白名單**：安裝時 `AGENT_CONSOLE_ALLOW_CIDR` 底線；心跳收主系統 CIDR 取交集（空則忽略＋稽核）；隱含 loopback；`browser_plane_server.py` 連入檢查；CLI `console-allow-cidr reset` | evidence-agent | `core/browser_plane_server.py` 218、`config/config.py` 162、`scripts/install/install.sh` 🔴3426（子命令進 lib） | `core/console_allow_cidr.py`、`scripts/install/lib/console_allow_cidr.sh` | P0-6 | ⚠️ 可砍→8444 維持現況不限；手冊寫「靠客戶防火牆」 |
| P1-6 | **安裝與升級流程（D13）**：新裝強制問 local-admin 密碼；升級非互動不強制；完成畫面保管提示；`reset-admin-password`、`wipe`、`re-enroll` 擴充清本機；`upgrade` 前問執行中任務 | evidence-agent | `scripts/install/install.sh` 🔴3426、`deploy/install.sh` 🔴2894（**兩份 install.sh**，docker 與原生各一，都要改） | `scripts/install/lib/local_admin.sh`、`lib/wipe.sh` | P1-3 | ❌（沒密碼就沒有 local-admin） |
| P1-7 | **FE console 登入頁、殼改側欄、三態**：`Login.vue` 從主站 `Login.vue` 複製版面刪分流邏輯取代 `Blocked.vue`；`App.vue` 的 TabView 換成側欄＋頂欄＋狀態列（照主站 `AppSidebar`／`AppTopbar` 版面，選單寫死清單、teal 主題色、分頁標題帶 Agent 名）；`phase='login'`；`GET /console/state` 驅動狀態列與側欄反灰；設離線密碼對話框；「我的離線密碼」；local-admin 改密碼 | FE console | `App.vue` 149、`api.js` 74、`main.js` 51、`console.scss` 91、i18n `agent-console.json` | `console/Login.vue`、`console/Shell.vue`（側欄＋頂欄＋狀態列）、`console/SetOfflinePasswordDialog.vue`、`console/state.js`（小 store） | P1-4 | ❌ |
| P1-8 | **總覽改 dashboard**：警示區（連線、待回傳、磁碟、清冊過舊、帳密損壞、被拒絕）、網路一行、正在執行、最近五筆、靜態資訊；`overview.py` 加 alerts 組裝與最近執行查詢 | evidence-agent＋FE console | `core/console/overview.py` 33、`console/views/Overview.vue` 141 | `console/components/AlertList.vue` | P1-3、P2-1（最近執行） | ⚠️ 可砍→總覽維持 FR-157 現況，有事要自己翻各頁 |

#### P2 能做事（9 項）

| ID | 做什麼 | repo | 改既有檔 | 新開檔 | 依賴 | 可砍？ |
|---|---|---|---|---|---|---|
| P2-1 | **Agent 手動執行骨架**：`_run_one` 的 ack／result／report-sink 抽成可注入介面（雲端 sink／本機 sink），不複製；`local_runs` 表寫入（`source` 值：`cloud_task`／`agent_manual`，畫面字「主系統任務」「Agent 手動」）；並發上限（2 掃描 1 診斷）；取消沿用 `_RUNNING_TASKS`；離線 30 天與未同步擋；**ZAP 網址型目標納清冊**（`allowed_targets.py` 加網址欄位、hostname 解析成 IP 比對） | evidence-agent | `core/task_executor.py` 369（抽 sink，淨減）、`core/allowed_targets.py` 203 | `core/task_sinks.py`、`core/local_runs.py` | P0-4、P1-3 | ❌（P2-2、P2-5 都靠它） |
| P2-2 | **手動掃描 API**：`GET /console/api/scan/tools`（八支全部、帳密狀態、capabilities）、`GET /console/api/scan/targets`（清冊）、`GET /console/api/scan/schema/{tool}`（既有 `tool_schemas`）、`POST /console/api/scan/runs`（清冊 check、帳密 `credential_resolver`、入 `local_runs`）、`GET /console/api/scan/runs/{uid}`、`POST .../cancel`；scope `run-local-scan`；稽核。**第三方元件掃描**：源碼上傳走這台既有 `PUT /scan-input`（本機 session 代替主系統簽的票）、執行走既有 `sbom_scan` connector、結果回傳走 `source_scans` 既有收件路徑並標來源＝Agent 手動，**不進 `agent_local_runs`**（§4.3b） | evidence-agent | `core/console/routes.py` 212（掛 blueprint）、`core/scan_input.py`（接受本機 session） | `core/console/scan_routes.py` | P2-1 | ⚠️ 可砍→老闆的「離線手動掃描」沒了，是核心需求 |
| P2-3 | **掃描工具分頁**：`Scan.vue`：工具卡、清冊目標挑選（搜尋＋手打驗網段）、`tool_schemas` 表單（重用主站 `DetectionConfigField`）、進度、取消、結果連到紀錄 | FE console | `App.vue` 149（加 tab） | `console/views/Scan.vue`、`console/scanApi.js` | P2-2 | 同 P2-2 |
| P2-4 | **網路除錯執行器**：新套件 `core/netdiag/`：九卡儀表板讀取（`ip`／`ss`／`resolvectl`／`ethtool`／`nmcli`，容器形態偵測回「不可用」）、十項診斷（每項固定逾時、`subprocess` 陣列、目標先過 `allowed_targets.is_allowed()` 或三固定例外、速率限制、單目標序列、**連線間隔 1 秒、埠上限 5**）、`limits.py` 多一支 `estimate(selection)` 回總連線數／秒數／噪音等級（D20，門檻 10 寫死）、主系統固定序列（重用 `cloud_trust.describe_tls_failure`、`clock_skew`）、判讀規則引擎（確定性表）、報告產生（JSON＋文字＋PDF 走 WeasyPrint） | evidence-agent | 無 | `core/netdiag/__init__.py`、`dashboard.py`、`probes/{ping,dns,port,tls,http,ssh,mtu,traceroute,mtr,cloud}.py`、`verdict.py`、`report.py`、`limits.py` | P1-3 | ⚠️ 可拆：儀表板（P2-4a）、十項診斷（P2-4b）、判讀（P2-4c）、PDF（P2-4d）各自可砍；砍 P2-4c 結果只剩原始輸出 |
| P2-5 | **網路除錯 API**：`GET /console/api/netdiag/dashboard`、`GET .../targets`、`POST .../runs`（進 `local_runs` kind=diag）、`GET .../runs/{uid}`、`GET .../runs/{uid}/report.{txt,pdf}`；scope `net-diag`；未同步只放 cloud 序列；稽核 | evidence-agent | `core/console/routes.py` 212 | `core/console/netdiag_routes.py` | P2-1、P2-4 | 同 P2-4 |
| P2-6 | **網路除錯分頁**：`NetDiag.vue`：儀表板（資訊區＋連線狀態燈分開；重新整理、複製成文字）、目標、勾選與噪音標示、**噪音計算條**（打 `estimate` 即時顯示總連線數）、超門檻確認框列原因（D20）、路徑圖元件、判讀卡、摺疊原始輸出、匯出、回傳 | FE console | `App.vue` 149 | `console/views/NetDiag.vue`、`console/components/PathFigure.vue`、`console/components/VerdictCard.vue`、`console/netdiagApi.js` | P2-5 | 同 P2-4 |
| P2-7 | **執行紀錄分頁**：`Runs.vue`：主系統派的＋Agent 手動同一表、來源欄、拒絕也列、回傳三狀態（已回傳附時間／待回傳／回傳失敗附原因）、在線「立即重試」（打 `POST /console/api/runs/{uid}/resend`，走 outbox 立即 drain 一筆）、報告下載 | FE console＋evidence-agent | `App.vue` | `console/views/Runs.vue`、`core/console/runs_routes.py` | P2-1 | ⚠️ 可砍→結果只能從掃描頁與除錯頁各自看 |
| P2-8 | **維運區（D14 Agent 側）**：重啟（優雅結束：取消執行緒、關 DB、`exit 0`）、確認框列中斷任務、uptime 與上次重啟原因、支援診斷包（log 尾段、去敏設定、儀表板快照、最近診斷）；**證據檔儲存位置表單（D22）**：四種型別（內建 SeaweedFS／自有 SeaweedFS／MinIO 相容 S3／本機磁碟）、欄位與 CLI `configure-storage` 同一組、測試連線、儲存後 Agent 自己重載不重啟、「換位置不搬舊檔」警語；祕密金鑰走既有 `credential_store`；**表單儲存層與工具帳密共用**（抽 `core/console/secret_form.py`，P4-1 同一支）；scope `configure` | evidence-agent＋FE console | `main_app.py` 31（結束鉤子）、`App.vue`、`core/console/tools.py` 213（抽共用表單） | `core/console/ops.py`、`core/console/ops_routes.py`、`core/console/storage_routes.py`、`core/support_bundle.py`、`core/console/secret_form.py`、`console/views/Ops.vue`、`console/views/StorageForm.vue` | P1-3 | ⚠️ 可砍→重啟靠 SSH 下 `restart`；診斷包可單獨砍；儲存表單可砍→照舊走 CLI |
| P2-9 | **心跳旗標重啟（D14 主系統側 Agent 端）**：`restart.observe(data)`：`when_idle` 等無任務、`now` 立即；中斷任務標 `cancelled` | evidence-agent | `core/enroll.py` 411（一行） | `core/restart_flag.py` | P0-6、P2-8 | ⚠️ 可砍→「在線但 8444 被擋」的情境無法遠端重啟 |

#### P3 接回主系統（7 項）

| ID | 做什麼 | repo | 改既有檔 | 新開檔 | 依賴 | 可砍？ |
|---|---|---|---|---|---|---|
| P3-1 | **Agent outbox**：`outbox` 表；`_result()`、`local_runs` 摘要、稽核事件失敗入列；心跳成功後 `drain()`（50 筆、退避、送成功才刪）；丟棄順序；離線判定遲滯（3 敗／2 成）；磁碟門檻 | evidence-agent | `core/enroll.py` 411（一行）、`core/task_executor.py`（`_result` 改走 sink） | `core/outbox.py` | P0-4、P2-1 | ⚠️ 可砍→斷線期間的任務結果**永遠丟**（現況就是如此）；是老闆「續傳」需求本體 |
| P3-2 | **主系統收自主執行紀錄**：`local-runs` 端點接線、`agent_local_runs` 落庫、報告下載走資料面 JWT（既有 `data_plane_auth`）、`started_by` nickname enrich | BE | `core/plugins/remote_agent.py` | `core/plugins/_agent_local_runs.py` | P0-6、P3-1 | ⚠️ 可砍→台北看不到各區自主執行；老闆「報告回傳總部」做不到 |
| P3-3 | **自主執行紀錄頁**：新 view `AgentLocalRunsView.vue`（篩 Agent／種類／工具／人／日期、報告下載、離線標「報告在離線的 Agent 上」）；`ui_routes`＋`route_capabilities` 登記（FR-132 守衛） | FE 主站＋BE migration | `api.js`、i18n、`scripts/sql/` 一支 | `src/views/remote-agent/AgentLocalRunsView.vue` | P3-2 | 同 P3-2 |
| P3-4 | **主系統離線顯示**：任務頁「Agent 離線中，等待回傳（最後心跳）」橫幅資料與元件；清單「待傳」欄；不計入失敗 | BE＋FE 主站 | jedi-detection 任務狀態組裝（`detection_orchestration_service.py` 🔴2375——**只讀不加**，狀態欄位在 jedi-remote-agent 的 `_enrich_online` 補）、FE 任務頁元件 | FE `AgentOfflineBanner.vue` | P0-6 | ⚠️ 可砍→畫面轉圈不說明；體驗問題非功能問題 |
| P3-5 | **7 天 reaper 接線（D15）**：`core/scheduler.py`（714）加一條 `@_job_scoped("agent_task_offline_reaper")` 呼叫 P0-6 的 `run_daily_tick()`；`system_context` 具名；看到 0 筆不動 | BE | `core/scheduler.py` 714（加 20 行；已逼近上限，**抽 `core/scheduler_agent.py`**） | `core/scheduler_agent.py` | P0-6 | ⚠️ 建議留：它修的是既有「永遠執行中」缺口，與本案無關也該做 |
| P3-6 | **主系統要求重啟**：`POST /remote-agents/{uid}/restart` 接線＋`_ADMIN_CAPABILITIES`；FE 清單「要求重啟」按鈕（子元件 `RestartAgentDialog.vue`，when_idle／now） | BE＋FE 主站 | `core/plugins/remote_agent.py`、`RemoteAgentManage.vue` 766（一行） | `src/views/remote-agent/RestartAgentDialog.vue` | P0-6、P2-9 | ⚠️ 可砍，同 P2-9 |
| P3-7 | **守衛測試（Agent 側）**：七條（§6.2）：所有入口過 `allowed_targets`、無 `shell=True`、工具清單無 nmap／tcpdump／iperf、local-admin 打清冊外 403、未同步全 403、無新增使用者路由、終態冪等 | evidence-agent＋BE test | — | `test/test_fr166_guards.py`（Agent）、`test/security/code/test_agent_console_guards.py`（BE） | P2、P3 | ❌（CLAUDE.md 核心共用邏輯要寫） |

#### P4 LDAP 與收尾（6 項）

| ID | 做什麼 | repo | 改既有檔 | 新開檔 | 依賴 | 可砍？ |
|---|---|---|---|---|---|---|
| P4-1 | **Agent LDAP 三選一（同步／自設／不接）**：裝 `jedi-ldap`（加進 Nuitka `--include-package` 清單）；「身分來源」卡（形狀同工具帳密：Fernet 存服務帳密、測試連線、釘住指紋存 `local_credentials`）；同步模式收心跳 `ldap_config`；登入時多一分支（名單內＋AD 密碼對＝通過；名單不在＝拒）；心跳回報 `ldap_source` | evidence-agent | `core/console/local_auth.py`（P1-3 新檔）、`core/console/tools.py` 213（身分來源卡與工具帳密同形，**抽共用表單儲存成 `core/console/secret_form.py`** 再各自薄層）、`scripts/build/build_agent_release.sh` | `core/ldap_source.py` | P0-1、P1-3 | ⚠️ 可整項延後→離線靠離線密碼與 local-admin；老闆「分公司用自己 AD」做不到 |
| P4-2 | **主系統 LDAP 下發**：心跳 `ldap_config` 從 `THIRD_PARTY_LOGIN` 組（服務帳密 Fernet 密文原樣下發、Agent 用自己的 key 轉存——或下發明文走 mTLS 後 Agent 立即 Fernet，二擇一在 design 定）；Agent 清單顯示「身分來源：同步／自設 x.x.x.x／不接」 | BE＋FE 主站 | `core/plugins/remote_agent.py`、`RemoteAgentManage.vue`（一欄） | `core/plugins/_agent_ldap_config.py` | P0-3、P0-6、P4-1 | 同 P4-1 |
| P4-3 | **FE console 身分來源卡** | FE console | `console/views/Tools.vue` 467 | `console/views/IdentitySource.vue`（或併入 Tools 作第二區，看 Tools 體積） | P4-1 | 同 P4-1 |
| P4-4 | **SPEC**：`docs/spec-site/current/` Agent 主控台頁改版（登入三層、四分頁）、Agent 管理頁（離線操作者、自主執行紀錄、要求重啟）、LDAP 設定頁（釘住）；`writing-feature-specs` skill | BE docs | 既有 spec 頁 | — | 全部 | ❌ |
| P4-5 | **手冊與安裝文件**：「Agent 獨立作業」一章（登入三條路、離線能做什麼、除錯噪音表與總量門檻（D20）、安裝後連不上排查序列、local-admin 保管、離職流程、CIDR 逃生門、多據點命名建議、「Agent 檔案權限是整台」（D21）、儲存位置改在主控台設（D22））；安裝需求（8444、`CAP_NET_RAW`、容器形態不可用項）；資安例外登記兩條＋`docs/security-report/requirements/` 註記 | BE docs＋evidence-agent `deploy/README.md` | `deploy/README.md` | 手冊章節 | 全部 | ❌ |
| P4-6 | **e2e**：主系統登入 LDAP（鎖定、釘住）、Agent 管理頁三個新動作；Agent 主控台不在 e2e 範圍（另一台機器） | test repo | — | features | P0～P3 | ⚠️ 可延後到下一輪 e2e 補 |

### 14.4 數量總覽

| 階段 | 工項 | 卡（一卡一 repo 拆後） | 不可砍 | 新套件 | 動到的超標檔 |
|---|---|---|---|---|---|
| P0 | 7 | 9 | 5 | jedi-ldap | — |
| P1 | 8 | 11 | 6 | — | `install.sh` ×2（🔴 子命令進 lib） |
| P2 | 9 | 13 | 2 | — | — |
| P3 | 7 | 10 | 1 | — | `scheduler.py` 714（抽）、`detection_orchestration_service.py` 🔴2375（只讀不加） |
| P4 | 6 | 8 | 2 | — | — |
| **合計** | **37** | **約 51** | **16** | 1 | 4 |

**最小可出貨組合（PM 砍到底的下限）**：P0 全部（9 卡）＋P1 不可砍 6 項＋P2-1／P2-2／P2-3（手動掃描）＋P3-1／P3-2／P3-5＋P4-4／P4-5。約 28 卡。砍掉的是：CIDR、網路除錯整塊、維運與重啟、自主執行紀錄頁、離線顯示、LDAP、e2e。

**老闆原始五點對應**：① 直接登入＝P1；② 離線身分＝P0-1～3＋P1；③ 獨立掃描＝P2-1～3；④ 網路除錯＝P2-4～6；⑤ 續傳＝P3-1～2；資安＝P3-7＋P4-5 例外登記。P4 LDAP 是決策者追加（老闆可能會要）。

### 14.5 銜接保證：每階段結束的狀態

| 階段結束 | 升級既有客戶後 | 沒做的功能 |
|---|---|---|
| P0 | SQLite 多四張空表；主系統 LDAP 登入變嚴（鎖定、釘住：第一次登入自動信任現有伺服器憑證，使用者無感）；心跳多幾個欄位舊 Agent 忽略 | 全部看不到 |
| P1 | 直接打 8444 有登入頁；裝機要設 local-admin（升級不強制、紅字提醒） | 登入後只有 FR-157 三頁 |
| P2 | 多三個分頁；結果留 Agent | 主系統看不到自主執行 |
| P3 | 復線自動補傳；主系統多紀錄頁與離線橫幅 | — |
| P4 | 分公司可接自己的 AD | — |

每階段都是「加法」：沒有任何一階段會把前一階段做的東西改形狀。這是戰線拉長也不斷的根據。

---

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

| 你想知道 | 看哪份 |
|---|---|
| 做出來長什麼樣 | [雛形](prototype/index.html)（右上切在線／離線） |
| 為什麼要做、各選項與建議 | 本稿 |
| 主控台殼、入場券、救援碼怎麼做的 | [FR-157 design](../FR-157-2610-agent-local-settings-console/design.md) §5.1、[FR-156 design](../FR-156-2610-agent-side-sbom-scan-and-vulndb-sync/design.md) E1 |
| 派工骨架與 connector | [FR-056 design](../FR-056-2607-detection-tool-integration/design.md)、evidence-agent `core/task_executor.py` |
| 全量工作清單、哪些可砍、最小可出貨組合 | 本稿 §14 |
| 定案與開卡 | design.md（D16 裁完後落） |
