---
title: "檢測 Agent 原生安裝包 — 設計文件 (FR-066)"
brand: "Guidant AI · **FR-066** 檢測 Agent 原生安裝包"
eyebrow: "FR-066 · Agent Native Installer — 設計文件 · 2026-08-18"
h1: "檢測 Agent 原生安裝包 設計定案"
lede: "把檢測 Agent（evidence-agent）從 docker 三容器出貨，改為 **Nuitka 編譯＋外部工具全包的一顆 tarball**，`install.sh` 一鍵安裝為 Linux systemd 服務。本文件承接同資料夾需求討論稿的 D1–D9 定案，轉為正式設計：build 管線、tarball 打包與工具 bundle、install.sh 與 systemd、驗收與文件，共 4 子需求 × 11 子任務。"
chips: [
  {text: "設計定案 2026-08-18・待實作", kind: ok},
  {text: "開放查證項 Q1–Q4（實作期）", kind: warn},
  {text: "前作：FR-063 Nuitka / FR-064 簽章 / FR-065 installer", kind: accent},
  {text: "標的 repo：evidence-agent", kind: plain},
  {text: "拆分：4 子需求 × 11 子任務", kind: plain}
]
footer: "FR-066 · 檢測 Agent 原生安裝包 — 設計文件 · 2026-08-18 · 內容真相源：本資料夾 discussion.md（D1–D9 定案）· 標的 repo：evidence-agent（v0.2.27）"
---

> 狀態：**設計定案・待實作**（D1–D9 已拍板；Q1–Q4 為實作期查證項）｜建立日期：2026-08-18
> 前作：[FR-063 Nuitka 落地版打包 design](../FR-063-2608-nuitka-packaging/design.md)（build 管線／私房 wheel／resource_path 慣例）／[FR-064 防竄改偵測 design](../FR-064-2608-tamper-detection/design.md)（manifest 簽章泛用層）／[FR-065 落地版 Installer design](../FR-065-2608-onprem-installer/design.md)（install.sh 七步結構／維運子命令）
> 討論稿（唯一內容真相源，D1–D9 拍板脈絡與被排除方案全文）：[`discussion.html`](./discussion.html)

## 給 PM 的三分鐘版 {#pm nav="給 PM"}

> 這節不需要任何技術背景就讀得懂。工程細節從下一節起。

**這案在做什麼**：檢測 Agent 是我們派駐在客戶機房、替我們去做掃描的小程式。它原本包在
docker 容器裡出貨（容器＝一個跟外界隔開的獨立小房間）。但 Agent 的工作就是**量測它所在
的那台機器**，隔著這層隔離會讀不到硬體身分、或每次重啟讀到的值都不一樣——**這件事已經
真的出過事**（系統認不得同一台機器）。本案把它改成直接裝進機器裡，就像同業的掃描 Agent
（Nessus、Qualys、Wazuh）一貫的做法。

**為什麼值得做**（三個實際好處）：

1. **身分不會漂**——機器識別碼直讀主機，不再靠外掛腳本從外面抄值硬撐
2. **客戶不必先會 docker**——解開一顆包、跑安裝腳本、回答三個問題就好
3. **封閉機房也能裝**——掃描工具全部隨包附上，安裝過程不必連外網下載任何東西

順帶做掉的：客戶端少一個資料庫服務、少一個網路轉發服務要維運（都省成 Agent 自己承接），
以及**防偽章**——出貨前逐個檔案蓋章，有人動過包裡任何檔案，Agent 就拒絕啟動。

**影響哪些畫面／操作**：**產品畫面完全不變**。這是交付形態的改造，客戶端的變化是「裝的
方式不同了」；雲端後台看到的 Agent 清單、派工、報告全部照舊。唯一要注意的是：改了機器
身分的算法，**既有已開通的 Agent 指紋會變、需要重新開通一次**（決策 D11，當時存量最少、
最便宜）。

**拍板了什麼**（決策 D1–D19 的白話版，完整理由見 §3）：

| 題目 | 定案 |
|------|------|
| 程式怎麼給 | 編譯成機器碼，客戶看不到我們的原始碼 |
| 工具怎麼帶 | 全部塞進同一顆包，不做「核心包＋工具包」分層——分開給就有漏帶的風險，封閉機房現場出事沒網路可補救 |
| 本地資料怎麼存 | 從「另起一個資料庫服務」縮成單一檔案（它只服務一張表，養一個服務不值得） |
| 防竄改要不要做 | 要——Agent 以最高權限跑在客戶機器上、還握著加密憑證，是整條信任鏈最容易被摸的一端 |
| 怎麼升級 | 新版裝到旁邊、切換指向即完成；出錯切回去就是回滾 |
| Windows 版 | **凍結**（D19，PM 拍板）。兩條路線實測都判死（Docker Desktop 使用者一登出整組停掉；另一條方案閒置一分鐘就被系統凍結）。現階段要掃 Windows 主機的解法是：Linux Agent 走遠端連線去掃，這是既有功能 |
| 出貨哪一版 | **docker compose 版**（D16）。原生安裝版開發完成但暫不出貨，等真的出現封閉網路／不能裝 docker 的客戶再解凍 |

**現在到哪了**：母卡 CM-1284 已於 2026-08-23 收案，compose 版已隨產品出貨。
各子需求「怎麼親手驗它做完了」見下一節的分工概述表。

---

## 變更紀錄

| 日期 | 變更 | 對應 |
|------|------|------|
| 2026-08-18 | 初版設計定案：D1–D9 定案轉入、現況接入點盤點、4 子需求 × 11 子任務拆分、Q1–Q4 歸屬標注、端到端驗收 | FR-066 |
| 2026-08-18 | 補 Windows 可行性初判入 D8（附錄：分層難度表＋工具鏈盤點＋SmartScreen 風險，T-4.3 起點素材） | FR-066 D8 |
| 2026-08-20 | §2 分工概述升級：加「完成怎麼判定（決策者檢查法）」欄＋非工程讀者導讀（決策者反饋：原版太工程師、不知道每階段怎麼檢查） | FR-066 |
| 2026-08-20 | §5.2.3 落地調整：manifest 由單份改**雙層**（agent 層給啟動 gate、bundle 層給交付完整性），圖 2 同步更新；T-2.3（CM-1294）實作時發現單份與啟動 gate 掃描錨點衝突 | FR-066.2 D6 |
| 2026-08-20 | 新增 FR-066.5 EL（Rocky/RHEL 9）支援：D10 決策＋T-5.1~5.3 拆分（方案 A 低 glibc 容器編譯，單一 tarball 不變） | FR-066.5 |
| 2026-08-21 | 新增 FR-066.6 docker compose 形態＋SeaweedFS 內建儲存：D11 指紋二源化（全域）／D12 雙形態矩陣／D13 沿用 FR-039 既有 Agent 儲存鏈；T-6.1~6.4 拆分。另記 FR-066.5 PM 決議暫緩 | FR-066.6 |
| 2026-08-21 | T-6.1 落地：D11 補「撞號守門的豁免條件＝憑 uid 認出」與 `--upgrade` 須更新維運指令副本兩則實作教訓（皆為實測抓到的錯，見 D11 註） | FR-066.6 D11 |
| 2026-08-21 | FR-066.6 增 T-6.5 configure-storage 子命令（D14 裁示：本地 CLI 先做、web 下發另開評估卡給 PM） | FR-066.6 |
| 2026-08-22 | FR-066.6 增 T-6.6 compose 版互動安裝程式（D15：交付形態對齊主產品 installer 模式——問答引導，客戶不碰 docker 指令；T-6.3 產物為其積木） | FR-066.6 |
| 2026-08-22 | 增 T-6.7 恢復系統內建儲存（出廠快照＋configure-storage 選項；主產品對應功能另卡處理） | FR-066.6 |
| 2026-08-22 | **D16 出貨形態收斂**：原生 tarball 暫不出貨、現階段一律 compose 版（D12 出貨面凍結，開發成果保留）；T-4.1 隨之暫緩、T-4.2 範圍改 compose 為主。另收 CM-1335 裁示：compose 交付包廢除 install/upgrade 兩包制、收斂一顆通吃 | FR-066 D16 |
| 2026-08-22 | 新增 FR-066.7 Windows（Docker Desktop）支援：D17 裁示（docker 路線先行、原生版不討論；Docker Desktop 承載；install.ps1＋薄指引；setup.exe 記 PM 反饋觸發升級項）；T-7.1~7.3 拆分。另記 D16 補充：手冊（T-4.2）解凍條件＝FR-065 產品本體 Windows 與本案 .7 皆收口後兩本一起寫 | FR-066.7 |
| 2026-08-23 | 新增 FR-066.8 原生 Windows 版：D18 裁示（Docker Desktop 判死於出貨〔T-7.1 實測登出全滅 P0〕、B〔WSL+engine 常駐〕否決、原生服務為 Windows 唯一出貨路線；code signing 階段策略＝現階段不買憑證走客戶 IT 白名單、第一個正式 Windows 客戶在望時買 OV、救火買 EV）；T-8.1~8.5 拆分。FR-066.7 收斂為 demo 限定（T-7.2/T-7.3 降級，見 D18 註） | FR-066.8 |
| 2026-08-23 | **D19 Windows 線全面凍結**（PM 拍板）：先以 Linux 為主，FR-065/FR-066 不再探討 Windows 安裝，客戶真有需求再開卡。T-7.4（CM-1346）B 案生死驗證判死收錄（WSL VM 閒置 60 秒凍結 userspace、四種挽救全滅、外部 watchdog=每分鐘重開機非常駐）；.7/.8 全卡樹暫緩、探路資產保留。T-4.2 手冊解凍改為 Linux 版立即撰寫（主產品對齊卡 CM-1347＋agent 手冊 CM-1299） | FR-066 D19 |

---

## 1. 需求背景與端到端流程

### 1.1 背景：容器裡的 Agent 量不到宿主

