---
title: "Agent 獨立作業主控台 — 設計文件 (FR-166)"
brand: "Guidant AI · **FR-166** Agent 獨立作業主控台"
eyebrow: "FR-166 · 設計文件 v2 · 2026-10-28（D1～D23 定案；v1 的 41 張＋手測追加 29 張）"
h1: "主系統斷線，現場照樣能登入、能掃、能查線路，連回來自動補傳"
subtitle: "直接打 Agent 網址登入（三層身分）、八支掃描工具離線手動執行、網路除錯、斷線續傳、多據點報告回總部；誰能登與能掃誰是兩道門"
lede: "老闆要 Agent 在與主系統斷線時仍能獨立作業。本案給 Agent 主控台加一頁登入與四個分頁：登入分三層（安裝時設的本機管理員、主系統授權的離線操作者、主系統帳號跳轉），掃描範圍一律照主系統下發的清冊、不因離線放寬；網路除錯以總連線數計噪音、超門檻先確認；結果先存 Agent、連回主系統自動補傳；LDAP 連線抽成獨立套件讓主系統與 Agent 共用，憑證改首次信任釘住。離線操作者的權限跟主系統同一份，名單只管能不能離線登入。本文件把 23 項決策展開到「照做就能交」；v1 的 41 張卡 9 批之外，手測又追加 29 張（§10）。"
chips: [
  {text: "D1～D23 定案", kind: ok},
  {text: "v1 41 張 9 批＋手測追加 29 張", kind: accent},
  {text: "跨 5 repo：BE／FE／jedi-python-package／evidence-agent／test", kind: accent},
  {text: "推翻 CM-2239：LDAP 憑證改首次信任釘住", kind: accent},
  {text: "陷阱：誰能登≠能掃誰，掃描範圍永遠過清冊", kind: crit},
  {text: "陷阱：登記型檔先佔位，後面的卡不碰", kind: crit}
]
footer: "FR-166 · 設計文件 v2 · 2026-10-28 · 討論稿：[discussion.html](discussion.html) · 雛形：[prototype/index.html](prototype/index.html) · 前作：[FR-157](../FR-157-2610-agent-local-settings-console/) · 沿革見 LOG 與 git log"
---