檢測 Agent（evidence-agent，獨立 repo `~/Projects/Billows/Audit-Manager/evidence-agent`，v0.2.27，Flask + dependency-injector）目前唯一交付形式是 docker image（`python:3.11-slim`）＋三容器 compose（`agent` 本體＋`agent-db` postgres:16＋`nginx` mTLS sidecar），出貨走 `docker save` / `docker load`。

但 agent 的本職是**量測宿主機**——機器指紋要讀 `/sys/class/dmi/id/product_uuid`、`/etc/machine-id`、主網卡 MAC，這些在容器裡**讀不到或會漂移**（已發生 Debian base image `/etc/machine-id` 空檔退回隨機 MAC 的指紋漂移事故），現行靠 `deploy/collect-host-id.sh` workaround 從宿主抽值塞進容器硬撐。業界同類產品（Nessus / Qualys / Wazuh / Elastic / Datadog Agent）**全部以原生套件（deb/rpm/tarball）＋ systemd 服務交付**——貼宿主量測的產品，原生安裝就是行規。

FR-065（產品落地 installer）期間決策者裁定：**agent 安裝包另起新 FR、非 docker**，即本案。

### 1.2 本案目標

- Agent 做成 **Nuitka standalone 編譯**（把 Python 程式編成機器碼、原始碼不可見）的 tarball 安裝包
- `install.sh` 一鍵安裝為 **Linux systemd 服務**
- 外部檢測工具**全包在同一顆 tarball**，離線（封閉網路）完全自足
- **Windows 不實作**，只出評估文件與前提清單

### 1.3 前作可複用資產

| 前作 | 可直接複用 |
|------|-----------|
| **FR-063** Nuitka 落地版打包 | `scripts/build/build_release.sh` build 管線、dependency-injector 私房 wheel `rebuild_wheels.sh`、`resource_path()` 慣例、DI 靜態清單處理手法 |
| **FR-064** 防竄改偵測 | 簽章泛用層 `sign_payload` / `verify_payload`（帶 `type` 欄）、`gen_integrity_manifest.py`、`sign_manifest.sh`、License Center 簽發 |
| **FR-065** 產品落地 installer | `install.sh` 七步結構、`--check-only`、安裝落檔慣例、digest 驗證、維運子命令（status / logs / uninstall / fingerprint） |

### 1.4 端到端安裝時序

客戶側從解 tarball 到心跳上線，mTLS（雙向 TLS 認證）全自動、客戶零 openssl 操作：

```{.mermaid cap="圖 1 — 客戶側一鍵安裝到心跳上線（D4/D5 全自動 mTLS）"}
%%{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 OP as 客戶操作者
    participant IS as install.sh
    participant SD as systemd
    participant AG as guidant-agent 服務
    participant CL as 雲端產品（FR-065 已含 PKI）
    OP->>IS: 解 tarball、執行 install.sh
    IS->>IS: 七步：前置檢查→驗 manifest 簽章→落檔 versions/&lt;ver&gt;→裝 bundle 工具→建 symlink→裝 systemd unit→啟動
    IS->>OP: 互動問答三題（雲端位址／註冊 token／本機對外位址）
    IS->>IS: 生成 /etc/guidant-agent/agent.env
    IS->>SD: systemctl enable --now guidant-agent
    SD->>AG: 啟動（啟動時驗 manifest 簽章）
    AG->>AG: 直讀宿主指紋（product_uuid / machine-id / MAC）
    AG->>CL: enroll：產 keypair＋CSR（SAN=本機位址）＋token 註冊
    CL-->>AG: CA 簽回 client 憑證＋8443 server 憑證
    AG->>AG: 憑證存 cert_dir，gunicorn/ssl 自起 8443 資料面
    loop 心跳
        AG->>CL: mTLS 心跳（404 → 自動重註冊）
    end
```

---

## 2. 分工概述（30 秒版）

> 本節寫給**決策者與非工程讀者**：每個大項在做什麼、產出是什麼、以及**你要怎麼親手判定它完成了**。工程細節從 §3 起。

**整案一句話**：把檢測 Agent 從「docker 三容器出貨」改成「Nuitka 編譯＋外部工具全包的一顆 tarball，`install.sh` 一鍵裝成 Linux systemd 服務」——宿主指紋直讀、SQLite 取代 PG、agent 自起 TLS 取代 nginx sidecar、build 期 manifest 簽章＋啟動驗章。

| 子需求 | 做什麼（白話） | 產出 | 完成怎麼判定（決策者檢查法） |
|--------|---------------|------|------------------------------|
| **FR-066.1** Nuitka build 管線與程式改造 | 把 agent 的 Python 程式編成機器碼，順手把「只有在 docker 裡才成立」的地方改掉：資料庫從要另起一個 PG 容器改成單檔 SQLite、版號不再讀 pyproject.toml、檔案定位改編譯相容寫法、指紋直接讀宿主 | agent 版 `build_release.sh`＋可跑的 Nuitka standalone 產物 | 在 build 機（188）跑 `smoke_agent_release.sh` 看 **5/5 全綠**——產物翻不到 `.py` 原始碼、服務起得來、健康檢查有回應。✅ 已達成（2026-08-20，決策者親跑） |
| **FR-066.2** tarball 打包與工具 bundle | 把掃描工具（CINC / sonar-scanner / LibreOffice / 中文字型…）全部塞進同一個壓縮包，客戶不用連網也裝得起來；出貨前逐檔算 hash 簽章，啟動時驗章防竄改 | `guidant-agent-<ver>.tar.gz`（含 manifest.sig）＋`build_bundle` 打包腳本 | 一鍵產出 `guidant-agent-<ver>.tar.gz`＋校驗碼、解包目錄結構與 §5.2.3 圖一致；且**親手改包內任一個檔案 → agent 拒絕啟動**（防偽章生效）、斷網容器內工具裝得起 |
| **FR-066.3** install.sh 與 systemd | 客戶拿到 tarball 後的一鍵安裝腳本：問三個問題就裝成開機自起的系統服務，升級換版切個 symlink、出錯切回去就回滾，還附 status / logs / uninstall 等維運子命令 | `install.sh`（安裝／升級／回滾／維運）＋systemd unit | 找一台沒裝過的乾淨 Linux：解包 → `sudo ./install.sh` 回答三題 → `systemctl status guidant-agent` 顯示 active；升一版再退回，服務都活著 |
| **FR-066.4** 驗收與文件 | 找一台乾淨 Linux 機從頭裝一遍、斷網環境也裝一遍，確認真的能跑檢測任務；寫客戶安裝手冊；Windows 版可不可行寫成評估文件 | 真機驗收紀錄＋客戶手冊＋Windows 評估文件 | 斷網真機從安裝到跑完一個檢測任務、報告回到雲端**全程通**；手冊給沒參與過的人照著做能裝成功；Windows 評估文件能支撐「做／不做」的商務判斷 |
| **FR-066.5** EL（Rocky/RHEL 9）生態支援（暫緩） | 讓同一顆安裝包也能在政府金融常用的 Rocky Linux / RHEL 9 上裝起來、跑得動：agent 編譯搬進低版本函式庫容器（debian:11）做，一份產物通吃三家族；安裝腳本認得 EL 的 SELinux／firewalld 並給指引（只檢測＋輸出指令，不自動改客戶機）；最後在真的 Rocky 9 機器驗一輪 | 容器化編譯的 build 管線＋EL 適配的 install.sh＋EL9 真機驗收紀錄（tarball 仍是同一顆，不分家） | 找一台 Rocky Linux 9 機器（SELinux 開啟狀態）照同一份 tarball 裝：`sudo ./install.sh` 裝完 `systemctl status guidant-agent` 顯示 active、雲端後台看得到 agent 上線、派一單 CINC 檢測任務跑完報告回到雲端 |
| **FR-066.6** docker compose 形態＋SeaweedFS 內建儲存 | 出一版 docker compose 形態的 agent 安裝包：同一顆編譯產物裝進容器、SeaweedFS 證據儲存服務一起出貨，agent 收到的證據預設存進 SeaweedFS；客戶自有 SeaweedFS/MinIO 也能設定指過去；機器指紋改用兩個掛載可得的來源 | compose 交付包＋agent seaweedfs 儲存支援＋指紋二源化＋configure-storage 設定命令＋compose 版互動安裝程式 | 找一台有 docker 的 Linux 機：`docker compose up -d` 起來後雲端後台看得到 agent 上線；到儲存設備設定頁選「使用 Agent 服務」存檔，上傳一個證據檔，能在 SeaweedFS 的管理網頁看到那個檔、產品端能正常預覽下載 |
| **FR-066.7** Windows（Docker Desktop）支援 | 讓 Windows 客戶也能裝檢測 Agent——不重編程式，客戶在 Windows 上裝 Docker Desktop（圖形介面的容器執行軟體），拿同一顆 compose 安裝包，用 Windows 版互動安裝程式（install.ps1，PowerShell 腳本）問答三題裝起來；網路連入、機器指紋在 Windows 環境的差異由安裝程式與指引文件處理 | install.ps1（Windows 版互動安裝程式）＋Windows 安裝前置指引文件＋160 真機驗收紀錄 | 找一台裝好 Docker Desktop 的 Windows 機（192.168.50.160）：解開同一顆安裝包 → 跑 install.ps1 回答三題 → Docker Desktop 看到兩個容器綠燈 → 雲端後台看得到 agent 上線 → 派一單檢測任務跑完報告回雲端；重置 Docker Desktop 後照指引重新開通能接回 |
| **FR-066.8** 原生 Windows 版 | Windows 正式出貨版——agent 與內建儲存（SeaweedFS）編譯/打包成 Windows 原生程式，裝成兩個開機自起的 Windows 系統服務，不經 docker；問答式安裝程式（install.ps1）、升級回滾、防竄改簽章對齊 Linux 版。機器指紋改讀 Windows 本機識別碼（重灌才變，不受容器/登出影響） | Windows build 管線＋agent.exe/weed.exe 雙服務＋install.ps1＋乾淨機驗收紀錄 | 找一台沒裝過的 Windows 機：跑 install.ps1 回答三題 → 服務清單看到兩個 Guidant 服務「執行中」→ **登出、重開機後服務自動回來、雲端 agent 仍在線**（這是 docker 版做不到的關鍵差異）→ 派一單檢測任務跑完報告回雲端 → 改包內任一檔案後服務拒絕啟動 |

**依賴一句**：.1 → .2 → .3 **串行**（build 產物是打包的輸入、打包產物是安裝的輸入），.4 收口；與 FR-063/064/065 不同，本案各階段產物互為輸入，無平行空間。.6 依賴 .1 編譯產物，與 .4 收口平行；.5 暫緩最後做。

---

## 3. 決策定案（D1–D9）

九項全部已由決策者拍板；每項含定案理由與被排除方案（全文脈絡見討論稿 §2）。

| # | 決策 | 定案與理由 | 被排除方案 |
|---|------|-----------|-----------|
| **D1** | 打包形式 | **Nuitka standalone 編譯 → tarball → systemd 服務**。standalone 同為機器碼編譯，自家 code 不可見，保護強度與 FR-063 BE 打包相同；第三方套件以原始 `.py` 附帶（EXCLUDE 名單機制），皆為公開開源套件、不構成洩漏 | **onefile 模式**：①啟動先解壓暫存目錄有延遲；②FR-064 逐檔 manifest 驗章在單一自解壓檔上難做；③外部工具反正要目錄形式，onefile 省不了目錄 |
| **D2** | 本地 DB | **agent-db（postgres:16）退役，換 SQLite**。現況 agent-db 只服務 jedi-file-upload 的 `upload_files` 一張表（`core/app_factory.py:37` 註解明寫；`create(checkfirst=True)` 自建、無 migration 體系）；單表、單進程寫入、無並發、無 RLS，正是 SQLite 甜蜜點；`init_db()` 吃 SQLAlchemy URL，換 `sqlite:///` 即可。**前置守門＝Q2**（PG 方言掃描） | ①要求宿主裝 PG：加重客戶前置需求，違背一鍵安裝；②bundle PG 進 tarball：包更肥、多一個服務要維運（起停／備份／密碼），為一張表不值得 |
| **D3** | 外部工具交付 | **全包一顆 tarball，install.sh 自動安裝，離線完全自足**。封閉網路情境（FR-059.3 已驗證為既定支援情境）下，全包才不存在漏帶／版本錯配的現場事故；體積 ~1GB+ 已明示接受。bundle 清單見 §5.2 | 方案 B「核心包＋工具附加包分層」：多檔交付有漏帶風險，封閉網路現場出事無網路可補救；方案 C「宿主前置需求清單」：把安裝品質丟給客戶，且客戶自裝 InSpec 會踩 CINC 授權紅線 |
| **D4** | mTLS 承接 | **agent 以 gunicorn/ssl 自行承接 8443 資料面 TLS，nginx sidecar 退役**。mTLS 驗證邏輯本來就在 agent code 內（`core/data_plane_auth.py`），nginx 只是轉發層，砍掉不損失任何安全能力；8443 server 憑證由 enrollment（agent 向雲端自我註冊）時 CA 簽出（CSR SAN＝base_url host），客戶不需自備憑證 | —（保留 nginx 意味多一個要 bundle 與維運的服務，無對價） |
| **D5** | 設定模型 | **`/etc/guidant-agent/agent.env` 作為 systemd EnvironmentFile**。沿用現有全環境變數設計（`config/config.py`，agent 無設定檔）——零 code 改動；`install.sh` 互動問答三題（雲端位址／註冊 token／本機對外位址）生成該檔。客戶側 mTLS 全自動：自我註冊流程已存在（`core/enroll.py`：產 keypair＋CSR → token 註冊 → 憑證簽回存 cert_dir → 心跳走 mTLS → 404 自動重註冊），客戶零 openssl 操作；雲端側 PKI 由 FR-065 產品 install.sh 已涵蓋（install.sh:1936 `AGENT_CERT_DIR`） | —（引入新設定檔格式＝無謂 code 改動） |
| **D6** | 簽章／防竄改 | **本 FR 一起做，瘦身版：build 期 manifest（逐檔 hash 清單）簽章＋啟動時驗章**。複用 FR-064 泛用層：簽章加一個 `agent_manifest` type、`gen_integrity_manifest.py` 改常數、`sign_manifest.sh` 近零改，複用成本僅 1–2 張小卡。值得做的理由：agent 以 root 服務跑在客戶機、握 mTLS 憑證，是整條信任鏈最容易被摸的一端 | FR-064 runtime 全家桶（抽查／lockdown／unlock token）：對 agent 過重，不做 |
| **D7** | 升級／回滾 | **`/opt/guidant-agent/versions/<ver>/` ＋ `current` symlink 切換**。升級＝解新版本目錄、切 symlink、重啟；回滾＝切回舊 symlink。SQLite 檔與 mTLS 憑證放資料目錄、不隨版本目錄走；agent 無 DB migration 負擔（D2 單表自建），不需 FR-065 那套 pg_dump 備份模型，升級模型比產品端輕得多 | —（複製 FR-065 備份模型＝為不存在的 migration 負擔付維運成本） |
| **D8** | Windows | **只出評估文件＋前提清單，不實作不承諾**（比照 FR-065 D9；FR-065 對應工作 T-4.3 於 2026-08-18 裁定跳過、評估文件未產出——agent 版評估文件從零寫，不假設有前稿可抄）。評估三題：①指紋來源（WMI / registry）②服務形式（Windows Service / NSSM）③工具鏈可用性（CINC / sonar-scanner 等在 Windows 的形態）。脫離 docker 後 Docker Desktop 商業授權疑慮消失；但 machine-id 等指紋問題在 Windows 依然存在，是評估重點 | 實作 Windows 版：需求未定案，不投 |
| **D9** | 指紋 | **只做 Linux 原生直讀，不預抽 OS 抽象層**。直讀 `/sys/class/dmi/id/product_uuid`、`/etc/machine-id`、主網卡 MAC——`core/fingerprint.py` 已有伏筆註記（「原生封裝後直接讀系統路徑，模型不變」）；`deploy/collect-host-id.sh` workaround 隨之廢除 | 先抽 OS 抽象層：Windows 需求未定案（D8 只出評估文件），為未定需求預先抽象是過度設計；等 Windows 真要做再抽，重構成本可控 |
| **D10** | EL（Rocky/RHEL）支援方式 | **方案 A：agent 編譯搬進 debian:11（glibc 2.31）容器**，與 syslibs 採集同一基準邏輯——低 glibc 編譯向上相容，一份產物通吃 Debian 11+/Ubuntu 20.04+/EL9；工具 bundle 層已於 T-2.1 三家族實測通過，故只補編譯層＋安裝腳本 EL 適配＋EL 真機驗收；firewalld/SELinux 只檢測與指引、不自動改 | 方案 B 雙 build lane 出兩顆 tarball：QA/出貨負擔雙倍、違背 D3 單包精神；方案 C 宣告不支援 EL：Rocky/RHEL 是政府金融常態需求，把需求推走 |
| **D11** | 指紋二源化（全域） | **device_uuid 由三源 `sha256(product_uuid\|machine-id\|mac)` 改二源 `sha256(product_uuid\|machine-id)`，全域生效（原生版一起改）**。docker 形態下 MAC 量不到宿主（bridge 網路虛擬網卡），machine-id 與 product_uuid 可唯讀掛載解決；只改 docker 版會讓同機兩種裝法算出兩個身分，故全域統一。代價：既有已 enroll agent（123 真機＋DEV 測試筆）指紋全變需重 enroll——現在存量最小最便宜。VM 模板複製撞號風險（machine-id 整批相同）改由 **BE enroll 端撞號偵測守門**（同指紋再註冊→拒絕並提示）承接，冗餘從演算法搬到檢核 | ①僅 docker 版二源：同機雙身分；②保留三源＋collect-host-id.sh 人工抄 MAC：已被 D9 廢除的 workaround，會過期、靜默錯 |