> 狀態：**v2：全部實作合回 `feature/FR-139`，決策者手測中**｜日期：2026-10-28｜母卡 [CM-3109](https://app.notion.com/p/FR-166-Agent-LDAP-41-9-405346da4cd081beafa6e633e35831d6)、子卡 CM-3110～CM-3168（v1）、CM-3170～CM-3196（手測追加，§10）｜前作：FR-157（主控台殼、入場券、救援碼、工具帳密）、FR-156（8444 門、第三方元件掃描在 Agent）、FR-056（派工骨架）、FR-129（清冊）
> 討論稿（選項推演、§13 全案重新檢視、§14 全量工作清單）：[`discussion.html`](./discussion.html)｜雛形：[`prototype/index.html`](./prototype/index.html)（十二幕互動，上方「跟著走」四條流程）
> 開發 branch：五個 repo 2026-10-26 查皆在 `feature/FR-139`；runner 開工 `git branch --show-current` 再確認，不對就停下問，不切

> 🔴 **這份是設計決策，不是現況。** 決策（§3）與行為規則（§5）已更新到 v2；§4 現況盤點與 §6 拆卡表的行號與座標停在 v1 定稿當時。上線後的畫面與 API 現況看 SPEC 頁。

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

| 日期 | 變更 | 對應 |
|---|---|---|
| 2026-10-26 | 初版設計定案（D1～D22），41 張卡 9 批；同日開卡 | 母卡 CM-3109，子卡 CM-3110～CM-3150 |
| 2026-10-28 | v2：D22 只剩內建儲存、新增 D23；AD 登入改填 AD 帳號反查名單；離線登入天數改租戶設定；§5.1／§5.5／§5.6／§8 同步；新增 §10 手測追加卡 | CM-3170～CM-3196 |

---

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

老闆 2026-10-26：Agent 與主系統斷線時仍能獨立作業——直接打 Agent 網址登入、不經任務按八支掃描工具、網路除錯（ping、traceroute 這類，使用者勾選，有些公司會告警）、斷線不停工復線補傳、多據點（高雄、台南、日本、新加坡）各自作業報告回台北；不能變跳板或駭客工具。決策者追加：登入方式比較業界模式、LDAP（老闆會要）、重啟 Agent、總覽像儀表板、登入頁與殼照主系統改 Agent 模式。

現況（FR-157 後）：Agent 8444 有主控台三頁（總覽、檢測工具、檔案），只能從主系統簽入場券跳進去；斷線時沒有任何登入方式。八支工具全由主系統心跳領任務執行，Agent 自己沒有「選工具、填目標、按執行」的入口。結果回報失敗就丟。沒有網路除錯。

### 做完之後

```{.mermaid cap="圖 1 — 三層登入與兩道門：誰能登由身分決定，能掃誰由主系統下發的清冊決定"}
sequenceDiagram
    participant U as 現場使用者
    participant AG as Agent 8444
    participant M as 主系統
    M-->>AG: 心跳回應：離線操作者名單（誰、範圍、到期）＋授權清冊＋CIDR
    Note over AG: 名單與清冊存本機 SQLite；Agent 端沒有「新增使用者」
    U->>AG: 直接打網址，登入頁依連線狀態給分頁
    alt 在線
        U->>M: 「主系統登入」跳回主系統驗，帶入場券回來
    else 離線
        U->>AG: 帳號＋離線密碼（或 AD 密碼）；本機管理員 local-admin
        AG->>AG: 名單內？未到期？密碼對？沒被鎖？四種失敗同一回應
    end
    AG-->>U: 既有 30 分鐘 session（scope 照名單範圍）
    U->>AG: 掃描工具：選工具→選目標→執行
    AG->>AG: 目標過 allowed_targets.check()（與派工同一支）；未同步＝一律拒
    AG->>AG: 走 _run_one 同一條，result 走本機 sink 寫 local_runs
    AG-->>M: 心跳後 outbox.drain()：摘要與報告參照回主系統（任何終態冪等）
```

```{.mermaid cap="圖 2 — 離線續傳：結果先存 Agent，連回自動補傳；主系統派工 Agent 離線 7 天才收尾"}
flowchart LR
  R[任務／手動執行完成] --> S{主系統連得到？}
  S -->|是| M[(主系統 agent_tasks／agent_local_runs)]
  S -->|否| O[(Agent SQLite outbox)]
  O -->|心跳成功後 drain 50 筆／次，送成功才刪| M
  M -->|同 uid 任何終態重送| I[回 200 不重做]
  T[主系統派工 Agent 離線] -->|7 天內| W[顯示「等待回傳」不計失敗]
  T -->|超過 7 天| F[reaper 標 failed agent_unreachable]
```

---

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

整案：**Agent 主控台多一頁登入與四個分頁（掃描工具、網路除錯、執行紀錄、服務與重啟），主系統連得上照舊跳轉，連不上用事先授權的離線密碼或本機管理員；能掃誰一律照主系統清冊；結果先存 Agent、連回自動補傳；LDAP 抽套件共用。**

| 階段 | 做什麼 | 做完之後客戶多了什麼 | 停在這裡會怎樣 | 批 |
|---|---|---|---|---|
| **P0 底層** | LDAP 連線抽成 `jedi-ldap`、Agent 本機 DB 版本管理、主系統兩張新表與心跳欄位、補主系統 LDAP 三個資安洞 | 看不到新東西；主系統 LDAP 登入變嚴（連錯會鎖、憑證換了會擋） | 不會壞 | 1～3 |
| **P1 登入** | Agent 登入頁、三種身分、主系統勾離線操作者、安裝設本機管理員密碼、主控台連入白名單 | 直接打 Agent 網址能登入；管理員在主系統勾離線操作者 | 登進去只有既有的三頁（總覽、檢測工具、檔案） | 1～5 |
| **P2 離線能做事** | 手動掃描、網路除錯、執行紀錄、重啟、儲存設定 | 主系統連不到也能掃、能查線路、能重啟 | 結果只在 Agent 上 | 2～6 |
| **P3 接回主系統** | outbox 續傳、主系統收紀錄頁、任務頁「等待回傳」、7 天收尾、主系統要求重啟 | 連回自動補上；台北看各區做了什麼；任務不永遠卡執行中 | 不會壞，結果留 Agent | 5～8 |
| **P4 LDAP 與收尾** | Agent 接客戶 AD 三選一、手冊、SPEC、守衛測試、資安例外登記、e2e | 分公司用自己的 AD 登 Agent | 不會壞 | 7～9 |

**決策者檢查法**（每階段對照雛形幕）：P1 幕 1、10、12；P2 幕 2～6；P3 幕 5 回傳欄、幕 10 下半；P4 幕 7、11。PM 裁全做，無可砍項。

---

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

全文與選項推演在討論稿 §9；這裡列定案與一句理由。

| # | 決策 | 定案 | 理由 | 被排除 |
|---|---|---|---|---|
| **D1** | 離線登入模式 | 三層：安裝時強制設 `local-admin`（既有救援碼 sub 加密碼、永不過期、仍過清冊）＋主系統授權離線操作者（心跳下發名單、本人在線時設離線密碼、到期日；權限見 D23）＋入場券跳轉；救援碼保留；Agent 端沒有新增使用者；本機管理員不由主系統遠端停用或重設 | 名單只在主系統一處管；主系統密碼不離開主系統；業界（Nessus／FortiGate／UniFi）皆安裝時建本機 admin | 純本機多帳號 |
| **D2** | 有效期與鎖定 | 授權預設 90 天可選、上限一年；**離線登入天數由租戶在主系統設定（預設 7、1～30 天），心跳下發，Agent 本機只能再收緊（可關閉離線登入）**；超過天數停用離線密碼登入與獨立掃描，AD 登入不受天數限制；已登入的離線操作者每個請求重驗；5 次鎖 15 分（單調時鐘，整台重開才解）；密碼規則同主系統（12 碼＋大小寫＋數字）；local-admin 永不過期，登入時通知租戶管理員、密碼 180 天提醒 | 清冊可能早過期；搬走的 Agent 不能永遠獨立運作；天數是租戶的資安政策 | 不限期／Agent 端自訂天數 |
| **D3** | 獨立掃描目標範圍 | 仍限清冊，離線用快取不放寬；手打限授權網段；**八支全開**，ZAP 網址解析成 IP 後過同一張清冊（Agent `allowed_targets.py` 補網址欄位） | 決策者：所有工具都要能在 Agent 獨立執行；不拿 FR-129 當理由砍 | 離線放寬／拔 ZAP |
| **D4** | 結果落點 | 主系統新表 `agent_local_runs`＋Agent 管理頁「自主執行紀錄」分頁；報告本體留 Agent；第三方元件掃描走既有 `source_scans` 不進此表 | 與任務證據分開、多台彙整一頁 | 塞 `detection_executions` |
| **D5** | 除錯工具 | 本機儀表板（資訊與連線狀態分開）＋十項診斷全進第一版；結果＝路徑圖＋判讀卡（確定性規則） | 老闆要 ping／traceroute；判讀要多項合起來 | 分期 |
| **D6** | 除錯目標 | 清冊＋主系統＋閘道＋DNS 四類；手打限授權網段；未同步只准後三項 | 不能變跳板 | 自由輸入 |
| **D7** | 離線續傳 | Agent outbox（任務結果永不丟）＋心跳 drain；主系統任何終態冪等；「等待回傳」顯示；離線判定遲滯 3 敗／2 成 | 實查回報只對 cancelled 冪等，succeeded 重送會重產證據 | 主系統主動拉 |
| **D8** | 多據點 | 不加據點欄，靠 Agent 名稱；手冊寫命名建議 | 決策者：「不就是 Agent Name 就好」 | 據點欄 |
| **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`；Agent 優雅結束由 systemd／compose 重拉 | 決策者：從主系統發動＝跳轉進主控台按 | Agent 呼叫 systemctl |
| **D15** | 永久離線任務收尾 | 主系統派工 Agent 離線 7 天自動 failed（`agent_unreachable`），具名 system_context 排程 | 實查主系統沒有派工逾時 | 永遠等 |
| **D16** | Agent 本機 DB 版本 | 最小 `schema_version`＋有序 migration list；既有 `upload_files` 當 v1、本案四張表 v2 | 收斂「每表各自 heal」 | alembic |
| **D17** | Agent LDAP 來源 | 三選一：跟主系統同步（預設）／這台自己設／不接；名單仍主系統決定，LDAP 只驗密碼。**AD 分頁帳號欄填 AD 帳號（跟主系統登入一樣）**，Agent 用名單上主系統下發的 `ad_account` 反查是誰；未同步不准 AD 登入 | 各區可能不同 AD；使用者在主系統就是填 AD 帳號，不能要他記第二個 | 只同步／填主系統帳號 |
| **D18** | LDAP 憑證與加密 | 首次信任釘住、拿掉「驗證憑證」勾選框、預設 STARTTLS；**推翻 CM-2239**；主系統設定頁與 Agent 同步改 | 客戶沒憑證可貼；釘住零操作擋中間人 | 維持勾選框 |
| **D19** | LDAP 實作共用 | 抽 `jedi-ldap` 零相依套件（只靠 ldap3），jedi-iam 瘦身呼叫，Agent 裝它不裝 jedi-iam | 不綁表、不綁 Flask 才能復用；jedi-common 版本線衝突 | 複製一份 |
| **D20** | 除錯噪音怎麼算 | 以**這次總連線數**計，執行前算給使用者看；超過 10 次或含中高項目跳確認框列原因；埠一次上限 5、連線間隔 1 秒；門檻寫死 | 決策者：「全勾不就一堆」 | 只能勾一項 |
| **D21** | 主控台檔案頁權限 | 維持 FR-157 D7 租戶層；授權頁與手冊寫明「勾了＝看得到這台全部檔案」；專案層另開 FR | 離線操作者逐人勾可控；專案層是獨立工項 | 本案提前做 |
| **D22** | 安裝題目搬 UI | 只搬證據檔儲存位置進「服務與重啟」頁，且**主控台只能選內建 SeaweedFS**；自有 SeaweedFS／MinIO／本機磁碟只能用 CLI `configure-storage`；重新註冊與升級留 CLI | 客戶不能從畫面把證據指到別處；重新註冊影響大且罕見 | 主控台四型全開／全搬／不搬 |
| **D23** | 離線操作者的權限從哪來 | **跟主系統同一份**：名單只管「能不能離線登入、到期日」，不另勾範圍；每次心跳依該使用者當下的 Agent 能力點算出範圍下發（`restart` 不下放，D14）；能力點被拿光的人照列、標撤銷，Agent 踢掉他的 session；主系統沒有任何 Agent 能力點的人不能加入 | 同一個人在線跳轉與離線登入只有一份權限，主系統改角色下一跳就生效 | 名單逐人勾範圍（上限為本人能力點） |

::: {.callout .decided}
**畫面用字（決策者兩輪審雛形後定）**

來源欄一律「主系統任務」「Agent 手動」（不寫「本機」）；「身分來源」頁叫「LDAP 設定」；總覽只放當下（狀態燈、這台 Agent、正在執行），歷史在執行紀錄；狀態燈每盞只回答正常／注意／異常加一句原因，Agent 事實另開區塊不上色。回傳三狀態：已回傳（附時間）／待回傳／回傳失敗（附原因），在線可「立即重試」。登入失敗四種同一句（SR-01.1）。
:::

::: {.callout .decided}
**資安例外登記（兩條，reviewer 看到要查得到理由）**

- **SR-08.2 逐項回結果**：規則寫給「目標自由填」的情境。本案目標限清冊＋三固定例外、單目標序列、速率上限、噪音總量門檻；回詳細結果（通不通、斷在哪跳）是功能本體。
- **SR-04.9 內網直連**：`safe_http_fetch` 拒內網，而除錯對象就是內網。用 `httpx` 直連但套同樣限制（不跟隨跨 host 轉址、最多 5 跳、大小上限、只取標頭）。
- 兩條在 P4-5 寫進 `docs/security-report/requirements/` 註記。
:::

---

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

每列都開檔確認過（討論稿 §13.1 實查）。行數＝2026-10-26 現況。

| 元件 | 現況座標 | 現況 | 本案動作 | 卡 |
|---|---|---|---|---|
| 主控台 session | Agent `core/console/session.py` 75：記憶體 `SessionStore`、滑動 30 分、`token_urlsafe(32)` | 跳轉與救援碼發 session | 離線登入、local-admin 走**同一個 store**；加 `revoke_by_sub` | P1-3 |
| 路由守門 | `core/console/routes.py` 212 `require_session(scope)`：無 session 401、scope 不在票上 403 | 既有三頁 | 新路由全掛它；新 scope `run-local-scan`／`net-diag`／`restart` 加進 `agent_auth.CONSOLE_SCOPES`；**routes.py 由 P1-4 一次掛四支 blueprint** | P1-4 |
| 本機管理員身分 | `core/console/local_login.py` 188：`LOCAL_ADMIN_SUB="local-admin"`、`_create_local_session` 發全 scope | 救援碼登入用 | **不是新身分**：同 sub 加密碼路徑，進同一個 `_create_local_session` | P1-3 |
| 本機稽核與回傳 | `core/console/audit.py` 138：JSONL、游標、`REPORTED_ACTIONS` 白名單；`core/console/heartbeat.py` 76 隨心跳帶 | 四個動作 | `REPORTED_ACTIONS` 加 `local_login`／`local_scan_started`／`net_diag_run`／`restart_requested`／`report_exported`；主系統事件碼對應加 | P1-4、P0-6 |
| 授權目標清冊 | `core/allowed_targets.py` 203：心跳下發、`check(params)` 從 `hosts`／`targets` 抽目標、從未收到＝一律拒；網址刻意不在 `_CONNECT_FIELDS`（檔頭明寫） | 派工 `_run_one` 整包 check | 獨立掃描與除錯呼叫同一支；**加網址欄位**：`urlsplit` 取 hostname → `getaddrinfo` → 過同一張清冊 | P2-1 |
| 任務執行骨架 | `core/task_executor.py` 369 `_run_one`：ack → check → 帳密 → connector.run → 上傳 → result；`_RUNNING_TASKS` 取消 | 雲端一條路徑 | **抽 sink**：ack／result／report-sink 成可注入介面，雲端 sink＝現況、本機 sink 寫 `local_runs`；不複製 `_run_one` | P2-1 |
| 心跳迴圈 | `core/enroll.py` 411 `_heartbeat_loop`：依序 observers（skew、allowed_targets、error_tracking、vulndb、tool_schemas、console）、`dispatch_tasks`；401／403／404 已分路 | 六個 observer | **P1-3 一次掛三個新 observer**（`offline_operators`／`restart_flag`／`outbox.drain`，後兩支先 no-op）；`heartbeat_status` 露 `last_error_kind` | P1-3、P2-9、P3-1 |
| 本機帳密 | `core/credential_store.py` 172（Fernet、`secrets.key` 0600） | 工具帳密 | 手動掃描 `credential_resolver` 同一份；LDAP 服務帳密、儲存設定祕鑰也存這裡 | P2-2、P2-8a、P4-1 |
| 工具表單定義 | `core/tool_schemas.py` 95 心跳下發快取 | 主控台工具頁 | 掃描工具參數表單從這裡長 | P2-2、P2-3 |
| 工具頁表單儲存 | `core/console/tools.py` 213 | 工具帳密專用 | **抽 `secret_form.py`**：工具帳密、儲存設定、LDAP 自設三者共用；`tools.py` 淨減 | P2-8a |
| 檔案索引與建表 | Agent SQLite `upload_files`；`core/app_factory.py` 208 `create(checkfirst)`＋`_heal_upload_files_columns` | 沒有 migration 機制 | **D16**：`core/local_db/` 最小版本表；既有建表＋heal 收成 v1，四張表 v2 | P0-4 |
| 瀏覽器門 | `core/browser_plane_server.py` 218 綁 `0.0.0.0` | 不限來源 | 連入檢查：安裝 CIDR 底線∩主系統下發，隱含 loopback | P1-5 |
| 維運 CLI | `scripts/install/install.sh` 3426、`deploy/install.sh` 2894（**兩份都超標**）；`case` 子命令；`do_re_enroll`；`confirm_cloud_certificate` 已有「核對指紋 y/N」 | 無 lib 目錄 | **開 `scripts/install/lib/`**（本案第一支）；新子命令進 lib、`case` 一行呼叫；新裝加兩題；升級分互動；`re-enroll` 擴充清本機 | P1-5、P1-6 |
| 主控台 FE | `compliance-manager-fe/console/`：`App.vue` 149 `phase`、`api.js` 74 401→`onSessionLost`、`Blocked.vue`、三 view | TabView | 殼換 `Shell.vue`（側欄＋頂欄＋狀態列）；**選單清單常數一次寫全九項**，各分頁卡只新開 view；`Login.vue` 取代 `Blocked.vue` 為 `phase='login'`；`Tools.vue` 467 表單段抽 `SecretForm.vue` | P1-7a、P1-7b、P2-8b |
| 入場券 | jedi-remote-agent `agent_ticket_service`（`purpose=console_login`、`scope`、`jti`） | 兩 scope | 三新 scope 走同一張票 | P1-1 |
| 心跳契約（主系統端） | jedi-remote-agent `agent_enrollment_service.py` 608 `_heartbeat`；`heartbeat_console.py` 白名單收法 | 收 `configured_tools`／`storage`／`console_events` | 收 `outbox_size`／`local_admin_set`／`uptime_sec`／`last_restart_reason`／`console_allow_cidr_installed`／`sync_state`／`ldap_source`；回 `offline_operators[]`／`console_allow_cidr`／`restart`／`ldap_config` | P0-6 |
| 結果回報冪等 | `agent_task_service.py` 170 `receive_result`：只 cancelled 短路 | succeeded 重送重做 | 短路條件擴成**任何終態** | P0-6 |
| 派工逾時 | **沒有**：`dispatched_at` 只是記錄欄位 | 永久離線任務永遠執行中 | `run_daily_tick()` reaper（套件）＋BE 排程接線（抽 `scheduler_agent.py`，`scheduler.py` 714 只加一行） | P0-6、P3-5 |
| 套件 migration | jedi-remote-agent `migrations/010-remote-agent-console.sql` 為最新 | 010 | 011：兩張新表、`remote_agents` 六新欄、RLS | P0-5 |
| 能力點 | FR-157 `scripts/sql/2026-10-22-fr157-agent-console-capabilities.sql`（configure／browse-files） | 六顆 | 加 `run-local-scan`／`net-diag`／`restart`，照同形狀一支主線 migration | P1-1 |
| 主系統接線 | BE `core/plugins/remote_agent.py` 289、`_agent_vulndb.py` 先例 | adapters＋`_ADMIN_CAPABILITIES` | 新邏輯各開 `_agent_offline_operators.py`／`_agent_local_runs.py`／`_agent_ldap_config.py`；`remote_agent.py` 只接線 | P1-1、P3-2、P4-2 |
| Agent 管理頁 | FE `src/views/remote-agent/RemoteAgentManage.vue` 766 | 逼近上限 | **只加欄與一行掛子元件**：`OfflineOperatorsDialog.vue`、`RestartAgentDialog.vue`、`AgentOfflineBanner.vue`、新 view `AgentLocalRunsView.vue` | P1-2、P3-3b、P3-4b |
| LDAP 登入 | jedi-iam `ldap_adapter.py` 295（目錄段 ~180 行＋綁定段 ~30 行，繼承 `BaseRepository`）；`LdapConfigForm.vue` 279 | 三個洞：失敗不計鎖定、加密預設無、憑證預設不驗 | 目錄段抽 `jedi-ldap`；adapter 瘦成 30 行；`LDAP_VERIFY_CERT` 退役改 `LDAP_TRUSTED_FINGERPRINT`；FE 拿掉勾選框 | P0-1、P0-2、P0-3、P0-7 |
| 離線任務顯示 | jedi-detection `detection_orchestration_service.py` 2375（**只讀不加**） | — | 狀態欄位在 jedi-remote-agent `_enrich_online` 補 | P3-4a |
| 第三方元件掃描 | FR-156 後執行全在 Agent；上傳 `PUT /scan-input` 驗主系統簽的票；結果 `source_scans` | 只有主系統入口 | `scan_input.py` 多接受本機 session；結果照走 `source_scans` 標來源 Agent 手動，**不進 `agent_local_runs`** | P2-2 |

---

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

### 5.1 登入與身分（P1）

**Agent 端資料（SQLite v2，P0-4）**：

| 表 | 欄 | 說明 |
|---|---|---|
| `local_credentials` | `sub` PK、`kind`（local_admin／offline／ldap_service）、`hash`（argon2id）、`updated_at`、`failed_count`、`locked_until_mono` | 鎖定計時用 `time.monotonic` 的偏移存法（重啟即清，可接受） |
| `offline_operators` | `sub`、`display_name`、`ad_account`、`scope` JSON、`expires_at`、`revoked`、`received_at` | 心跳下發整份覆蓋；撤銷→`SessionStore.revoke_by_sub`；`scope` 是主系統當下算好的（D23），Agent 不判斷 |
| `local_runs` | `uid`、`kind`（scan／diag）、`tool`、`targets` JSON、`params` JSON（不含帳密）、`started_by`、`status`、`summary` JSON、`report_file_uid`、`created_at`、`finished_at`、`synced_at`、`cloud_uid`、`sync_error` | 報告本體走既有 `upload_files` |
| `outbox` | `id`、`kind`（task_result／local_run／audit）、`payload`、`idempotency_key`、`attempts`、`next_retry_at`、`created_at` | 上限 10,000；丟棄順序 audit → local_run；task_result 永不丟 |

**登入 API（P1-4）**：

```
GET  /console/state                 無需 session；回 {net: online|offline|unsynced|disabled, offline_days, agent:{name,ip,version}, ldap_source}
POST /console/local-session         body ≤1KB {sub, password, method: password|ldap}；所有失敗同 401 同 body {error_code:"CONSOLE_LOGIN_FAILED"}；成功發既有 session
POST /console/api/me/password       {old, new}；只能改自己（local-admin 或本人離線密碼）；12 碼
```

判定順序（`local_auth.verify`）：①`sub == LOCAL_ADMIN_SUB` → 查 `local_credentials`，不受到期與離線天數影響。②找人：`method=ldap` 且目錄是 AD → `sub` 是 AD 帳號，用 `offline_operators.get_by_ad_account` 反查名單（網域與帳號各自不分大小寫比對，命中不是恰好一人就當找不到，不猜）；其餘情況 `sub` 就是主系統帳號。找不到、已撤銷、到期 → 失敗。③`method=password` 另受離線天數限制、本機關閉離線登入時一律拒；`method=ldap` 不受天數限制，只問 AD、不退回離線密碼，沒綁 AD 的人 AD 分頁一律失敗。④鎖定：`failed_count >= 5` 且 `locked_until` 未到 → 失敗。所有失敗同一回應、反查不到也照樣跑一次雜湊（回應時間不洩漏有沒有人綁）；原因只進 log 與稽核（`not_listed`／`method_blocked`／`credential_rejected`）。之後 session、稽核、鎖定計數一律記主系統帳號；**已登入的離線操作者每個請求都重驗**名單與天數。

**未同步判定**：`allowed_targets.received and offline_operators.received_at is not None`。未同步時 `state.net = unsynced`，掃描與除錯（主系統連線診斷除外）入口 403。

**主系統端（P0-5／P0-6／P1-1／P1-2）**：`remote_agent_offline_operators`（`agent_id`、`user_id`、`scope` JSONB、`expires_at`、`local_password_set_at`、`revoked_at`、`tenant_id`、RLS FOR ALL）。授權 API 三核：agent 屬本租戶、user 屬本租戶、user 在主系統至少持有一顆 Agent 能力點；授權只收 `{user_id, expires_at}`，不收範圍（D23）。心跳回應 `offline_operators[]`（sub、display_name、ad_account、scope、expires_at、revoked），其中 `scope` 由 `_scopes_for(user_id)` 依當下能力點即時算、`ad_account` 取 jedi-iam `user_auth_providers.provider_login`（使用者用 AD 登過主系統一次才有）。表上的 `scope` 欄只是授權當下的快照。`remote_agents` 加 `console_allow_cidr`、`outbox_size`、`local_admin_set`、`uptime_sec`、`restart_requested`、`ldap_source`。能力點 `detection.remote-agent.{run-local-scan,net-diag,restart}`，路由綁 `remote-agent-manage` ANY。

**CIDR（P1-5）**：`AGENT_CONSOLE_ALLOW_CIDR`（安裝時問，空＝不限）；心跳收 `console_allow_cidr` 取交集，空交集忽略＋稽核；`127.0.0.0/8`／`::1` 永遠放行；`browser_plane_server` 在 accept 後第一件事檢查。主系統端存前驗交集非空（Agent 回報 `console_allow_cidr_installed`）。

**安裝與升級（P1-6，雛形幕 12）**：新裝題目 4/5 本機管理員密碼（12 碼、二次輸入、argon2id 寫 `local_credentials`）、5/5 白名單；完成畫面保管框。升級：`[ -t 0 ]` 真照新裝問（Enter 跳過），假只紅字。新子命令 `reset-admin-password`／`wipe`／`console-allow-cidr reset` 進 `scripts/install/lib/`，`case` 一行 `source`。`do_re_enroll` 擴充：outbox 非空先問，清名單、清冊、`local_runs`、outbox。`cmd_upgrade` 前查執行中任務，互動問、非互動標 cancelled。

### 5.2 手動掃描（P2-1～P2-3）

**sink 抽取（P2-1）**：

```python
class TaskSink(Protocol):
    def ack(self, task) -> None
    def result(self, task, outcome) -> None       # 失敗可拋，由 outbox 接
    def report(self, task, file) -> str           # 回 file_uid
CloudSink  = 現況三個函式搬進來，行為不變
LocalSink  = 寫 local_runs；report 走既有 file_upload_service；result 同時入 outbox(kind=local_run)
_run_one(task, sink)  # 既有呼叫改傳 CloudSink()；手動執行傳 LocalSink()
```

並發：`_RUNNING_TASKS` 既有；手動執行加計數，掃描 >2 或診斷 >1 進佇列。取消走既有 cancel_event。離線 30 天／未同步在 `start()` 擋。

**ZAP 網址納清冊**：`allowed_targets._target_networks` 加 `_URL_FIELDS`（ZAP 的 `target_url`／`urls`），`urlsplit().hostname` → `socket.getaddrinfo` → 每個 IP 過 `_is_allowed_ip`；解不到＝拒。主系統派的 ZAP 一併受管。

**API（P2-2）**：`GET /console/api/scan/tools`（八支：code、name、configured、capabilities_ok）；`GET .../targets`（清冊：devices[]、networks[]、version、received_at）；`GET .../schema/{tool}`（既有 `tool_schemas`）；`POST .../runs`（`{tool, targets[], params}` → 清冊 check → `credential_resolver` → `LocalSink` → 回 `local_uid`）；`GET .../runs/{uid}`；`POST .../runs/{uid}/cancel`。scope `run-local-scan`。第三方元件：`PUT /scan-input/<uid>` 多認本機 session（`scan_input.py` 驗票處加分支），執行走既有 `sbom_scan` connector，結果走 `source_scans` 既有收件並標 `source=agent_manual`。

### 5.3 網路除錯（P2-4～P2-6）

**`core/netdiag/`**：`dashboard.py` 讀 `ip -j addr`／`ip -j route`／`resolvectl`／`ss -ltn`／`ethtool`／`nmcli`，容器形態（`/.dockerenv` 或無 NetworkManager）回 `unavailable`；回 `{info:{...}, lamps:[{name,state,why}]}` 資訊與燈分開。

十項 probe 共同規則：目標先過 `allowed_targets.is_allowed(ip)` 或三固定例外（主系統 host、預設閘道、DNS，由 Agent 自己算不收 body）；`subprocess.run([...], shell=False, timeout=固定)`；輸出長度上限 64KB、固定 regex 解析；連線之間 `sleep(1)`；同目標每分鐘 3 輪（記憶體 token bucket）。各項逾時：ping 10s／DNS 5s／埠每個 3s（上限 5 個）／TLS 10s／HTTP 10s／SSH 10s／MTU 10s／cloud 序列 60s／traceroute 60s（無 `CAP_NET_RAW` 回 unavailable）／mtr 式 60s。TLS 用 `ssl.create_default_context()`，捕 `SSLCertVerificationError` 當結果；程式不得出現 `CERT_NONE`。HTTP 用 `httpx`：不跟隨跨 host 轉址、最多 5 跳、只取標頭、大小上限。

**`limits.estimate(selection)`**（D20）：連線數 = ping 4 ＋ DNS 1 ＋ 埠 N ＋ TLS 1 ＋ HTTP 1 ＋ SSH 1 ＋ MTU 1 ＋ cloud 5 ＋ traceroute 30 ＋ mtr 60；秒數 = 連線數 ＋ 各項逾時估；等級：含 mtr＝高，含 traceroute 或連線數 >10＝中，否則安靜。`POST runs` 等級非安靜需 `confirm=true`。

**`verdict.py`**：確定性規則表（fixture 驅動），輸出結論／依據／建議動作三句；主系統連線診斷固定序列 DNS → 閘道 → 主系統 → 443 → TLS（`describe_tls_failure`）→ 心跳端點 → 時鐘（`clock_skew`）→ 代理，三種結論：連不到／連得到但身分被拒（401／403）／憑證驗不過。

**報告**：JSON＋文字＋PDF（既有 WeasyPrint 路徑，`html.escape`）；存 `upload_files`；`local_runs.kind=diag`。

### 5.4 續傳與主系統端（P3）

**outbox（P3-1）**：`CloudSink.result` 失敗 → 入列（`idempotency_key = f"{kind}:{uid}:{status}"`）；心跳成功後 `drain()`：依 `created_at` 送最多 50 筆，退避 1／2／4／8 分上限 1 小時，送成功才刪；`heartbeat_status` 加遲滯（3 敗→offline、2 成→online）；磁碟低於門檻停止入列 audit 類。心跳 payload 帶 `outbox_size`。

**主系統冪等（P0-6）**：`receive_result` 短路條件 `task.status in TERMINAL` → 回 200 不重寫；`POST /agents/local-runs` 批次 ≤50、`agent_id` 從通行證取、`(agent_id, local_uid)` 唯一衝突視為已收；已停用／撤銷 Agent 拒收。

**reaper（P0-6 套件＋P3-5 接線）**：`run_daily_tick()`：`agent_tasks.status in (dispatched, running)` 且該 Agent `last_heartbeat_at < now - 7d` → `failed`、`fail_reason=agent_unreachable`；0 筆不寫 log。BE `core/scheduler_agent.py` 註冊 `@_job_scoped("agent_task_offline_reaper")`，`scheduler.py` 只加一行。

**離線顯示（P3-4a／P3-4b）**：`_enrich_online` 加 `agent_offline_waiting`、`agent_last_heartbeat_at`；FE 任務頁橫幅、清單「待傳」「LDAP」欄。

### 5.5 LDAP（P0-1／P0-2／P0-3／P0-7／P4-1～P4-3）

**`jedi-ldap`**（新套件，只靠 `ldap3`）：

```python
@dataclass class LdapConfig: host, port=389, encryption="starttls"|"ssl"|"none", base_dn, is_ad, bind_user, bind_password, ca_pem=None
class FingerprintStore(Protocol): get(host) -> str|None; set(host, fp, meta)
class LdapDirectory:
    def __init__(cfg, store: FingerprintStore)
    def test_connect() -> ConnectResult(ok, reason: "ok"|"unreachable"|"cert_changed"|"cert_invalid"|"bind_failed"|"plaintext_warning", fingerprint, cert_meta)
    def verify_user(username, password) -> DirectoryUser(uid, name, email, groups) | raises LdapAuthError(reason)
```
首次連線 `store.get(host) is None` → 記指紋；之後不同 → `cert_changed`，不連。`encryption="none"` 回 `plaintext_warning`。不 import Flask、不碰任何表。

**jedi-iam 瘦身（P0-2）**：`LdapAdapter` 只留「`DirectoryUser.uid` → `user_auth_providers` → `users`」綁定段；LDAP bind 失敗走既有 `login_failed_count` 同一路徑；`LDAP_VERIFY_CERT` 退役，`LDAP_TRUSTED_FINGERPRINT`／`LDAP_TRUSTED_AT` 由 system-core 設定組提供（P0-3）並當 `FingerprintStore`；`/ldap/connect-test` 換 host 不帶舊密碼。

**Agent 三選一（P4-1）**：`ldap_source` ∈ synced／local／none。synced：心跳 `ldap_config` 帶主系統設定與服務帳密（主系統解密後經 mTLS 心跳下發；Agent 收到後用自己的 `credential_store` key 加密保存、不落明文、不進 log），指紋由主系統下發；local：主控台表單（`secret_form.py`）存 `credential_store`，指紋存 `local_credentials`（`kind=ldap_fingerprint`）；none：略過。登入分支見 5.1 ③。Nuitka `--include-package=ldap3,jedi_ldap`。

### 5.6 儲存設定搬 UI（P2-8a／P2-8b）

`secret_form.py`：通用「欄位定義 → 驗證 → 祕密欄進 `credential_store`、其餘進設定檔 → 測試連線 hook → 套用 hook」。工具帳密（既有）、儲存設定、LDAP 自設三者各一份欄位定義。儲存設定的表單只有一個選項「系統內建 SeaweedFS」（D22），GET 不回任何端點、帳號、金鑰，只回 `current_type` 說明現在存哪；沒有內建儲存的機器（原生安裝、源碼模式）套用與測試回 409 並指向 CLI；「改回安裝時設定」保留。套用後 `storage` 模組 reload，不重啟。FE `SecretForm.vue` 同理三頁共用。

### 5.7 安全規則對照

全表在討論稿 §13.6；每張卡的紀律段帶「資安規範」行，runner 回寫列對到哪幾條。本案特別要守：四種登入失敗同回應（SR-01.1）、body 上限在 before_request（SR-04.1）、機器身分只認通行證且只動自己名下（SR-01.6／01.7）、新表 RLS FOR ALL（SR-02.1）、授權三核（SR-02.4）、argon2id 且 API 不回雜湊（SR-06.1）、密碼欄進遮罩（SR-06.2）、subprocess 不經 shell（SR-04.8／04.10）、`CERT_NONE` 禁用（SR-03.2）、兩條例外登記（§3 callout）。

---

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

**批（Stage）＝工作坊同時開幾個小弟一起跑的那一組**：同一批不改同一個既有檔、不依賴對方產出、不搶同一外部資源。階段（P0～P4）只是分組標籤。底表在 [`handoff/cards.py`](handoff/cards.py)，兩個斷言已過：依賴都在更早的批；同批同 repo 無兩張卡改同一既有檔。

**登記型檔先佔位（解掉 7 組撞檔）**：`core/console/routes.py` 由 P1-4 一次掛四支 blueprint（後三支先空殼）；`core/enroll.py` 由 P1-3 一次掛三個 observer（後兩支先 no-op）；`console/Shell.vue` 由 P1-7a 一次寫全九項選單（view 懶載入指向佔位）並佔好 i18n key。後面的卡只填自己的檔。

**repo 分布**：evidence-agent 15、FE 11、BE 8、jedi 套件 6、test 1。**套件異動（發版前統一確認）**：jedi-ldap 新建、jedi-iam、jedi-system-core、jedi-remote-agent；開發期走 path dependency，發版等令。

| 批 | 階段 | 卡 | repo | reuse／extend／new＋理由 | 改哪些既有檔 | 新開哪些檔 | 依賴 | 驗收（對照雛形） |
|---|---|---|---|---|---|---|---|---|
| 1 | P0 | **P0-1** 抽 jedi-ldap 零相依套件：目錄連線、首次信任釘住、結構化失敗 | jedi 套件 | new（抽出）：從 jedi-iam `ldap_adapter.py` 搬目錄連線段，不是重寫；套件只靠 ldap3 |  | `jedi-ldap/（整支：`LdapDirectory`、`LdapConfig`、`DirectoryUser`、`FingerprintStore` protocol、tests）` | — | `LdapDirectory(cfg).test_connect()` 對 DEV 的 LDAP 回結構化結果；指紋第一次記、第二次比對、變了回 `CertChanged`；`grep -r flask jedi-ldap/` 為 0；tests 綠且含突變（把比對拿掉要紅） |
| 1 | P0 | **P0-4** Agent 本機 DB 最小版本機制：schema_version＋有序 migration list（D16） | evidence-agent | extend：把 `app_factory.py` 的 `create(checkfirst)`＋`_heal_upload_files_columns` 收成 v1，不另寫第二套建表 | `core/app_factory.py 208` | `core/local_db/__init__.py`、`core/local_db/migrations.py` | — | 空 DB 啟動→`schema_version=2`、四張表在；舊 DB（只有 upload_files）啟動→升到 2 且既有列不動；重啟不重套；test 含突變（把 v2 拿掉要紅） |
| 1 | P0 | **P0-5** jedi-remote-agent 兩張新表、remote_agents 新欄、三個能力點、domain／repo／service | jedi 套件 | extend：照套件既有 domain→repo→service 形狀加兩個 aggregate；能力點進既有 `plugin/contract.py` 清單 | `jedi_remote_agent/plugin/contract.py`、`infra/remote_agent/model/remote_agent.py 87`、`domain/.../remote_agent_domain_service.py 171`、`migrations/011-remote-agent-offline-operators.sql（新號）` | `domain/offline_operator/*`、`domain/local_run/*`、`infra/offline_operator/*`、`infra/local_run/*`、`app/service/offline_operator_service.py`、`app/service/local_run_service.py` | — | DEV 套 011 後 `\d compliance.remote_agent_offline_operators`／`agent_local_runs` 有 RLS 四指令；`test_rls_all_tenant_tables.py` 綠；service 單元測試綠 |
| 1 | P1 | **P1-7a** FE console 殼改側欄＋頂欄＋狀態列、登入頁骨架、選單清單一次寫全（照雛形幕 1～2，先接假 state） | FE 主站／console | extend：`App.vue` 的 phase 機制保留，TabView 換 `Shell.vue`；登入頁從主站 `Login.vue` 複製版面刪分流。**登記型檔集中**：`Shell.vue` 的選單是一份清單常數（九項全寫、view 用 `defineAsyncComponent` 懶載入、檔不存在先指向佔位），之後各分頁卡只新開自己的 view 檔、不碰 `Shell.vue`；i18n `agent-console.json` 各頁的 key 也在這張卡一次佔好（值先放中文占位） | `console/App.vue 149`、`console/main.js 51`、`console/console.scss 91`、`i18n agent-console.json` | `console/Shell.vue`、`console/Login.vue`、`console/state.js` | — | `npm run build:CONSOLE` 過；開 dist 看到側欄 14rem teal、頂欄、狀態列三態切換；登入頁三分頁依 state 變；`grep border-left console/` 為 0 |
| 2 | P0 | **P0-2** jedi-iam 瘦身接 jedi-ldap：失敗計入鎖定、憑證釘住欄、換主機重填密碼 | jedi 套件 | extend：`LdapAdapter` 刪目錄段改呼叫 jedi-ldap，只留綁定 30 行；鎖定計數走既有 `login_failed_count` 路徑，不另開計數器 | `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 | LDAP 密碼錯 5 次→鎖（與本地同一計數）；`LDAP_VERIFY_CERT` 不再被讀；連線 test 換 host 不帶舊密碼；既有 jedi-iam tests 綠＋新增三條突變 |
| 2 | P0 | **P0-6** jedi-remote-agent 心跳契約擴充、終態冪等、local-runs 批次端點、restart 端點、7 天 reaper、事件碼 | jedi 套件 | extend：心跳收發照 `heartbeat_console.py` 白名單收法加欄位；`receive_result` 短路條件從 cancelled 擴成任何終態（改既有判斷，不另開路徑）；reaper 照 `run_daily_tick` 既有排程形狀 | `app/service/agent_enrollment_service.py 608`、`app/service/agent_task_service.py 170`、`api/routing.py 142`、`app/service/heartbeat_console.py`、`common/code.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 | 同 uid 重送 succeeded 回 200 且 `write_success` 只跑一次（test）；心跳回應含 `offline_operators[]`；`POST /agents/local-runs` 51 筆回 400、重複 `(agent_id,local_uid)` 去重；reaper 對離線 8 天的 dispatched 標 failed、7 天不動 |
| 2 | P1 | **P1-3** Agent 離線身分：名單快取 observer、local-admin／離線密碼 argon2id、鎖定、有效期、30 天停用、未同步判定；一次掛三個心跳 observer | evidence-agent | extend：`local_login.py` 既有 `LOCAL_ADMIN_SUB`＋`_create_local_session` 加密碼路徑，不新身分；session 走既有 `SessionStore` 加 `revoke_by_sub`；observer 照 `_observe_allowed_targets` 形狀。**登記型檔集中**：`enroll.py` 這張卡一次掛 `offline_operators`／`restart_flag`／`outbox.drain` 三個 observer（後兩支先放 no-op 檔），P2-9／P3-1 只填自己的檔、不碰 `enroll.py` | `core/enroll.py 411（掛三個 observer）`、`core/console/session.py 75`、`core/console/local_login.py 188`、`core/heartbeat_status.py` | `core/console/local_auth.py`、`core/offline_operators.py`、`core/local_credentials.py`、`core/restart_flag.py（no-op 殼）`、`core/outbox.py（no-op 殼）` | P0-4 | `local_auth.verify(sub, pw)` 對名單外／密碼錯／到期／被鎖回同一個結果物件；5 次錯鎖 15 分（monotonic）；撤銷→該 sub session 全消；30 天停用旗標對；未同步判定＝`allowed_targets.received and operators.received` |
| 2 | P2 | **P2-4** 網路除錯執行器 core/netdiag/：儀表板讀取、十項診斷、estimate 噪音、判讀規則、報告 | evidence-agent | new（新模組）＋reuse：TLS 用既有 `cloud_trust.describe_tls_failure`、時鐘用 `clock_skew`、目標守門用既有 `allowed_targets.is_allowed()`；PDF 走既有 WeasyPrint 路徑 |  | `core/netdiag/__init__.py`、`dashboard.py`、`probes/{ping,dns,port,tls,http,ssh,mtu,traceroute,mtr,cloud}.py`、`verdict.py`、`report.py`、`limits.py` | — | 每支 probe `shell=False`、參數陣列、逾時固定（grep 守衛）；`limits.estimate()` 對全勾回 >100 次、等級高；埠 >5 拒；`verdict` 對固定 fixture 回確定結論；`grep CERT_NONE` 為 0；未同步只放 cloud 序列 |
| 3 | P0 | **P0-3** jedi-system-core LDAP 設定組加釘住指紋欄、服務帳密改 Fernet | jedi 套件 | extend：`THIRD_PARTY_LOGIN` 設定組加兩鍵；Fernet 用既有 `jedi_common` 加密 helper（先 grep），不另寫 | `jedi_system_core 設定 schema`、`migrations（套件）` |  | P0-2 | 設定存取回 `trusted_fingerprint`／`trusted_at`；明文密碼升級後讀回是密文、解得開；既有設定頁不壞 |
| 3 | P0 | **P0-7** 主站 LDAP 設定頁：拿掉驗證憑證勾選、已信任憑證＋重新信任、預設 STARTTLS（雛形幕 11） | FE 主站／console | extend：改 `LdapConfigForm.vue` 既有欄位，不新開頁；錯誤碼同步 `error-code.json` | `src/views/ldap-config/LdapConfigForm.vue 279`、`src/config/locales/.../error-code.json`、`src/config/api/api.js` |  | P0-2 | 幕 11 三變化；`grep verify_cert` 為 0；加密下拉預設 STARTTLS、「無」在末尾標僅限測試；憑證變了顯示紅帶＋重新信任按鈕 |
| 3 | P1 | **P1-1** 主系統接線：plugins/remote_agent 接 P0-5／P0-6 port、三端點能力點、能力點 migration、CIDR 交集驗證 | BE | extend：`core/plugins/remote_agent.py` 既有 adapters 加 port，新邏輯進 `_agent_offline_operators.py`（照 `_agent_vulndb.py` 先例）；能力點 migration 照 FR-157 `2026-10-22-fr157-agent-console-capabilities.sql` 形狀 | `core/plugins/remote_agent.py 289`、`core/host_capabilities.py 275` | `core/plugins/_agent_offline_operators.py`、`scripts/sql/2026-xx-xx-fr166-agent-standalone-capabilities.sql` | P0-5、P0-6 | DEV 套 migration 後三能力點在；`_ADMIN_CAPABILITIES` 三端點登記；CIDR 無交集回 400；BE 重啟實打三端點 200／403 |
| 3 | P1 | **P1-4** Agent 登入 API：local-session、me/password、state；稽核五個新動作；一次登記四支 console blueprint | evidence-agent | extend：守門用既有 `require_session(scope)`；稽核用既有 `audit.record`＋`REPORTED_ACTIONS` 加五值。**登記型檔集中**：`routes.py` 這張卡一次掛 `local_auth_routes`／`scan_routes`／`netdiag_routes`／`ops_routes`（後三支先放空 blueprint 檔），之後 P2-2／P2-5／P2-8a 只填自己的檔、不碰 `routes.py` | `core/console/routes.py 212（掛四支 blueprint）`、`core/console/audit.py 138（REPORTED_ACTIONS 加五值）` | `core/console/local_auth_routes.py`、`core/console/scan_routes.py（空殼）`、`core/console/netdiag_routes.py（空殼）`、`core/console/ops_routes.py（空殼）` | P1-3 | 四種失敗同 401 同 body（test）；body >1KB 413；`GET /console/state` 無 session 可打、回四態；改密碼要舊密碼；稽核 jsonl 有 `local_login` 無密碼欄 |
| 3 | P1 | **P1-5** 主控台連入 CIDR 白名單：安裝底線、心跳交集、隱含 loopback、連入檢查、CLI reset（開 scripts/install/lib/） | evidence-agent | new（小模組）＋extend：`browser_plane_server.py` 連入檢查加一處；CLI 子命令進新開 `lib/`（install.sh 超標只准拆），`case` 一行呼叫 | `core/browser_plane_server.py 218`、`config/config.py 162`、`scripts/install/install.sh 3426（case 一行）`、`deploy/install.sh 2894（case 一行）` | `core/console_allow_cidr.py`、`scripts/install/lib/console_allow_cidr.sh` | — | 底線 `192.168.10.0/24` 時 10.0.0.1 連 8444 被拒、127.0.0.1 放行；心跳下發更寬的被忽略＋稽核；`console-allow-cidr reset` 後不限；兩份 install.sh 都只多一行 |
| 3 | P2 | **P2-1** Agent 手動執行骨架：_run_one 抽 sink、local_runs 寫入、並發上限、ZAP 網址納清冊 | evidence-agent | extend（抽共用）：`_run_one` 的 ack／result／report-sink 抽成可注入介面，雲端 sink＝現況、本機 sink 新增；**不複製 _run_one**；`allowed_targets.py` 加網址欄位解析（改既有 `_target_networks`） | `core/task_executor.py 369（淨減）`、`core/allowed_targets.py 203` | `core/task_sinks.py`、`core/local_runs.py` | P0-4、P1-3 | 主系統派工行為不變（既有 tests 綠）；本機 sink 寫 `local_runs`；第 3 個掃描排隊；ZAP `https://x/` 解成清冊外 IP 被拒、清冊內放行；離線 30 天／未同步時 `start()` 拒 |
| 4 | P1 | **P1-2** 主站離線操作者授權對話框＋清單欄（雛形幕 10 上半） | FE 主站／console | new（子元件）：`RemoteAgentManage.vue` 766 只加一行掛＋一欄，不往裡加；表單元件 reuse 主站既有 Dialog／DataTable | `src/views/remote-agent/RemoteAgentManage.vue 766（一行＋一欄）`、`src/config/api/api.js`、`i18n` | `src/views/remote-agent/OfflineOperatorsDialog.vue` | P1-1 | 幕 10 離線操作者段；沒能力點的格反灰；「Agent 檔案」欄標整台；0 位紅字；撤銷後「有待同步的撤銷」 |
| 4 | P1 | **P1-6** 安裝與升級流程：新裝強制設 local-admin、升級分互動／無人值守、reset-admin-password／wipe、re-enroll 清本機、upgrade 前問執行中（雛形幕 12） | evidence-agent | extend：兩份 install.sh 各只在 `case` 加行、安裝題目段加兩題；新子命令進 P1-5 開好的 `lib/`；re-enroll 擴充既有 `do_re_enroll` | `scripts/install/install.sh 3426`、`deploy/install.sh 2894` | `scripts/install/lib/local_admin.sh`、`scripts/install/lib/wipe.sh` | P1-3、P1-5 | 新裝不設密碼裝不完；`upgrade </dev/null` 不問只紅字；`reset-admin-password` 後舊密碼失效；`wipe` 先清 SQLite 再刪目錄；re-enroll 有 outbox 先問；shellcheck 綠 |
| 4 | P1 | **P1-7b** FE console 登入頁接真端點：三態、四種登入、設離線密碼對話框、我的離線密碼、改密碼（雛形幕 1 五變化） | FE 主站／console | extend：接 P1-7a 殼與 P1-4 端點；`api.js` 401 走既有 `onSessionLost` | `console/Login.vue`、`console/Shell.vue`、`console/state.js`、`console/api.js 74` | `console/SetOfflinePasswordDialog.vue` | P1-7a、P1-4 | 幕 1 五變化實打：離線密碼登入進總覽；失敗同一句；斷線 30 天只剩本機管理員；跳轉首次自動跳設密碼框；改密碼成功 |
| 4 | P2 | **P2-2** 手動掃描 API：tools／targets／schema／runs／cancel；第三方元件走既有 scan-input 與 source_scans | evidence-agent | extend：`scan_input.py` 多接受本機 session（不另開上傳口）；帳密走既有 `credential_resolver`；表單定義走既有 `tool_schemas` | `core/console/scan_routes.py（P1-4 已掛的空殼，填內容）`、`core/scan_input.py` |  | P2-1 | `GET tools` 回八支含帳密狀態；`POST runs` 清冊外 403；未同步 403；sbom 走 `PUT /scan-input` 本機 session 成功；取消走既有 `_RUNNING_TASKS` |
| 4 | P2 | **P2-5** 網路除錯 API：dashboard／targets／runs／report；未同步只放 cloud 序列 | evidence-agent | extend：掛既有 blueprint；守門 `require_session('net-diag')`；寫入走 P2-1 的 `local_runs` | `core/console/netdiag_routes.py（P1-4 已掛的空殼，填內容）` |  | P2-1、P2-4 | `POST runs` 勾全部回 estimate 超門檻需 `confirm=true`；未同步只准 cloud；報告 txt／pdf 下載過 scope；`local_runs` kind=diag |
| 4 | P2 | **P2-8a** 維運 API：重啟（優雅結束）、uptime、支援診斷包、儲存設定表單 API（抽 secret_form 與工具帳密共用） | evidence-agent | extend（抽共用）：`tools.py` 213 的表單儲存段抽成 `secret_form.py`，工具帳密與儲存設定、P4-1 的 LDAP 都用它；重啟掛 `main_app.py` 結束鉤子 | `main_app.py 31`、`core/console/tools.py 213（抽出）`、`core/console/ops_routes.py（P1-4 已掛的空殼，填內容；儲存設定路由也放這支）` | `core/console/ops.py`、`core/console/secret_form.py`、`core/support_bundle.py` | P1-3 | 重啟 when_idle 等任務完才 exit 0、now 立即且任務標 cancelled；診斷包 zip 無帳密無清冊；儲存表單四型別存讀測；`tools.py` 行數淨減且既有工具帳密 tests 綠 |
| 5 | P1 | **P1-8** 總覽 API：狀態燈、這台 Agent 資訊、正在執行（雛形幕 2 三區塊） | evidence-agent | extend：`overview.py` 33 加三組組裝，燈與資訊分開回；資料來源全 reuse（heartbeat_status、disk_space、local_runs、credential_store） | `core/console/overview.py 33` |  | P1-3、P2-1 | 回應三區：`lamps[]`（name/state/why）、`info{}`、`running[]`；未同步回 `registration{}` 且其他三區空 |
| 5 | P2 | **P2-9** 心跳旗標重啟 observer：when_idle／now | evidence-agent | extend：observer 照既有形狀掛 `enroll.py` 一行；重啟動作 reuse P2-8a 的 ops | `core/restart_flag.py（P1-3 已掛的 no-op 殼，填內容）` |  | P1-3、P0-6、P2-8a | 心跳帶 `restart: now`→任務 cancelled 後 exit；`when_idle`→等；回報 `last_restart_reason` |
| 5 | P3 | **P3-1** Agent outbox：表、入列、心跳後 drain、退避、丟棄順序、離線遲滯、磁碟門檻 | evidence-agent | extend：`_result` 走 P2-1 抽好的 sink，失敗入列；drain 掛 `enroll.py` observer；idempotency_key 配 P0-6 終態冪等 | `core/outbox.py（P1-3 已掛的 no-op 殼，填內容）`、`core/task_sinks.py（雲端 sink 失敗→入列）` |  | P0-4、P1-3、P2-1 | 斷網跑完任務→outbox 1 筆；復線一次心跳後 0 筆且主系統收到一次；滿 10,000 先丟 audit 再 local_run、task_result 不丟；3 敗切離線 2 成切在線 |
| 5 | P3 | **P3-2** 主系統收自主執行紀錄：local-runs 端點接線、落庫、報告下載走資料面 JWT、nickname enrich | BE | extend：接 P0-6 端點到 plugins；下載 reuse 既有 `data_plane_auth`；enrich 用 `common/util/audit_nickname.py` | `core/plugins/remote_agent.py` | `core/plugins/_agent_local_runs.py` | P0-6、P1-1 | Agent 送 2 筆→`agent_local_runs` 2 列含 `started_by_name`；重送不增；下載 URL 帶資料面 JWT 能拿到報告 |
| 5 | P3 | **P3-5** 7 天 reaper 接線：抽 core/scheduler_agent.py（scheduler.py 714 只准拆） | BE | extend（拆檔）：`scheduler.py` 加一行呼叫，邏輯進新檔；reuse P0-6 `run_daily_tick()` | `core/scheduler.py 714（一行）` | `core/scheduler_agent.py` | P0-6 | 排程具名 `agent_task_offline_reaper`；DEV 造一筆離線 8 天 dispatched→失敗 `agent_unreachable`；0 筆不寫 log |
| 5 | P3 | **P3-7** 守衛測試（Agent 側七條）：入口過清冊、無 shell=True、無 nmap/tcpdump/iperf、local-admin 清冊外 403、未同步 403、無新增使用者路由、無 CERT_NONE | evidence-agent | new（test）：照 BE `test/test_module_boundaries.py` 形狀寫 grep 型守衛 |  | `test/test_fr166_guards.py` | P2-2、P2-5 | 七條綠；每條有「故意違規一次會紅」的突變證明寫在 docstring |
| 6 | P2 | **P2-3** FE console 掃描工具分頁：步驟條、工具卡、清冊目標、tool_schemas 表單、執行中、ZAP 網址、第三方元件上傳（雛形幕 3 四變化） | FE 主站／console | reuse：參數表單用主站 `DetectionConfigField`；步驟條照主站派任務精靈；上傳走既有 scan-input 元件 | `console/Shell.vue（加選單項）` | `console/views/Scan.vue`、`console/scanApi.js` | P1-7b、P2-2 | 幕 3 四變化實打；手打清冊外 IP 當場擋；ZAP 網址解析顯示；sbom 上傳到這台；執行中進度輪詢、取消 |
| 6 | P2 | **P2-6** FE console 網路除錯分頁：資訊區＋連線狀態燈、目標、勾選、噪音計算條、超門檻確認框、路徑圖、判讀卡（雛形幕 4） | FE 主站／console | new（元件）：`PathFigure`／`VerdictCard` 新開；燈元件與總覽共用（P1-8 同形） |  | `console/views/NetDiag.vue`、`console/components/PathFigure.vue`、`console/components/VerdictCard.vue`、`console/netdiagApi.js` | P1-7b、P2-5 | 勾項目即時打 estimate 更新噪音條；超門檻跳框列原因；確認後結果路徑圖＋判讀卡；匯出 txt／pdf |
| 6 | P2 | **P2-7** FE console 執行紀錄分頁：同一表、來源欄、回傳三狀態、立即重試、報告下載（雛形幕 5） | FE 主站／console | reuse：主站 DataTable 慣例；重試打 P3-1 的 resend |  | `console/views/Runs.vue` | P1-7b、P2-2、P3-1 | 幕 5；離線重試反灰；在線重試後變已回傳附時間；拒絕列有 |
| 6 | P2 | **P2-8b** FE console 服務與重啟分頁＋總覽改版：狀態卡、重啟確認框、本機設定、儲存位置表單、診斷包；總覽三區（雛形幕 6、幕 2） | FE 主站／console | reuse：儲存表單與工具帳密表單同一個元件（`Tools.vue` 467 的表單段抽成 `SecretForm.vue`，兩頁共用） | `console/views/Overview.vue 141`、`console/views/Tools.vue 467（抽出）` | `console/views/Ops.vue`、`console/components/SecretForm.vue`、`console/components/LampGrid.vue` | P1-7b、P1-8、P2-8a | 幕 6 兩變化、幕 2 三態；`Tools.vue` 行數淨減且既有工具帳密頁不壞；燈元件總覽與除錯頁同一支 |
| 6 | P3 | **P3-3** 主站自主執行紀錄頁 BE 側：ui_routes＋route_capabilities migration（FR-132 守衛） | BE | extend：照既有 `ui_routes` migration 形狀一支 |  | `scripts/sql/2026-xx-xx-fr166-agent-local-runs-route.sql` | P1-1 | DEV 套後選單有「Agent 自主執行紀錄」且綁 `read` 或 `net-diag`；FR-132 守衛測試綠 |
| 6 | P3 | **P3-4a** jedi-remote-agent `_enrich_online` 補離線等待回傳狀態欄（detection_orchestration_service 只讀不加） | jedi 套件 | extend：在既有 `_enrich_online` 加兩欄；不碰 jedi-detection 超標檔 | `jedi_remote_agent/.../remote_agent_service.py `_enrich_online`` |  | P0-6 | 任務序列化回 `agent_offline_waiting: true`＋`agent_last_heartbeat_at` 當 Agent 離線且任務 dispatched/running |
| 7 | P3 | **P3-3b** 主站自主執行紀錄頁 FE：篩 Agent／種類／工具／人／日期、報告下載、離線標示（雛形幕 10 中段） | FE 主站／console | new（view）：主站 DataTable 慣例；篩選 SegmentButtons | `src/config/api/api.js`、`i18n`、`路由表` | `src/views/remote-agent/AgentLocalRunsView.vue` | P3-2、P3-3 | 幕 10 自主執行紀錄段；Agent 離線那筆顯示「報告在離線的 Agent 上」；下載走資料面 |
| 8 | P3 | **P3-4b** 主站 Agent 清單待傳／LDAP 欄＋要求重啟對話框＋任務頁離線橫幅（雛形幕 10 清單與下半） | FE 主站／console | new（兩個子元件）`RestartAgentDialog.vue`、`AgentOfflineBanner.vue`；`RemoteAgentManage.vue` 766 只加兩欄一顆按鈕，不往裡加邏輯 | `src/views/remote-agent/RemoteAgentManage.vue（兩欄＋一行按鈕）`、`任務頁元件`、`i18n` | `src/views/remote-agent/RestartAgentDialog.vue`、`src/components/remote-agent/AgentOfflineBanner.vue` | P3-4a、P3-6 | 幕 10 清單三欄、要求重啟段、任務頁顯示段；送出重啟後 Agent 下次心跳重啟 |
| 7 | P3 | **P3-6** 主系統要求重啟接線（BE） | BE | extend：接 P0-6 端點到 `_ADMIN_CAPABILITIES` | `core/plugins/remote_agent.py` |  | P0-6、P1-1、P2-9 | `POST /remote-agents/{uid}/restart` 回 200 且下次心跳回應帶 `restart`；無 `restart` 能力點 403 |
| 7 | P4 | **P4-1** Agent LDAP 三選一：裝 jedi-ldap、ldap_source、同步收心跳 ldap_config、登入多一分支、Nuitka include | evidence-agent | extend：登入分支加在 P1-3 的 `local_auth.py`；設定表單用 P2-8a 的 `secret_form.py`；指紋存既有 `local_credentials` | `core/console/local_auth.py`、`scripts/build/build_agent_release.sh`、`pyproject.toml（加 jedi-ldap）` | `core/ldap_source.py` | P0-1、P1-3、P2-8a | synced 模式收心跳設定後 AD 密碼登得進；名單外 AD 對仍 401；local 模式自設可連；none 退回離線密碼；Nuitka build 含 ldap3 |
| 8 | P4 | **P4-2** 主系統 LDAP 設定下發心跳：ldap_config 組裝（服務帳密密文下發、Agent 轉存）、清單 LDAP 欄值 | BE | extend：照 `_agent_vulndb.py` 先例新開 `_agent_ldap_config.py`；設定讀 P0-3 的 system-core | `core/plugins/remote_agent.py` | `core/plugins/_agent_ldap_config.py` | P0-3、P0-6、P4-1 | 心跳回應含 `ldap_config`（密文）；Agent 端轉存後能用；清單 LDAP 欄顯示 synced/local/none |
| 8 | P4 | **P4-3** FE console LDAP 設定分頁：三選一、同步唯讀、自設表單（雛形幕 7） | FE 主站／console | reuse：表單用 P2-8b 的 `SecretForm.vue` |  | `console/views/LdapSettings.vue` | P2-8b、P4-1 | 幕 7；同步模式唯讀；自設測試連線；指紋顯示 |
| 9 | P4 | **P4-6** e2e：主站 LDAP 登入鎖定與釘住、Agent 管理頁三個新動作 | test | reuse：site-regression 既有 page object |  | `site-regression features/steps（LDAP、agent-manage）` | P0-7、P1-2、P3-3b、P3-4b | headless 綠；Agent 主控台不在範圍 |
| 9 | P4 | **P4-4** SPEC：Agent 主控台頁改版、Agent 管理頁、LDAP 設定頁（writing-feature-specs） | BE | extend：改既有 spec 頁 | `docs/spec-site/current/（三頁）` |  | 全部實作卡 | 三頁變更紀錄各一行；文件衛生 grep 0 |
| 9 | P4 | **P4-5** 手冊與安裝文件：Agent 獨立作業一章、安裝需求、資安例外登記兩條、出貨基線待重產回報 | BE | extend：手冊既有章節結構；例外登記進 `docs/security-report/requirements/` | `docs/user-manual/`、`docs/security-report/requirements/` | `手冊章節` | 全部實作卡 | 章節齊；SR-08.2／SR-04.9 例外登記可查；文件衛生 grep 0；回寫明講出貨基線待重產。Agent repo `deploy/README.md` 的安裝需求段由 P1-6 順手改，本卡只引用 |

**決策者功課（開工前）**：批 1 派出前確認五個 repo 都在 `feature/FR-139`。卡號對照見 `handoff/FR-166-STATE.md` 任務總表。

---

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

| # | 步驟 | 期望 |
|---|---|---|
| 1 | 主系統 Agent 管理頁勾王小明為高雄 Agent-01 離線操作者（四範圍、90 天） | 下次心跳 Agent SQLite `offline_operators` 有一列；清單欄「1 位」 |
| 2 | 王小明從主系統「開啟主控台」跳進去 | 自動跳「設定這台的離線密碼」；設完側欄底部「離線登入」可見 |
| 3 | 拔 Agent 對主系統的線；直接打 `https://<agent>:8444/console` | 登入頁三分頁（AD／離線密碼／本機管理員）；橫幅「主系統連線中斷」 |
| 4 | 用離線密碼登入 | 總覽：紅帶斷線第 N 天；狀態燈「主系統連線」異常；正在執行 |
| 5 | 掃描工具：OpenSCAP → 勾 web-01 → 參數 → 開始 | 執行中進度；完成後執行紀錄一列「Agent 手動」、回傳「待回傳」 |
| 6 | 手打 192.168.77.5（清冊外） | 當場擋；執行紀錄一列「拒絕」；稽核有 |
| 7 | 網路除錯：目標 db-01，全勾，開始 | 噪音條「高」、連線數 >100；確認框列原因；確認後路徑圖＋判讀卡 |
| 8 | 只勾 ping | 噪音「安靜」，不跳框 |
| 9 | 接回線 | 兩次心跳內：執行紀錄「已回傳」附時間；主系統自主執行紀錄頁看到第 5 步那筆；下載報告拿到同一份 |
| 10 | 主系統任務頁，Agent 離線期間派一筆到它 | 顯示「Agent 離線中，等待回傳」；不計失敗；接回線後跑完 |
| 11 | 用 local-admin 登入（在線） | 全部分頁可進；「服務與重啟」有「變更」密碼；掃清冊外仍 403 |
| 12 | 斷線 31 天（改 Agent 時鐘或改 `last_heartbeat_ok_at`） | 登入頁只剩本機管理員；離線密碼 401；手動掃描 403；主系統派的任務復線照跑 |
| 13 | 全新安裝 | 問本機管理員密碼與白名單，不設不給裝完；完成畫面保管框；裝完未註冊打網址只有本機管理員能按，登入後只有主系統連線診斷 |
| 14 | `upgrade </dev/null` | 不問密碼、只紅字；主控台總覽與主系統清單紅字「本機管理員密碼未設定」 |
| 15 | 主系統 LDAP 設定頁 | 無「驗證憑證」勾選；第一次測試連線後顯示「已信任」指紋；換憑證後登入被擋、紅帶、重新信任後恢復 |
| 16 | LDAP 密碼連錯 5 次 | 鎖 15 分（與本地同計數） |
| 17 | Agent LDAP 設定選「跟主系統同步」 | 心跳後 AD 密碼登得進；名單外 AD 密碼對仍 401 |
| 18 | 主系統「要求重啟」when_idle | Agent 跑完任務後重啟；uptime 歸零；清單看到「上次重啟原因」 |
| 19 | 主控台「服務與重啟」改儲存位置為自有 SeaweedFS，測試連線，儲存並套用 | 不重啟生效；舊檔警語；CLI `configure-storage` 仍可用 |
| 20 | 守衛測試 | Agent `test_fr166_guards.py` 七條綠；BE `test_rls_all_tenant_tables.py`、FR-132 路由守衛綠 |

---

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

| 風險 | 怎麼擋 |
|---|---|
| local-admin 密碼被現場共用、稽核分不出誰 | 手冊：平常走離線操作者；稽核記來源 IP；主系統「0 位」紅字 |
| 離線期間撤銷不了 | 有效期＋30 天停用；主系統標「有待同步的撤銷」 |
| CIDR 設錯鎖自己 | 隱含 loopback；交集非空驗證；CLI reset；手冊 SSH port-forward |
| 重啟中 drain 到一半重送 | idempotency_key＋主系統終態冪等 |
| outbox 塞爆 | 10,000 上限、丟 audit → local_run、task_result 不丟 |
| 除錯被客戶 IDS 當攻擊 | D20 總量門檻＋確認框；單目標序列；間隔 1 秒；手冊「先跟網管講」 |
| 兩份 install.sh 超標 | 新邏輯進 `lib/`，`case` 只加行；shellcheck |
| `RemoteAgentManage.vue` 766 | 四個新 UI 全子元件，只加欄與一行 |
| `_run_one` 抽 sink 動到派工核心 | 既有 tests 綠為硬條件；CloudSink 行為逐字搬 |
| 登記型檔撞批 | 先佔位給 owner 卡；cards.py 斷言 |
| 雛形與 FE 用了左側粗邊框 | 交稿前 grep 邊框樣式為 0（CLAUDE.md UI 禁令） |
| jedi-common 版本線（Agent 0.0.31 vs jedi-iam ≥1.3.4） | jedi-ldap 零相依，Agent 不裝 jedi-iam |
| 心跳 `ldap_config` 帶服務帳密 | 走 mTLS；Agent 收到即用自己 key 加密保存；不落明文、不進 log |
| AD 帳號撞名（不同網域同帳號、或同一個 AD 帳號綁給兩人） | 反查命中不是恰好一人就當找不到；輸入帶網域才比網域 |
| 主系統改了角色，離線名單還是舊權限 | D23：範圍每次心跳即時算，能力點拿光就標撤銷 |
| 下載弱點資料失敗後要等 30 分鐘才重試 | 主控台「立即檢查更新」清冷卻並喚醒心跳；10 秒去抖、下載中不重複觸發 |
| 無人值守升級沒設密碼 | 紅字三處；不擋升級 |
| 判讀卡講錯 | 確定性 fixture 表；報告註明「規則判讀非 AI」 |

---

## 10. 手測後追加的功能 {#additions nav="手測追加"}

v1 的 41 張卡合回後，決策者手測再追加 29 張（含首腦直接修的一批）。決策層的變動已併入 §3（D2、D17、D22、D23）與 §5；這裡列每張卡做了什麼。

**登入與身分**

| 卡 | 內容 | repo |
|---|---|---|
| CM-3170／3171／3172 | 離線登入天數由租戶設定（預設 7、1～30），心跳下發；Agent 套用並可本機關閉；主系統 Agent 管理頁設定天數、顯示本機管理員最近登入 | BE＋套件／Agent／FE |
| CM-3173 | AD 登入只問 AD、不受離線天數限制；已登入的離線操作者每個請求重驗 | Agent |
| CM-3174／3175 | 本機管理員密碼設定時間回報主系統、180 天提醒；本機管理員登入時通知租戶管理員 | Agent／BE＋套件＋FE |
| CM-3176 | 主系統與主控台閒置 15 分鐘提醒、30 分鐘登出（共用元件） | FE |
| CM-3177 | 離線密碼規則同主系統；登入頁 AD 與離線密碼分開送 | FE |
| CM-3178 | 安裝時主系統位址只收 https（源碼模式明確設定才放 http） | Agent |
| CM-3179 | Agent 名稱在主控台與主系統兩邊可改，以修改時間較新者為準 | 四 repo |
| CM-3183／3184／3192 | 名單下發 `ad_account`；AD 分頁填 AD 帳號、反查名單；失敗原因分三種記 log | BE＋套件／Agent |
| CM-3189 | 離線操作者對話框顯示綁定的 AD 帳號；登入頁提示 | FE |
| CM-3193／3194 | D23：離線權限跟主系統同一份；對話框拿掉範圍勾選，改顯示主系統權限 | BE＋套件／FE |

**主控台**

| 卡 | 內容 | repo |
|---|---|---|
| CM-3185／3186 | 網址照主系統用一般路徑（`/console/runs`），重新整理停原頁，不存在的頁 404 | Agent／FE |
| CM-3187／3188 | 報告線上預覽（`/preview` 子路由），沿用主系統預覽對話框 | Agent／FE |
| CM-3190／3191 | D22：儲存位置只剩內建 SeaweedFS | Agent／FE |
| CM-3195／3196 | 總覽「弱點資料」卡「立即檢查更新」：清失敗冷卻、喚醒心跳立刻跳一次 | Agent／FE |

**主系統**

| 卡 | 內容 | repo |
|---|---|---|
| CM-3180／3182 | 「Agent 掃描執行紀錄」併入第三方元件的 Agent 手動掃描，選單搬到「合規與稽核」；版面與主控台一致、失敗可看原因 | BE＋套件／FE |
| CM-3181 | 背景工作排程去重 | BE |

**首腦直接修（未開卡）**：主控台贅述刪除、執行紀錄頁重做（今日／自訂區間、⋮ 操作、重新執行、失敗原因可複製）、錯誤訊息遮罩、麵包屑、版本號加 v；主系統 Agent 管理頁「連線至 Agent」、離線操作者對話框改版、角色權限頁補漏翻譯、主系統「檢測工具」頁先隱藏。施工細節在各 commit 與 CM-3177 卡尾。

---

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

| 你想知道 | 看 |
|---|---|
| 老闆說什麼、五種登入比較、§13 死結與既有機制實查、§14 全量工作清單 | [討論稿](discussion.md) |
| 畫面長什麼樣、流程怎麼走 | [互動雛形](prototype/index.html)（「跟著走」） |
| 每個決策為什麼 | 本文件 §3 |
| v1 之後手測追加了什麼 | 本文件 §10 |
| 開工要改哪些檔、哪張卡、哪一批 | 本文件 §4、§6；底表 [`handoff/cards.py`](handoff/cards.py) |