> **D11 實作註（T-6.1 落地，2026-08-21）**——兩則都是實測才現形、且都「看起來已經做對了」：
>
> **① 撞號守門的豁免條件必須是「憑 agent_uid 認出來的」，不是「resolve 到同一列」。**
> BE `_resolve_agent` 在 agent 沒帶 uid 時會退用指紋去重，於是**複製出來的機器必然
> resolve 到被撞的那一列**、uid 自然相等——守門若比對 `resolved.uid` 就會判定「就是它
> 自己」而放行。首版即如此，實測（DEV 真打 register）回 HTTP 200 且原機那一列的
> base_url 被複製品覆寫，正是本守門要防的情境。指紋 fallback 這條路徑就是複製品會走
> 的，拿它當「同一台」的證據等於把守門前提交給攻擊面本身。已改為 `_resolve_agent`
> 回傳 `(entity, matched_by_uid)`，只豁免前者。
>
> **② 指紋演算法一改，`--upgrade` 必須一併更新 `guidant-agent-ctl`。** 該檔是安裝當下
> 複製出去的獨立腳本（讓客戶刪掉 2GB 安裝包後仍能用 status／logs），升級只換 /opt 下的
> 程式、不會動它。其 `fingerprint` 子命令是 agent 演算法的 shell **複刻**，版本不一致
> 就印出另一個值——而那正是客戶要抄去雲端開通的那串。123 真機實撞：服務報二源值、舊
> ctl 報三源值。原有的「與執行中服務對帳」機制有出聲（正是這樣抓到的），但它在服務沒
> 跑時（開通前，恰是 fingerprint 的主要使用時機）無從對帳，故不能當解法。
>
> 共同教訓：**凡是「同一套邏輯存在兩份實作」（Python↔shell 複刻、演算法↔守門豁免），
> 改一邊就要指名另一邊，且要對真端點／真機各跑一次**——單元層 fixture 會繞過真實
> 解析路徑（本例即因 fixture 直餵 `resolved=None` 而漏掉 ①）。
| **D12** | 雙形態支援矩陣 | **compose 版＝Linux 有 docker 環境首選（含 SeaweedFS 全套）；原生 tarball＝封閉網路／無 docker／未來 Windows 路線**。兩版**共用同一顆 Nuitka 產物與同一套簽章**，build_all 加 compose 打包出口，不開第二條產線 | —（雙產線＝雙倍 QA/出貨負擔，違背單一產物精神） |
| **D13** | 產品端「使用 Agent 服務」 | **＝FR-039 既有功能，BE/FE 零新功能**。雲端儲存設備設定頁本就有該選項（storage config 指 agent base_url/agent_uid → RemoteAgentAdapter → agent blob API → agent 依自身 STORAGE_TYPE 落地）。本案只做 agent 端：內建 SeaweedFS 預設接上＋自有後端 env 設定＋驗收既有鏈在新編譯 agent 上仍通 | —（在 BE/FE 另做新儲存選項＝重複造輪子，鏈路早已存在） |
| **D14** | 自有儲存後端的設定介面 | **本地 CLI 先做（T-6.5）**：agent 加 `configure-storage` 維運子命令——secret 不離機、連通驗證當場做、量級小且是 web 下發的必要兜底（下發失敗／無雲端連線時的現場設定路徑）。**web 設定下發開獨立評估卡給 PM 排優先度**（前置資安題：客戶儲存憑證入雲端 DB 是否可接受） | 直接做 web 下發（三端成本＋secret 治理題未決，不該擋住立即可用的 CLI） |
| **D15** | compose 版交付形態 | **配互動安裝程式（T-6.6）**，照主產品 FR-065 installer 同款模式：環境檢查→載 image→問答→生成設定→起服務→維運子命令。受眾是一線工程師/業務，不假設會 docker；SeaweedFS 憑證自動生成不讓客戶發明密碼 | 手填 .env＋docker compose up（T-6.3 首版形態——僅適合熟 docker 者，不符落地版交付受眾） |
| **D16** | 出貨形態收斂（2026-08-22） | **原生 tarball 版暫不出貨——現階段對客戶出貨一律 compose 版**。原生開發成果（.1~.3 全部產物與腳本）保留不廢，何時解凍等商務需求（封閉網路／無 docker 客戶出現時）。連動：T-4.1（原生乾淨機＋斷網驗收）暫緩；T-4.2 手冊改 compose 為主、原生降附錄。同日另一裁示（CM-1335）：compose 交付包**廢除 install/upgrade 兩包制、收斂一顆通吃**（兩包只差 82MB seaweedfs image，維護兩條路不值；客戶端以「本機已有該 image」為升級放行條件）。⚠️ 遺留：**斷網封閉網路驗證至今無人跑過**（D3 立案根基）——若 compose 版也出給封閉網路客戶，此驗證應轉移到 compose 版做，不隨原生凍結 | 雙形態同時出貨（D12 原案）：原生版每出一版都要 QA 兩形態，現階段無原生需求客戶，QA 成本無對價 |
| **D17** | Windows 支援路線（2026-08-22） | **docker（Docker Desktop）路線先行，原生 Windows 版暫不討論**（T-4.3 評估為據）。承載選 **Docker Desktop**（決策者定案）：安裝體驗是「裝一個 Windows 軟體」，客戶不碰 Linux 層；其商用授權（>250 人或年營收 >$10M 組織需付費訂閱）為**客戶側前置條件**，寫入指引與售前須知。指紋風險接受度（決策者裁示）：WSL/Docker Desktop 重置後指紋漂移＝可接受的維運事件，處理＝log 記錄＋「重置後重新開通」SOP，**不做 host 值注入 workaround**（維持 D9/D11 立場）。安裝程式出 **install.ps1**（PowerShell 互動版，與 D15「不假設客戶會 docker」同一受眾邏輯——也不假設會 Linux 終端）；交付物＝同一顆 onepack 包（一個位元組不改）＋install.ps1＋薄前置指引。**setup.exe 安裝精靈記為 PM 反饋觸發的升級項**（不做的理由：SmartScreen 攔無簽章 exe，消警告需買 OV/EV code signing 憑證——docker 路線本已繞開此成本，不為包裝形式請回來）⚠️ 2026-08-23 D18 修訂：Docker Desktop 降 demo/PoC 限定（T-7.1 實測登出全滅），出貨路線改 FR-066.8 原生版；T-7.2/T-7.3 降級為 demo 支援用途。 | ①WSL2＋docker engine（免授權費但客戶要自管 WSL，違背交付受眾設定）②路線甲「客戶在 WSL 終端跑現有 install.sh」（零開發但要求客戶進 Linux 世界，與 D15 受眾邏輯矛盾）③首版就做 setup.exe（SmartScreen＋憑證成本） |
| **D18** | Windows 出貨路線定案（2026-08-23） | **原生 Windows 服務版（FR-066.8）為 Windows 唯一出貨路線**。依據：T-7.1 實測證實 Docker Desktop 登出後 engine 全滅、SYSTEM 排程與 com.docker.service 兩條無登入自起路徑皆失敗（probe §8，P0）——「常駐」與「綁登入 session」本質矛盾，Docker Desktop 降為 **demo/PoC 限定**（有人值守場景仍可用，T-7.1 已驗全鏈通）。**B 案（WSL2＋docker engine＋排程器常駐）否決**：常駐機制未驗、四層架構（Windows→排程器→WSL→docker→容器）查修成本高、指紋仍是 VM 值、產出對原生版零複用——demo 用不到（A 夠）、出貨不如 C，兩頭不沾。**Code signing 階段策略**（行規=B2B agent 類幾乎全簽，但按階段投入）：現階段（Windows 客戶數 0）不買憑證——manifest 簽章（自有鑰）照做、SmartScreen 靠客戶 IT 白名單（GPO/AppLocker）指引承接，與客群（封閉網路、IT 管控）相容；**第一個正式 Windows 客戶在望即買 OV**（年費數百美金級，含數週申請＋信譽爬升期，不可等簽約後）；大客戶臨時需求買 EV（免爬升）。年費列為 Windows 版固定成本讓售前/PM 知情 | ①Docker Desktop 出貨（登出全滅 P0 無解）②B 案（見上）③現在就買憑證（為未驗證市場預付訂閱）④Azure Trusted Signing（台灣組織不可申請） |
| **D19** | Windows 線凍結（2026-08-23，PM 拍板） | **FR-065/FR-066 不再探討 Windows 安裝，先以 Linux 為主**；客戶真有需求再開卡。決策鏈收束：A（Docker Desktop）＝demo/有人值守可用但出貨不可（T-7.1 登出全滅）；B（WSL+engine 常駐）＝T-7.4 實測判死——WSL VM 閒置約 60 秒凍結整個 distro userspace（SYSTEM 帳戶被硬拒、S4U 可跑但照凍、vmIdleTimeout=-1 無效、distro 內 anchor 連坐被凍、外部 watchdog 只能做到「每分鐘重開機」會腰斬檢測任務），死因＝WSL VM 生命週期需要 Windows 側永不退出的進程握持，而該進程的正規形態就是 Windows 服務——推導出唯一活路 C（原生版）；C 需第二個編譯出口（Windows build 機＋MSVC），PM 裁示此投資等客戶訊號。**凍結時保留資產**：T-4.3 評估、T-7.1/T-7.4 兩份實測紀錄、FR-066.8 卡樹（CM-1340~1345 暫緩）；解凍首步＝build 機佈建。Windows 掃描需求的現行解答＝Linux agent 走 CINC WinRM 遠端掃（既有功能） | ①續投 B 唯一殘存路線（WinSW 握 wsl.exe——工同 C 的 T-8.3、換到六層架構、對 C 零複用）②Hyper-V appliance（等同要求客戶出一台 Linux，非解法）③立即投 C（PM：build 產線投資等客戶訊號） |

### D8 附錄：Windows 可行性初判（2026-08-18 口頭討論）

> **性質**：決策者 2026-08-18 口頭討論確認的**初判**，非定案——定案要等 T-4.3 正式查證。此節作為 T-4.3 評估文件的起點素材。

**核心結論**：本案技術路線（Nuitka＋SQLite＋paramiko＋環境變數設定）每一項都恰好沒堵死 Windows 的路；原生路線在 Windows 反而比 docker 路線順（免 Docker Desktop 企業授權）。粗估 Windows 版工作量約 Linux 版 40–60%（源碼層幾乎共用）。

**分層可行性表**：

| 層 | Windows 對應 | 難度 |
|---|---|---|
| Nuitka 編譯 | 原生支援，編成 .exe＋standalone 目錄（MSVC），同套 build 邏輯換 Windows build 機 | 低 |
| 常駐服務 | Windows Service：NSSM 包裝（業界標準，Wazuh 等同款）或 pywin32 原生 service | 低 |
| SQLite | 完全跨平台（D2 的額外紅利；PG 在 Windows 會很痛） | 零成本 |
| 機器指紋 | WMI `Win32_ComputerSystemProduct.UUID`＋registry `MachineGuid`＋MAC——即 D9 延後的抽象層，屆時再抽 | 中低 |
| mTLS/enrollment | 純 Python（cryptography/httpx），跨平台 | 零改 |
| 安裝腳本 | PowerShell 或 MSI/Inno Setup 重寫，流程邏輯（問三題→佈檔→裝服務）照搬 | 中 |
| 外部工具 bundle | 真正瓶頸所在，見下 | 高 |

**工具鏈逐項**：CINC Auditor 有 Windows msi ✅／sonar-scanner 有 windows-x64 zip（自帶 JRE）✅／git 有 portable 版 ✅／LibreOffice 有 Windows 版（離線佈署形式待查）🟡／xsltproc 無原生、可改用 Python lxml 取代（反而更乾淨）🟡／SSH 掃描走 paramiko 純 Python ✅。工具面無死路，但每項都要重做一次離線化整備。

::: {.callout .warn}
**新識別風險（Linux 版沒有的）**

Windows SmartScreen／Defender 會攔沒有 code signing 憑證的陌生 exe——可能需要購買 Windows code signing 憑證，屬 T-4.3 必查項。
:::

**待 T-4.3 正式查證項**：LibreOffice Windows 離線佈署形式、NSSM vs pywin32 原生 service 取捨、code signing 憑證需求與成本、Windows build 機規格。


---

## 4. 現況接入點盤點

evidence-agent repo 內每個會被本案觸動的元件，逐項列現況與本案動作：

| 元件 | 現況 | 本案動作 |
|------|------|---------|
| `Dockerfile` / `rebuild.sh` | docker image build 入口 | **退役**，換 agent 版 Nuitka build 腳本（.1） |
| `deploy/docker-compose.yml` | agent + agent-db + nginx 三服務編排 | **退役**，換 systemd unit（.3） |
| agent-db（postgres:16） | 只服務 jedi-file-upload `upload_files` 一張表 | **→ SQLite**（D2，.1） |
| nginx mTLS sidecar | 8443 TLS 終結＋轉發 | **退役**，agent 自起 TLS（D4，.3） |
| `deploy/collect-host-id.sh` | 容器讀不到宿主指紋的 workaround | **廢除**（D9，.1） |
| `config/config.py` 版本上報 | 讀 `pyproject.toml` 取版號 | Nuitka 後檔案不存在 → **改 version bake**（build 期把版號寫死進程式，同 FR-063 D6 手法，.1） |
| connector 硬編工具路徑 | `/usr/bin/*` 寫死（image 內裝好） | **配置化或 install.sh 建 symlink** 指向 bundle 工具（.2/.3） |
| `__file__` 定位三處 | `inspec.py:145` / `nmap.py:101` / `config.py:85` | **改 `resource_path()` 慣例**（FR-063 同款，.1） |
| `assets/` 資料檔 | nmap.xsl 等隨 repo | Nuitka `--include-data-dir` 帶入（.1） |
| dependency-injector | C extension，Nuitka 需特製 wheel | **複用 FR-063 私房 wheel**（`rebuild_wheels.sh`，.1） |
| jedi-common / jedi-file-upload | 私服（Nexus）套件 | **編進 binary**；build 機需可達 Nexus（.1） |
| DI wiring | 本來就是硬編靜態清單（無動態掃描） | 免改——FR-063 的 DI 掃描雷不會踩 |
| eventlet / WeasyPrint | agent **沒有**這兩個依賴 | 免處理——FR-063 另兩大雷也不會踩 |
| 心跳排程 | `threading.Thread`（非 APScheduler） | 免改，Nuitka 相容 |

::: {.callout .ok}
**好消息：FR-063 的三大 Nuitka 雷（eventlet monkey patch / WeasyPrint 動態載入 / DI 動態掃描）agent 一個都不會踩**

agent 沒有 eventlet、沒有 WeasyPrint，DI wiring 本來就是硬編靜態清單。`.1` 的風險面遠小於當初 BE 打包（冷 build 亦預期遠低於 BE 的 56 分鐘，見 Q3）。
:::

---

## 5. 詳細設計

### 5.1 FR-066.1　Nuitka build 管線與程式改造

**白話**：這階段做「讓 agent 能被編成機器碼、且編出來在裸機上跑得起來」的所有程式面工作——它是整案起點，後面的打包（.2）與安裝（.3）都以這裡的 build 產物為輸入。docker 時代靠容器環境撐著的假設（旁邊有一個 PG、版號讀得到 pyproject.toml、`__file__` 指得到源碼樹、指紋由 collect-host-id.sh 餵進來）在這一棒全部改掉。

#### 5.1.1 SQLite 化（D2）

- **Q2 前置查證先做**：掃 jedi-file-upload 的 model 定義與所有查詢，確認無 jsonb / `ON CONFLICT` 等 PG 方言；有的話評估改寫成本再定落地細節。
- `init_db()` 的 SQLAlchemy URL 換 `sqlite:///<資料目錄>/agent.db`；`upload_files` 表沿用 `create(checkfirst=True)` 自建，無 migration 體系要搬。
- SQLite 檔落在**資料目錄**（不在版本目錄，配合 D7 升級模型）。
- agent-db 容器、其在 compose 的編排、對應連線環境變數隨之退場。

#### 5.1.2 程式改造

- **version bake**：`config/config.py` 讀 `pyproject.toml` 取版號的邏輯，改為 build 期把版號 bake 進常數（FR-063 D6 同款手法）——Nuitka 產物內沒有 pyproject.toml。
- **`resource_path()` 三處**：`inspec.py:145` / `nmap.py:101` / `config.py:85` 的 `__file__` 定位改 `resource_path()` 慣例（FR-063 同款），配合 `--include-data-dir` 帶入 `assets/`（nmap.xsl 等）。
- **指紋直讀（D9）**：`core/fingerprint.py` 直讀 `/sys/class/dmi/id/product_uuid`、`/etc/machine-id`、主網卡 MAC；廢除 `deploy/collect-host-id.sh` 與其在部署流程的掛點。
- **工具路徑配置化**：connector 內 `/usr/bin/*` 硬編路徑改為可由環境變數／設定覆寫（預設值指向 install.sh 建立的 bundle 工具 symlink，見 .3）。

#### 5.1.3 agent 版 build_release

- 以 FR-063 `scripts/build/build_release.sh` 為藍本開 agent 版：dependency-injector 私房 wheel 直接複用 `rebuild_wheels.sh`；jedi-common / jedi-file-upload 從 Nexus 拉入編進 binary（build 機需可達 Nexus）。
- **Q3**：EXCLUDE_COMPILE_PKGS 名單依 agent 依賴集合（paramiko / python-gvm / zaproxy / cryptography 等）重評，不照抄 FR-063。
- build 完跑 smoke 驗證：產物在無 Python 環境的裸機（或乾淨容器）啟動、心跳邏輯 dry-run、SQLite 建表成功。

### 5.2 FR-066.2　tarball 打包與工具 bundle

**白話**：這階段把「.1 編好的 agent」跟「它工作時要呼叫的所有外部工具」組成一顆客戶拿了就能離線安裝的壓縮包，並在出貨前蓋上防竄改簽章。沒有這一棒，客戶機上要自己裝 CINC、LibreOffice、中文字型……封閉網路根本裝不了；有了它，一顆 tarball 就是完整交付物。

#### 5.2.1 工具 bundle（D3）

| 工具 | 體積 | 備註 |
|------|------|------|
| CINC Auditor（Ruby omnibus，全家桶自足安裝形式） | ~275MB | ⚖️ **授權紅線：絕不可換成官方 InSpec 商業 binary** |
| sonar-scanner（含 JRE） | ~147MB | amd64 專屬 |
| LibreOffice ＋ fonts-noto-cjk | ~400MB+ | 檔案預覽轉 PDF；⚠️ 字型家族名是 `Noto Sans CJK TC` 不是 `Noto Sans TC`；離線化形式＝**Q1**（自帶 deb 組 `dpkg -i` vs portable / AppImage，實測選定，判準：離線可裝、字型能一起落、體積可接受） |
| xsltproc ＋ `assets/nmap.xsl` | 小 | nmap 報告轉換 |
| git | 小 | |

- **不 bundle**：ZAP / OpenVAS（客戶自備 daemon）、OpenSCAP / Nmap 引擎（裝在目標主機、agent SSH 過去跑，不在 agent 機上）。
- **Q4**：查證 image 內 openscap-utils / sshpass 是否實際已無用（OpenSCAP connector 已改 paramiko 直連、不用官方 `oscap-ssh` 腳本）——確認殘留就不搬進 tarball，包更瘦。

#### 5.2.2 manifest 簽章＋啟動驗章（D6）

- 簽章泛用層（FR-064）加 `agent_manifest` type；`gen_integrity_manifest.py` 改常數（掃描根目錄／輸出路徑）產逐檔 SHA-256 manifest；`sign_manifest.sh` 近零改，走 License Center 簽發。
- agent 啟動時以編譯進 binary 的公鑰驗 `manifest.sig`，逐檔比對；驗章失敗拒絕啟動（瘦身版：不做 runtime 抽查／lockdown）。

#### 5.2.3 build_bundle 打包腳本

- 組 tarball 骨架：`agent/`（Nuitka 產物）＋`tools/`（bundle 工具）＋`install.sh`＋`systemd/`＋manifest 簽章（雙層，見下）。
- 產出 `guidant-agent-<ver>.tar.gz` 並附 sha256 檔（交付完整性驗證，FR-065 digest 慣例同款）。
- 實作：evidence-agent `scripts/build/build_agent_bundle.sh`（打包六步）＋`smoke_agent_bundle.sh`（六項交付驗證，含 gate 正反例突變測試）。

**🔄 落地調整（2026-08-20，T-2.3／CM-1294）：manifest 由單份改雙層。** 初版設計是骨架根一份 manifest 涵蓋全部；實作時發現它與啟動 gate 的掃描錨點衝突——gate（T-2.2）讀 **binary 所在目錄**（`agent/`）的 `manifest.sig` 並逐檔比對該樹，而工具樹安裝後散落 `/opt/cinc-auditor`、`/opt/libreoffice25.8`、`/usr/share/fonts`，不在版本目錄、gate 掃不到也不該掃（那些路徑不歸版本目錄管）。單份 manifest 的結果是：gate 在 `agent/` 下找不到 sig、路徑也對不上 → **正常包每次啟動必拒**。拆成雙層後各管各的：

| 層 | 落點 | 用途 | 驗證時機 |
|----|------|------|---------|
| agent 層 | `agent/manifest.json`＋`agent/manifest.sig` | runtime 啟動 gate（掃 agent/ dist：binary、.so、assets） | 每次啟動全驗 |
| bundle 層 | 骨架根 `manifest.json`＋`manifest.sig` | 交付完整性（掃整個骨架，**含 agent 層 sig 形成互鎖**） | install.sh 安裝前驗一次 |

簽章順序：agent 層先簽（它是交付檔的一部分）→ 骨架全組完 → bundle 層才產 manifest 送簽 → tar。打包腳本有可執行斷言擋順序倒置（bundle manifest 必含 `agent/manifest.sig`）。兩層同走 LC `agent_manifest` type、同一把鑰。

連帶兩個配套調整（同 commit `a11ccf2`）：
- `gen_agent_manifest.py` 加 `--external-symlinks {abort,skip}`：工具樹帶指向**安裝後路徑**的 symlink（CINC binstub、LibreOffice desktop 檔），屬 omnibus／deb 打包形式，tar 原樣保留出貨；bundle 掃描以 skip 略過不收錄（對 build 機上恰好同名的檔算 hash 既無意義也不穩定），dist 掃描維持 abort 原判準。
- `startup_gate.py` 錨點與 `resource_root()` 分家（`_manifest_root()`）：打包模式**固定＝binary 所在目錄、不吃 `AGENT_RESOURCE_ROOT`**——驗證錨點可被環境變數改指等於自廢，且 smoke 第 4c 項（拿該變數做 resource_path 反證）會讓簽章產物在 gate 假紅。源碼模式保留覆寫（fake-root 突變測試靠它）。

```{.mermaid cap="圖 2 — tarball 交付物結構（D1/D3/D6 落地形狀）"}
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
flowchart LR
  T["guidant-agent-&lt;ver&gt;.tar.gz<br/>~1GB+（D3 已接受）"]
  T --> A["agent/<br/>Nuitka standalone 產物<br/>自家 code 已編機器碼<br/>＋manifest.json/.sig（agent 層：<br/>啟動 gate 每次啟動全驗）"]
  T --> B["tools/<br/>外部工具 bundle"]
  T --> C["install.sh<br/>七步安裝＋互動問答<br/>upgrade / rollback / 維運子命令"]
  T --> D["manifest.json ＋ manifest.sig<br/>（bundle 層：install.sh 安裝前驗整包，<br/>涵蓋 agent 層 sig 互鎖）<br/>FR-064 泛用層簽章 type=agent_manifest<br/>🔄 雙層拆分見 §5.2.3"]
  T --> E["systemd/<br/>guidant-agent.service<br/>EnvironmentFile=/etc/guidant-agent/agent.env"]
  B --> B1["cinc-auditor（~275MB）<br/>⚖️ 不可換官方 InSpec"]
  B --> B2["sonar-scanner＋JRE（~147MB）"]
  B --> B3["LibreOffice＋fonts-noto-cjk（~400MB+）<br/>形式待 Q1 實測"]
  B --> B4["xsltproc＋nmap.xsl / git"]
```

出貨側 build 管線（FR-063/064 資產複用點）：

```{.mermaid cap="圖 3 — 出貨側 build 管線（.1 → .2 產物流）"}
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
flowchart LR
  S["evidence-agent 源碼<br/>+ jedi-common / jedi-file-upload<br/>（build 機需可達 Nexus）"] --> W["dependency-injector<br/>私房 wheel<br/>（複用 FR-063 rebuild_wheels.sh）"]
  W --> N["Nuitka standalone 編譯<br/>version bake ＋ resource_path()<br/>--include-data-dir assets/<br/>EXCLUDE 名單重評（Q3）"]
  N --> P["組 tarball 骨架<br/>agent/ + install.sh + systemd/"]
  TB["工具 bundle 下載/整備<br/>CINC / sonar-scanner /<br/>LibreOffice（Q1）/ xsltproc / git"] --> P
  P --> M["gen_integrity_manifest.py<br/>（FR-064 改常數）逐檔 hash"]
  M --> SG["sign_manifest.sh → License Center 簽章<br/>type=agent_manifest（D6）"]
  SG --> OUT["guidant-agent-&lt;ver&gt;.tar.gz<br/>出貨"]
```

### 5.3 FR-066.3　install.sh 與 systemd

**白話**：這階段做客戶手上唯一會碰到的東西——安裝腳本。客戶解開 tarball、跑 `install.sh`、回答三個問題，agent 就變成一個開機自起、由 systemd 看管的系統服務並自動向雲端註冊上線；日後升級換版、出問題回滾、日常看狀態看 log，也全走同一支腳本的子命令。它吃 .2 的 tarball 當輸入，是 .1/.2 成果對客戶的「出口」。

#### 5.3.1 install.sh 主流程

七步（承 FR-065 七步結構）：**前置檢查**（root / systemd / 磁碟空間 / amd64；支援 `--check-only`）→ **驗 manifest 簽章**（D6 交付完整性）→ **落檔** `/opt/guidant-agent/versions/<ver>/` → **裝 bundle 工具**（含 Q1 定案的 LibreOffice 落地形式與 fonts-noto-cjk 字型）→ **建 symlink**（`current` → 版本目錄；工具指令 symlink 供 connector 預設路徑取用）→ **裝 systemd unit**（`guidant-agent.service`，`EnvironmentFile=/etc/guidant-agent/agent.env`）→ **啟動**（`systemctl enable --now`）。

互動問答三題生成 `agent.env`（D5）：①雲端位址 ②註冊 token ③本機對外位址（CSR SAN 用）。生成後 agent 首次啟動即走既有 `core/enroll.py` 自我註冊，並以 gunicorn/ssl 自起 8443 資料面 TLS（D4）——nginx unit／設定完全不存在於交付物。

#### 5.3.2 upgrade / rollback（D7）

- `install.sh --upgrade <新tarball>`：驗章 → 解進 `versions/<新ver>/` → 停服務 → 切 `current` symlink → 起服務；資料目錄（SQLite 檔、mTLS 憑證、`agent.env`）不動。
- `install.sh --rollback`：切回前一版 symlink、重啟。無 DB migration、無備份還原步驟——這是刻意的輕量模型（D7）。

#### 5.3.3 維運子命令

比照 FR-065：`status`（服務狀態＋版本＋心跳連線）／`logs`（journalctl 包裝）／`uninstall`（停服務、移除 unit 與版本目錄；資料目錄詢問後才刪）／`fingerprint`（印出本機三指紋值，供雲端綁定核對）。

### 5.4 FR-066.4　驗收與文件

**白話**：前三棒各自驗過自己，這棒證明「整條路對客戶真的成立」——拿一台從沒裝過任何東西的 Linux 機，只給它一顆 tarball，從安裝到跑完一輪真實檢測任務；再模擬客戶的封閉網路（斷網）環境重來一遍。最後把過程寫成客戶看得懂的安裝手冊，並把 Windows 可不可行的評估寫成文件收口 D8。

- **真機驗收**：乾淨 Linux 機（非 build 機、非開發機）→ tarball → install.sh → 心跳上線 → 派一輪實際檢測任務（至少涵蓋一個用到 bundle 工具的 connector，如 CINC）→ 證據回收成功；另驗 upgrade → rollback 一輪。
- **封閉網路驗證**：斷外網環境完整重跑安裝與任務（工具 bundle 自足性的最終證明）。
- **客戶安裝手冊**：比照 FR-065 T-4.1 手冊體例（安裝／開通／升級／回滾一本通），agent 版另含目標主機前提（OpenSCAP / Nmap 引擎裝在受測機的說明沿用 FR-057/058 setup_guide 內容）。
- **Windows 評估文件**（D8）：從零寫，覆蓋指紋來源（WMI / registry）、服務形式（Windows Service / NSSM）、工具鏈可用性三題，結論給「可行性＋若做的前提清單」，不承諾時程。

---

## 6. 拆分（6 子需求 × 21 子任務）

依賴鏈：**FR-066.1 → .2 → .3 串行，.4 收口**。

### FR-066.1　Nuitka build 管線與程式改造 — 依賴：無（起點）

| # | 子任務 | 內容 | 驗收 | 依賴 |
|---|--------|------|------|------|
| T-1.1 | SQLite 化 | **Q2 掃描先行**（jedi-file-upload PG 方言）→ `init_db()` 換 `sqlite:///`、SQLite 檔落資料目錄、agent-db 退場 | Q2 掃描結果落文件；agent 以 SQLite 起動、檔案上傳/查詢功能綠；無任何 PG 連線需求 | — |
| T-1.2 | 程式改造 | version bake 取代讀 pyproject.toml；`__file__` 三處（inspec.py:145 / nmap.py:101 / config.py:85）改 `resource_path()`；指紋直讀＋廢 collect-host-id.sh（D9）；connector 工具路徑配置化 | 源碼樹不存在的環境下版號/資源定位正確；裸機直讀三指紋值與宿主實值一致；collect-host-id.sh 掛點全移除 | T-1.1 |
| T-1.3 | agent 版 build_release | 以 FR-063 管線為藍本：私房 wheel 複用、Nexus 套件編入、`--include-data-dir assets/`、EXCLUDE 名單重評（**Q3**）、smoke 驗證 | 無 Python 環境的乾淨機上產物可啟動、建 SQLite 表、心跳邏輯 dry-run 通過；build 腳本可重跑 | T-1.2 |

### FR-066.2　tarball 打包與工具 bundle — 依賴：FR-066.1（build 產物是輸入）

| # | 子任務 | 內容 | 驗收 | 依賴 |
|---|--------|------|------|------|
| T-2.1 | 工具 bundle | CINC omnibus／sonar-scanner＋JRE／LibreOffice 離線化（**Q1** 實測選定 deb 組 vs portable）＋fonts-noto-cjk／xsltproc＋nmap.xsl／git；**Q4** openscap-utils・sshpass 殘留查證 | 斷網環境下工具全數可安裝且可執行（cinc-auditor version / sonar-scanner -v / libreoffice 轉檔＋`fc-match 'Noto Sans CJK TC'` 正確）；Q1/Q4 結論落文件 | T-1.3 |
| T-2.2 | manifest 簽章＋啟動驗章 | 簽章泛用層加 `agent_manifest` type；`gen_integrity_manifest.py` 改常數；`sign_manifest.sh` 接 License Center；agent 啟動驗章、失敗拒起 | 正常包啟動綠；任改一檔啟動即拒（突變驗證）；簽驗流程可重跑 | T-1.3 |
| T-2.3 | build_bundle 打包腳本 | tarball 骨架組裝（agent/ + tools/ + install.sh + systemd/ + manifest）＋sha256 產出 | 一鍵產出 `guidant-agent-<ver>.tar.gz`＋sha256；解包結構與圖 2 一致 | T-2.1, T-2.2 |

### FR-066.3　install.sh 與 systemd — 依賴：FR-066.2（tarball 是輸入）

| # | 子任務 | 內容 | 驗收 | 依賴 |
|---|--------|------|------|------|
| T-3.1 | install.sh 主流程 | 七步安裝＋`--check-only`＋互動問答三題生成 agent.env＋systemd unit＋自起 TLS 8443（D4/D5） | 乾淨機一鍵裝完 → 服務 active → enroll 成功 → mTLS 心跳上線 → 8443 憑證為 CA 簽發（SAN=填答位址） | T-2.3 |
| T-3.2 | upgrade / rollback | `--upgrade`（驗章→解新版→切 symlink→重啟）＋`--rollback`（切回前版）；資料目錄不動（D7） | 升級後版本上報為新版且既有資料（SQLite / 憑證）保留；回滾後服務以舊版正常心跳 | T-3.1 |
| T-3.3 | 維運子命令 | status / logs / uninstall / fingerprint（FR-065 同款體例） | 四子命令輸出正確；uninstall 後系統無殘留 unit；fingerprint 輸出與雲端綁定值一致 | T-3.1 |

### FR-066.4　驗收與文件 — 依賴：FR-066.1–.3（收口）

| # | 子任務 | 內容 | 驗收 | 依賴 |
|---|--------|------|------|------|
| T-4.1 | 乾淨機真機驗收＋封閉網路驗證 | 乾淨 Linux 機端到端（§7 全清單）＋斷網環境重跑安裝與任務 | §7 端到端驗收全數通過並留紀錄 | T-3.3 |
| T-4.2 | 客戶安裝手冊 | 安裝／開通／升級／回滾一本通（FR-065 手冊體例），含目標主機前提說明 | 依手冊可由非開發者完成安裝（以 T-4.1 過程校驗步驟完備） | T-4.1 |
| T-4.3 | Windows 評估文件 | 從零寫：指紋（WMI/registry）／服務形式（Windows Service/NSSM）／工具鏈可用性三題（D8） | 三題各有結論與前提清單；明確標注「不實作不承諾」 | — |

### FR-066.5　EL（Rocky/RHEL 9）生態支援（PM 決議暫緩，最後做） — 依賴：FR-066.1–.3（產物與腳本是輸入）；於 .4 收口前完成

| # | 子任務 | 內容 | 驗收 | 依賴 |
|---|--------|------|------|------|
| T-5.1 | 編譯環境降 glibc（容器化編譯） | build_agent_release 改於 debian:11（glibc 2.31）容器內編譯（Python 版本對齊、私房 wheel／Nexus 憑證進容器、build_all 接上）；產物向上相容三家族 | smoke_agent_release 5/5 綠；rockylinux:9 容器內產物可啟動、無 GLIBC 缺符號；build_all 一鍵重出 tarball | — |
| T-5.2 | install.sh EL 適配 | `--check-only` 加發行版辨識（os-release）＋SELinux 狀態（getenforce）＋firewalld 8443 檢測；輸出對應指引指令，不自動改防火牆/安全政策；SELinux enforcing 下服務起不來的最小適配（runner 查證後定案，查證先於修） | Rocky 9 上 --check-only 輸出正確指引；deb 家族輸出不回歸；enforcing 下服務可 active | T-5.1 |
| T-5.3 | Rocky 9 真機端到端驗收 | SELinux enforcing 的 EL9 機器走全鏈：install → TOFU → enroll → 心跳 → 8443 mTLS → 派一單 CINC 檢測任務 → 報告回雲端、LibreOffice 轉 PDF 中文不缺字（驗收機由決策者提供） | §7 驗收清單於 EL9 全數通過並留紀錄 | T-5.2 |

### FR-066.6　docker compose 形態＋SeaweedFS 內建儲存 — 依賴：FR-066.1（編譯產物是輸入）；與 .4 平行

| # | 子任務 | 內容 | 驗收 | 依賴 |
|---|--------|------|------|------|
| T-6.1 | 指紋二源化（D11，先行） | device_uuid 改 sha256(product_uuid\|machine-id) 二源（agent 端演算法＋原生/容器兩形態一致）；BE enroll 端加同指紋撞號偵測（拒絕並提示，含 error code）；既有 agent 重 enroll SOP 落文件；collect-host-id.sh 殘跡確認全清 | 二源指紋在原生與容器內算值一致且等於宿主直讀；BE 撞號註冊被拒且訊息可辨；123 重 enroll 成功 | — |
| T-6.2 | agent SeaweedFS 儲存支援 | config.py 補 seaweedfs 分支（複用套件 SeaweedfsUploadConfigDTO）；env 契約定案（STORAGE_TYPE=seaweedfs 預設值與 SEAWEEDFS_* 變數，沿用既有 MINIO_* 命名精神、先 grep 全 codebase 確認無同義變數）；自有 SeaweedFS/MinIO 設定文件；防篡改成立條件查證（SeaweedFS S3 Object Lock/WORM 支援度＋威脅模型邊界，查證結論落文件、宣稱強度依結論定） | STORAGE_TYPE=seaweedfs 時檔案實際落 SeaweedFS 且 storage_type 記 seaweedfs；改 env 指自有 MinIO 亦通；查證結論落卡與文件 | — |
| T-6.3 | compose 交付包 | Nuitka 產物進薄 base image（非源碼版）＋SeaweedFS service（官方 image，weed server -s3 -filer，admin UI 曝露走內網介面綁定）＋指紋二源掛載（product_uuid/machine-id 唯讀）＋資料 volume 佈局（D7 精神：資料與版本分離）＋build_all 加 compose 打包出口（同一顆產物同一套簽章）；舊 deploy/docker-compose.yml 汰換或改造 | 乾淨 docker 機 compose up 一鍵起（agent＋SeaweedFS），agent 8443 mTLS 自起、enroll 上線；image 內翻不到 .py 源碼；build_all 一鍵出 compose 包 | T-6.1, T-6.2 |
| T-6.4 | 端到端驗收（含 D13 既有鏈路） | compose 版裝起 → 雲端儲存設備設定選「使用 Agent 服務」→ 上傳證據 → 檔案落 agent 側 SeaweedFS（admin UI 可見、DB storage_type 正確）→ 產品端預覽/下載通；升級（換 image tag）資料存活；驗收紀錄落文件 | 全鏈通並留紀錄；既有 FR-039 讀寫路徑無回歸 | T-6.3 |
| T-6.5 | configure-storage 子命令 | agent 維運子命令加 configure-storage：互動問答/帶參數設定儲存後端三型，寫 agent.env、實打端點驗連通、重啟生效；compose 形態對應操作路徑（exec 或 .env 說明）；deploy README 同步 | 三型各設定一輪皆通（連通驗證擋得住錯 endpoint/憑證）；設定後上傳實際落新後端；原生與 compose 皆有可操作路徑 | T-6.2 |
| T-6.6 | compose 版互動安裝程式 | 交付包補 install.sh（照主產品 FR-065 installer 模式）：①環境檢查（docker/compose plugin/磁碟/machine-id/product_uuid 可讀）②docker load＋digest 比對③互動問答（雲端位址/註冊 token/本機對外位址三題，SeaweedFS 憑證自動生成）④產 .env（600）⑤compose up 等 healthcheck⑥印上線結果；維運子命令 status/logs/restart/stop/start/uninstall＋configure-storage 轉呼叫容器內 ctl；--check-only/--config 非互動模式；升級模式（load 新 image→換 tag→up -d，資料 volume 不動）；README/INSTALL.txt 對齊 | 乾淨 docker 機解包跑 install.sh 問答三題→兩容器 healthy→enroll 上線，全程零 docker 指令；--check-only 在缺 docker 機器上正確擋下；維運子命令可用；upgrade 換版資料存活 | T-6.3 |

| T-6.7 | 恢復系統內建 SeaweedFS（出廠快照＋configure-storage 選項） | 安裝生成 SEAWEEDFS 帳密時快照 `factory-defaults.env` 到資料目錄（600、只含 SEAWEEDFS_* 五項，D7 資料目錄語意跨版本存活）；原生 `scripts/install/install.sh` 與 compose `deploy/install.sh` 兩處同步；configure-storage 第一題改四選項（1 系統內建恢復預設／2 自有 SeaweedFS／3 自有 MinIO／4 本機磁碟），選 1 從快照讀回→照舊連通驗證→寫入→重啟、user 零輸入；快照不存在時選項照顯但給明確指引（舊站點已改自有後端補不回，文件註明找原廠）；upgrade 路徑補快照（現行設定仍為內建值時）；「舊檔不搬家」紅字警告在恢復路徑同樣觸發（T-6.5 機制）；deploy README 對齊。主產品對應功能另卡處理 | 原生與 compose 各走一輪「內建→自有→恢復內建」循環，恢復後上傳實落內建 SeaweedFS；快照檔 600；無快照場景指引正確；deb 家族不回歸 | T-6.5, T-6.6 |

### FR-066.7　Windows（Docker Desktop）支援 — 依賴：FR-066.6（compose onepack 包是輸入）

> ⏸ **D19（2026-08-23）：本節全卡樹暫緩**——PM 拍板 Windows 不再探討；T-7.1/T-7.4 已完成的探路結論保留（見 t71-windows-probe.md／t74-wsl-engine-probe.md）。

| # | 子任務 | 內容 | 驗收 | 依賴 |
|---|--------|------|------|------|
| T-7.1 | 160 真機探路實測 | 在 192.168.50.160（Win11＋Docker Desktop）手動走通全鏈並記錄實況：onepack 解包→（暫用 WSL 終端跑現有 install.sh 或手動等效步驟）→兩容器起→enroll 上線→派一單檢測任務。重點查證四項落文件：①指紋二源掛載在 Docker Desktop 的實際行為（product_uuid 有無/machine-id 是誰的/守門與 log 表現）②雲端連回 8443 的網路路徑（Docker Desktop 端口映射行為，需不需要額外設定）③安裝第三題 IP 自動偵測的錯法與正確填法④重置 Docker Desktop 後重新開通 SOP 實測。實測記錄=T-7.2 的規格輸入 | 全鏈通並留實測紀錄；四項查證各有結論；擋路項（若有）逐條列出 | — |
| T-7.2 | install.ps1 互動安裝程式 | 照 deploy/install.sh 的流程邏輯（環境檢查→load image→問答三題→生成 .env→compose up 等 healthy→印上線結果）出 PowerShell 版；環境檢查改查 Docker Desktop（裝了沒/跑了沒/版本）；第三題預設值改 Windows 主機 IP 偵測；TOFU 憑證確認、SeaweedFS 帳密生成、factory-defaults.env 快照對等實作；維運子命令（status/logs/restart/stop/start/uninstall/configure-storage 轉呼叫容器內 ctl）對等；T-7.1 查證出的 Windows 專屬步驟（8443/防火牆等）內建進流程或明確指引 | 160 上解包→install.ps1 問答三題→兩容器 healthy→enroll 上線，全程不開 Linux 終端不手打 docker 指令；維運子命令可用；configure-storage 恢復循環通 | T-7.1 |
| T-7.3 | Windows 前置指引＋驗收收口 | 薄指引文件（Docker Desktop 安裝與授權須知/機器前提/install.ps1 用法/重置後重新開通 SOP/已知限制含指紋漂移邊界）；160 端到端終驗（含升級路徑：同 onepack 跑 upgrade 資料存活）；build 側若需（onepack 內附 install.ps1）調整打包腳本 | 指引可由未參與者照做裝成；終驗全鏈＋升級通留紀錄；PM 展示可用（setup.exe 升級項的反饋入口） | T-7.2 |
| T-7.4 | B 案生死驗證（已完成，判死） | WSL2＋docker engine＋排程器常駐可行性實測 | ✅ 完成（2026-08-23）：不可行——閒置 60 秒凍結，詳 t74-wsl-engine-probe.md | — |

### FR-066.8　原生 Windows 版 — 依賴：FR-066.1（Nuitka 產物源碼同源）；T-4.3 評估＋T-7.1 實測為規格輸入

> ⏸ **D19（2026-08-23）：本節全卡樹暫緩**——PM 拍板 Windows 不再探討；T-7.1/T-7.4 已完成的探路結論保留（見 t71-windows-probe.md／t74-wsl-engine-probe.md）。

| # | 子任務 | 內容 | 驗收 | 依賴 |
|---|--------|------|------|------|
| T-8.1 | Windows build 管線 | Windows build 機佈建（MSVC Build Tools＋Python 3.11＋Nexus 可達；機器待決策者定案，160〔Server 2025, 8vCPU/24GB〕可兼任候選）；dependency-injector 私房 wheel MSVC 重編；Nuitka standalone 編 agent.exe；build 腳本 PowerShell 載體；smoke 對應（產物翻不到 .py／可啟動／健康檢查）。參考 Linux 版教訓：1303~1309 七輪除錯，預留試錯棒數 | 無 Python 的乾淨 Windows 機上 agent.exe 可啟動、建 SQLite、心跳 dry-run 通過；build 可重跑 | build 機定案 |
| T-8.2 | 指紋 OS 抽象層＋程式改造 | fingerprint provider 化（D9 延後項正式開工）：Linux=讀檔（現行）、Windows=WMI Win32_ComputerSystemProduct.UUID＋registry MachineGuid（winreg 標準庫）；WMI 佔位值（FFFFFFFF…）處理策略；工具路徑預設值 Windows 化（AGENT_CINC_BIN 等）；設定落點 C:\ProgramData\GuidantAgent\ | 二源指紋在 Windows 讀值與 WMI/registry 直查一致；佔位值場景有明確行為；Linux 版行為零回歸（既有測試綠） | T-8.1 |
| T-8.3 | 雙服務化＋install.ps1 | WinSW（或同族 wrapper，實測選定）包 agent.exe＋weed.exe（SeaweedFS Windows 版）成兩個 Windows 服務（開機自起/崩潰重啟/log 輪替/優雅停止）；install.ps1 七步（環境檢查→驗章→落檔 versions\→裝服務→問答三題〔IP 偵測用 Find-NetRoute，T-7.1 已驗〕→防火牆規則自動加〔T-7.1 已驗一條 inbound 即通〕→啟動）；TOFU 憑證確認 PowerShell 重寫；SeaweedFS 帳密生成＋factory-defaults 快照＋configure-storage 對等；升級回滾（versions\＋junction 切換，D7 模型）；維運子命令 | 乾淨 Windows 機 install.ps1 問答三題裝成→兩服務執行中→enroll 上線；**登出＋重開機後服務自起、雲端仍在線**；升級回滾資料存活；維運子命令可用 | T-8.1（可與 T-8.2 平行起工） |
| T-8.4 | 工具鏈 Windows 離線整備 | CINC msi silent（msiexec /qn）＋安裝後路徑；sonar-scanner windows-x64 zip（自帶 JRE）；LibreOffice 選型實測（msi silent vs Portable，Q1-Windows）＋Noto CJK 字型落地＋headless 中文轉檔驗證；MinGit；weed.exe 釘版；Windows 版 tools-manifest（釘版＋sha256＋Nexus 第二來源）整套 | 斷網 Windows 機上工具全數可裝可執行（cinc-auditor version／sonar-scanner -v／LibreOffice 轉 PDF 中文不缺字）；manifest 完整 | 可與 T-8.1~8.3 平行 |
| T-8.5 | 簽章＋乾淨機端到端驗收 | manifest 簽章泛用層接 Windows 產物（agent_manifest type 沿用）＋啟動驗章＋突變測試；打包腳本出 Windows 交付包；乾淨機端到端：裝→enroll→跑 CINC＋SSH 遠端型任務→證據落本機 SeaweedFS→登出/重開機存活→升級回滾→防竄改；SmartScreen 白名單指引寫入文件（D18 策略） | §7 對應項全綠留紀錄；改任一檔啟動被拒；白名單指引可操作 | T-8.2, T-8.3, T-8.4 |

### 開放項歸屬（Q1–Q4）

| # | 查證項 | 歸屬子任務 | 性質 |
|---|--------|-----------|------|
| Q1 | LibreOffice 離線化形式（deb 組 vs portable/AppImage） | T-2.1 | 實測選定，判準：離線可裝／字型一起落／體積可接受 |
| Q2 | jedi-file-upload PG 方言依賴掃描 | T-1.1 | **D2 落地的前置守門**，.1 開工先做 |
| Q3 | Nuitka EXCLUDE_COMPILE_PKGS 名單重評 | T-1.3 | agent 依賴集合與 BE 不同，不照抄 FR-063；冷 build 預期遠低於 BE 56 分鐘 |
| Q4 | openscap-utils / sshpass 是否已無用 | T-2.1 | 殘留即不搬進 tarball，包更瘦 |

---

## 7. 端到端驗收

1. **乾淨 Linux 機**（無 Python、無 docker、非 build 機）解 tarball → `install.sh` 七步全綠 → 互動三題填答 → 服務 active、開機自起。
2. Agent 直讀宿主三指紋（product_uuid / machine-id / MAC）與實機值一致；enroll 全自動拿到 mTLS 憑證；**心跳上線**、雲端 agent 清單可見。
3. **一輪實際檢測任務跑通**：至少含一個吃 bundle 工具的 connector（CINC）＋一個 SSH 遠端型（OpenSCAP）→ 報告回收進證據池；LibreOffice 預覽轉 PDF 中文不缺字。
4. **封閉網路情境**：斷外網環境完整重跑 1–3（雲端產品在同內網），無任何步驟需要對外連線。
5. **升級→回滾**：`--upgrade` 新版心跳上報新版號、SQLite 資料與憑證保留；`--rollback` 後舊版正常服務。
6. **防竄改**：竄改任一交付檔後啟動被拒（manifest 驗章生效）。
7. 文件收口：客戶安裝手冊、Windows 評估文件產出。
8. **EL 家族**：以上 1–3 於 Rocky Linux 9（SELinux enforcing）重跑一輪通過（FR-066.5）。
9. **compose 形態**：docker 機上 compose 版完成上述 2/3/6 對應項＋「使用 Agent 服務」證據上傳落 SeaweedFS 全鏈通（FR-066.6）。
10. **Windows（Docker Desktop）形態**：160 真機上 install.ps1 全程問答安裝→enroll 上線→跑一單檢測任務→報告回雲端；重置 Docker Desktop 後照 SOP 重新開通成功（FR-066.7）。
11. **原生 Windows 形態**：乾淨 Windows 機 install.ps1 裝成雙服務→登出/重開機服務存活且雲端在線→跑檢測任務→證據落本機 SeaweedFS→升級回滾→防竄改拒起（FR-066.8）。

---

## 附註

- 本案標的 repo 是 **evidence-agent**；主專案（compliance-manager-be）只承載文件與簽章工具鏈的複用來源。
- Q1–Q4 不阻擋開工，但 Q2 是 T-1.1 的前置守門。
- Notion 開卡（母案＋子卡）於派工時依 `big-feature-workflow` skill Step 4/5 進行，本文件拆分表即開卡依據。
