---
title: "160 Windows 真機探路實測紀錄（FR-066.7 T-7.1 / CM-1337）"
brand: "Guidant AI · **FR-066** 檢測 Agent 原生安裝包"
eyebrow: "FR-066.7 T-7.1 · Windows 探路實測 · 2026-08-22"
h1: "160 Windows 真機探路實測紀錄"
lede: "在 192.168.50.160（Windows Server 2025 + Docker Desktop 29.7.2）手動走通 agent 全鏈，把四項 Windows 專屬問號查成答案。全鏈兩輪皆通（重置前後各派一單檢測任務、報告回雲端）；四項查證各有結論，其中查證①（指紋）與新識別的自起缺口都**推翻或修正了 T-4.3 評估文件的預測**，是 T-7.2 install.ps1 的硬規格輸入。"
chips: [
  {text: "實測紀錄・非設計文件", kind: warn},
  {text: "對應卡：CM-1337（T-7.1）", kind: plain},
  {text: "全鏈兩輪通・四項查證完成", kind: accent}
]
footer: "FR-066.7 T-7.1 · Windows 探路實測 · 2026-08-22 · 實測值以本文件為準，與 T-4.3 評估衝突處以本文件覆蓋"
---

> 🔴 **本文件記錄「探路怎麼走通的」，不是「客戶要這樣裝」。** 探路階段允許權宜手段
> （手改 compose 檔、SSH 遠端下指令、手寫 .env）；**正式交付一律不要求客戶開 Linux
> 終端、不要求客戶手改檔案**（D15/D17 受眾邏輯）。權宜處一律標示 ⚠️**探路權宜**，
> 與「結論」分開寫。
>
> 對應：design.md **D17**（Docker Desktop 路線裁示）、`windows-feasibility.md` §2
> （docker 過渡模式評估——本次實測即在驗證／推翻該節結論）。

## 0. 結論總表（30 秒版）

| # | 查證項 | 實測結論 | 對 T-4.3 評估的關係 | 卡點等級 |
|---|--------|---------|-------------------|---------|
| ① | 指紋 | **compose 原檔在 Windows 起不來**（不是「起得來但值不對」）。`/sys/class/dmi/id/product_uuid` 掛載讓 docker 直接報 `operation not permitted`；拿掉該行後 agent 正常啟動，指紋退化成 machine-id 單源、且該 machine-id 是 **Docker Desktop VM 的**（非 Windows 主機） | **修正**（評估推測「docker 建空目錄→graceful 回空」，實際是硬失敗） | 中（T-7.2 必處理，處理方式明確） |
| ② | 網路 | 8443 有正常映射到 `0.0.0.0`（Docker Desktop 自動做），但 **Windows 防火牆預設擋住入站**、Docker Desktop **不會**自動開規則。加一條 inbound allow 後雲端 mTLS 打通（HTTP 200） | **補充**（評估只提到「要靠 portproxy 或 mirrored networking」，實測顯示映射本身沒問題，缺的只是防火牆規則） | 低（一行指令解決） |
| ③ | IP 偵測 | `Find-NetRoute -RemoteIPAddress <雲端IP>` 回 `192.168.50.160`（正確的 Windows 主機網段 IP），與 Linux `ip route get` 語意等價 | **證實**（評估預判可移植，實測確認） | 低 |
| ④ | 重置重開通 | **分層結論**：重啟／`wsl --shutdown`／RDP 斷線 → 指紋**不變**、容器自起；**factory reset → 指紋漂移**（`9e382ac8…`→`8e579991…`），雲端多一筆同名 agent。重開通僅需 `docker compose up -d`（.env 與 compose 檔不在 volume 內，存活） | **證實並量化**（評估說「reset 即變」，實測給出確切邊界與新舊值） | 低（符合 D17「可接受維運事件」） |
| 🆕 | **自起（新識別，評估文件未涵蓋）** | **使用者登出後 Docker Desktop 全滅、engine 消失、agent 離線**。Docker Desktop 自起只掛在 `HKCU\...\Run`（需登入）；SYSTEM 排程與 `com.docker.service` 兩條無登入路徑實測皆**無法**起 engine | **新增**（T-4.3 未談及；這是 Docker Desktop 路線的結構性限制） | **高（P0，T-7.2 必須有答案）** |

**全鏈驗收**：兩輪端到端皆通——重置前（agent id=6，任務 id=13）與重置後（agent id=7，
任務 id=14）各派一單 nmap 檢測，皆 `succeeded`，報告
`Nmap掃描報告_192.168.50.123_20260823.html` 回到雲端、證據檔落在 160 本機 SeaweedFS。

---

## 1. 環境實況（與卡片記載的差異先講）

| 項目 | 卡片／STATE 記載 | 實測 | 說明 |
|------|----------------|------|------|
| OS | Win11 | **Windows Server 2025 Standard**（10.0.26100） | 機器名 `JEDI-WIN2025-01-AD`，網域 `jedicogy.com`。**不影響本次結論**（Docker Desktop 行為一致），但 T-7.2 的環境檢查文案不應寫死「Windows 11」 |
| Docker | 29.7.2 已裝 | ✅ 29.7.2，Docker Desktop（WSL2 backend） | context `desktop-linux`；kernel `6.18.33.2-microsoft-standard-WSL2` |
| 規格 | — | 8 vCPU / 24GB RAM / C: 剩 202GB | docker VM 配 12GB |
| WSL distro | — | `Ubuntu`（Stopped）、`docker-desktop`（Stopped） | 詳見 §4 為何 install.sh 仍不能用 |
| 既有角色 | WinRM 掃描受測機 | ✅ 確認（`compliance.agent_tasks` id=12 即以 160 為 GCB/WinRM 目標） | agent 安裝與受測角色並存，本次未觀察到衝突 |

**雲端側**：188 DEV（`CLOUD_ENDPOINT=https://192.168.50.188`，自簽憑證走 TOFU）。
🔴 全程未觸碰 STG／POC。DEV 側的**寫入**僅限：兩筆測試任務 insert（`compliance.agent_tasks`
id=13/14）＋ agent 自行 enroll 產生的兩筆 `compliance.remote_agents`（id=6/7）；其餘皆唯讀。

---

## 2. 交付包搬運與解包（無擋路項）

| 步驟 | 實測 | 耗時 |
|------|------|------|
| 188 → 160 傳輸 | `scp` 1375323973 bytes，sha256 `0b5d47f0…` **完全相符** | 約 12 分鐘 |
| 解包 | **Windows 內建 `tar.exe`（bsdtar 3.7.7）直接解 .tar.gz** | 45 秒 |
| image tar 校驗 | 兩顆皆與 `image-manifest.sha256` 相符（agent `a4ac9ecf…`／seaweedfs `31f47e9b…`） | — |
| `docker load` | 兩顆成功，agent image 3.32GB（載入後顯示 6.79GB） | 約 6 分鐘 |

**✅ 結論（T-7.2 規格輸入）**：
1. **不需要求客戶另裝解壓工具**——Windows 10/11/Server 2019+ 內建 `tar.exe` 可解 .tar.gz。
   install.ps1 可直接呼叫，或（若包已解開）省略此步。
2. **image-manifest.sha256 的校驗邏輯可原樣移植**——`Get-FileHash -Algorithm SHA256`
   輸出大寫 hex，比對前 `.ToLower()` 即可。
3. ⚠️ **`docker load` 的 3.3GB 是安裝流程最慢的一步（約 6 分鐘）**，install.ps1 要有進度
   提示，不能靜默等待（客戶會以為當掉）。

### 2.1 ⚠️ 已知雷：非互動 session 下 registry pull 會失敗

`docker pull`（任何 registry 拉取）在 SSH／非互動 session 下報：

```
error getting credentials - err: exit status 1,
out: `A specified logon session does not exist. It may already have been terminated.`
```

成因是 `credsStore: "desktop"` 的 credential helper 依賴 DPAPI 使用者 session。
`DOCKER_CONFIG` 指向空 config、`docker --config` 皆**無法**繞過（實測三種寫法皆失敗）。

**對 install.ps1 的意義**：**流程中不可有任何 registry pull**。所幸現行設計本就是
`docker load` 本地 tar（不碰 registry），此雷不會踩到——但**若未來為了省體積改成
「seaweedfs 從 registry 拉」，Windows 版會在這裡壞掉**，且錯誤訊息完全不指向根因。

---

## 3. 查證①：指紋（本卡最重的技術發現）

### 3.1 實測：compose 原檔在 Windows **起不來**

直接 `docker compose up -d`（一個字未改的交付包原檔）：

```
Container guidant-ai-agent Starting
Error response from daemon: error while creating mount source path
'/sys/class/dmi/id/product_uuid': mkdir /sys/class/dmi: operation not permitted
```

`docker compose config --quiet` 通過（語法沒問題），**失敗發生在啟動階段**。
seaweedfs 正常起來，只有 agent 這個服務起不來。

### 3.2 🔴 這推翻了 T-4.3 評估 §2.2 的預測

`windows-feasibility.md` §2.2 的後果鏈寫：

> product_uuid 缺 → compose 對不存在的來源做檔案掛載時 **docker 會自動建一個空目錄**
> 頂在掛載點上 → agent `_read_source()` 對目錄 open 失敗 → graceful 回空字串 →
> **指紋退化成只剩 machine-id 單源**

**實際上「docker 自動建空目錄」這一步不會發生**，因為 Docker Desktop 的 Linux VM 內
`/sys` 是唯讀（實測：`/sys/class/dmi/id/` 整個目錄不存在，且 docker 連 `mkdir /sys/class/dmi`
都被拒）。所以症狀不是「安靜地算錯指紋」，而是**大聲地起不來**。

**這對我們是好消息**：硬失敗比靜默算錯好得多——不會出現「客戶裝好了、雲端多一台
陌生 agent、沒人發現」的壞法。但 T-7.2 **必須**處理，否則 Windows 客戶第一步就卡死。

### 3.3 拿掉該行後的實測值

⚠️**探路權宜**：手動把 `docker-compose.yml` 的 product_uuid 掛載那行註解掉
（正式做法見 §3.5）。之後：

```
WARNING:agent:device fingerprint source unreadable (/host/product_uuid):
        [Errno 2] No such file or directory: '/host/product_uuid'
INFO:agent:device_uuid computed: 9e382ac8c3ffb8e4e943a12e1b1329d20e7dcb04d58ee9cceb01cd6657fee970
INFO:agent:agent enrolled: agent_uid=fa38de1a-9ac0-4beb-bbb0-53306af707d7 heartbeat=300s
INFO:agent:data-plane mTLS listening on 0.0.0.0:8443 (client certificate required)
```

agent 的 graceful 行為**與設計完全相符**（`core/fingerprint.py` `_read_source()` 記
WARNING 後回空字串），指紋算得出來、雲端註冊正常。獨立驗算確認：
`sha256("" + "|" + "650cb79deaff4f5c964103fa4d064271")` = `9e382ac8…`，與 agent log 一致。

### 3.4 machine-id 到底是誰的——完整溯源

這是本次最花工夫查清的一條鏈，記下來避免下次重查：

| 層 | 值 | 說明 |
|----|-----|------|
| **Windows 主機** WMI `Win32_ComputerSystemProduct.UUID` | `18AC7619-82E9-41BF-876C-BE6BA6C865AA` | **容器讀不到**（原生 Windows 版才能用） |
| **Windows 主機** registry `MachineGuid` | `94c8473c-e394-48d8-8e87-fb4c3c54e8dc` | **容器讀不到** |
| `docker-desktop` WSL distro 的 `/etc/machine-id` | **不存在** | 從 WSL 側看是沒有的 |
| **docker VM（nsenter 進 PID 1 namespace）** `/etc/machine-id` | `650cb79deaff4f5c964103fa4d064271` | **← 容器實際掛到的就是這個**。是 symlink → `/var/lib/machine-id` |
| 該檔所在檔案系統 | `/dev/sdd` **ext4**（`docker_data.vhdx`，1007G） | **不是 tmpfs**——這決定了它在什麼操作下存活（見查證④） |

**結論**：Windows／Docker Desktop 形態下，agent 的身分實質綁在
**`docker_data.vhdx` 這顆虛擬磁碟**，而非 Windows 主機硬體或 OS 安裝。

### 3.5 T-7.2 規格：怎麼處理這條掛載

**建議做法（不做 workaround，符合 D9/D11/D17 立場）**：
出貨 **Windows 專用的 compose 檔**（`docker-compose.windows.yml`），內容與 Linux 版
唯一差異就是**沒有 product_uuid 那行掛載**，並在該處留註解說明「此來源在 Docker Desktop
不存在，非遺漏」。install.ps1 直接用它。

**為什麼不是「install.ps1 動態改 compose 檔」**：客戶機上被腳本改過的 compose 檔
與出貨版不一致，日後升級／除錯要先分辨「這行是我們改的還是客戶改的」。出兩份檔、
各自完整，比一份檔加改寫邏輯清楚。

**為什麼不做 host 值注入**：把 Windows 的 `MachineGuid` 寫成檔案掛進容器，即是
D9 廢除的 `collect-host-id.sh` 的 Windows 復刻（D11 排除方案②的理由原文適用），
且 D17 已明示「指紋漂移＝可接受維運事件，不做 workaround」。

**⚠️ 附帶效應（須寫進 T-7.3 指引的已知限制）**：同一台 Windows 機，若日後改用
原生 Windows 版（若解凍）或換成 Linux，指紋必然不同、需重新綁定——因為兩者的
身分來源根本不是同一個東西。

---

## 4. install.sh 為什麼在 Windows 上不能用（T-7.2 的存在理由）

160 上 `bash.exe` **存在**（`C:\WINDOWS\system32\bash.exe`）——但那是 **WSL 啟動器**，
不是原生 shell。即使進 WSL 跑，`install.sh` 仍過不了：

1. **第①步環境檢查硬擋**：`check_environment()` 對指紋二源做
   `[[ ! -e "$fp_src" ]] && check_fail`——WSL2 沒有 `/sys/class/dmi/id/product_uuid`，
   直接 `fail ... 2` 中止（退出碼 2）。這**不是**繞過檢查就好的問題，因為……
2. **docker context 不同**：WSL distro 內的 docker CLI 與 Windows 側 Docker Desktop
   engine 是不同 context，掛載路徑語意也不同（WSL 路徑 vs Windows 路徑）。
3. **`ip route get` 必錯**：WSL2 NAT 網路下偵測到的是 WSL 內部 IP（172.x），
   不是 Windows 主機的 `192.168.50.160`。
4. `install.sh` 另有 `require_root`（`EUID -eq 0`）、`/usr/local/bin` 維運入口複製、
   `/etc/guidant-agent-compose/install.conf` 安裝紀錄——三者在 Windows 都無對應語意。

**✅ 結論**：T-7.2 出 install.ps1 是必要的，不是「比較好看」的選擇。且**現有 install.sh
不需為 Windows 做任何相容性修改**（兩條路線各自完整，見 §3.5 同理）。

---

## 5. 查證②：網路與 8443

### 5.1 實測序列

| 步驟 | 結果 |
|------|------|
| 容器起來後，160 本機 `netstat` | `0.0.0.0:8443 LISTENING`（PID＝`com.docker.backend`）✅ **Docker Desktop 的 port 映射預設就綁全介面** |
| 160 本機 `Test-NetConnection 192.168.50.160:8443` | `True` |
| **從 188 連 160:8443** | ❌ **不通**（TCP 層即失敗） |
| 查防火牆 | 三個 profile 全 Enabled；`Get-NetFirewallPortFilter` 查 8443 → **無任何規則**；查 Docker 相關 inbound 規則 → **一條都沒有** |
| 加一條 inbound allow TCP 8443 | ✅ 188 → TCP 通、TLS 握手成功 |
| 雲端帶真 client cert 打 `/health` | ✅ **HTTP 200 `{"status": "ok"}`** |

### 5.2 兩個容易誤判的點

- **`curl -k` 回 `HTTP=000` 不代表壞掉**：8443 是 mTLS，不帶 client cert 本來就被拒。
  診斷要用 `openssl s_client` 看握手（實測可見憑證 CN＝本機指紋、issuer＝
  `CN = Guidant Agent CA (onprem-188)`），或直接帶雲端的 client cert 測。
- **Docker Desktop 不會自動開防火牆**：這點與 Linux 不同（Linux 版 docker 直接改
  iptables）。實測 160 上**沒有任何** Docker 建立的 inbound 規則。

### 5.3 T-7.2 規格

install.ps1 必須內建這一步（不能只寫在文件裡靠客戶手動做）：

```powershell
New-NetFirewallRule -DisplayName 'Guidant AI Agent data plane (8443)' `
  -Direction Inbound -Action Allow -Protocol TCP -LocalPort $DataPlanePort -Profile Any
```

建議：① 先查有無同名規則（重裝時 idempotent）；② 埠號跟著 `AGENT_DATA_PLANE_PORT`
走，不寫死 8443（與 install.sh 集中一處的作法一致）；③ 解除安裝時移除該規則。

**⚠️ 未實測**：`-Profile Any` 在客戶的網域群組原則（GPO）環境下可能被覆蓋或禁止建立。
企業環境要留「規則建立失敗時明確報錯並給出手動指令」的退路。

---

## 6. 查證③：本機對外位址自動偵測

**✅ 可用，語意等價**：

```powershell
(Find-NetRoute -RemoteIPAddress 192.168.50.188 | Select-Object -First 1).IPAddress
# → 192.168.50.160
```

與 Linux `ip route get <雲端IP>` 取 src 欄的語意完全相同（都是問核心「往這個目的地
會從哪個來源位址出去」）。

**T-7.2 規格**：
1. 沿用 install.sh 的既有邏輯——偵測值只當**預設值**，仍要讓客戶確認／覆寫
   （`_detect_local_ip` 的註解已說明：偵測是猜測不是事實，NAT／反向代理環境會錯）。
2. 偵測失敗回空、不中止（互動模式改問客戶；非互動模式才 fail）。
3. 組出的值要是 `https://<IP>:<埠>`，埠跟著 `$DataPlanePort`。
4. ⚠️ **不可**在 WSL 內偵測（會得到 172.x）——這是 PowerShell 原生做的理由之一。

---

## 7. 查證④：重置與重新開通

### 7.1 分層實測（成本遞增，找出指紋在哪一層才漂移）

| 層級 | 操作 | machine-id | 容器 | 結論 |
|------|------|-----------|------|------|
| **1** | `wsl --shutdown`（等同重開機／Docker Desktop Restart） | `650cb79d…` **不變** | 自動回來、healthy | ✅ 指紋存活，雲端仍是同一筆 agent（id=6，指紋 `9e382ac8…`），心跳續傳 |
| **1b** | RDP **斷線**（session 變 `Disc`） | 不變 | 續跑 | ✅ engine 存活 |
| **2** | **Factory reset 等效**：關閉 Docker Desktop → 刪 `%LOCALAPPDATA%\Docker\wsl\disk\docker_data.vhdx` → 重啟 | `650cb79d…` → **`83ca785e…`** ⚠️ | **image 與 volume 全清空** | ⚠️ **指紋漂移**：`9e382ac8…` → `8e579991…` |
| **3** | 使用者**登出** | — | **全滅** | 🔴 見 §8（新識別的自起缺口） |

**為什麼刪 vhdx 等於 factory reset**：`DockerCli.exe` 沒有 reset 子命令
（實測 `-h` 只有 `SharedDrives`／`Shutdown`／`SwitchDaemon`／`SwitchLinuxEngine`／
`SwitchWindowsEngine`／`Verbose`），GUI Settings→Reset 的資料層效果即是重建
`docker_data` 磁碟。實測後 image 與 volume 確實全空，與 GUI reset 行為一致。

### 7.2 重置後重新開通 SOP（實測驗證過）

**前提**：交付包目錄（含 `.env`、`docker-compose.yml`、`certs-in/`、`factory/`）
在 `C:\onepack\...` 這種**宿主檔案系統路徑**，不在 docker volume 內——所以 reset
**不會**動到它們。實測確認 reset 後 `.env` 與 compose 檔都還在。

```powershell
# ① 重新載入 image（reset 清掉了）
docker load -i .\guidant-ai-agent-<版本>.image.tar
docker load -i .\chrislusf-seaweedfs-3.99.image.tar

# ② 重新啟動（設定完全不用重填）
docker compose up -d
```

實測結果：兩容器 47 秒內 healthy，agent 自動重新註冊。

**⚠️ 但雲端會多一筆**：

```
 id |  name              | fp（前16碼）     | status |  last_seen_at
  6 | JEDI-WIN2025-01-AD | 9e382ac8c3ffb8e4 | active | 13:43:40   ← 舊的，不再心跳
  7 | JEDI-WIN2025-01-AD | 8e579991102671d0 | active | 14:02:23   ← 新的，活著
```

兩筆**同名**、`status` 都是 `active`（舊的沒有自動轉成 inactive／offline），
差別只在 `last_seen_at` 停在重置前。

**⚠️ 資料損失**：reset 清掉三個關鍵 volume——`agent-db`（SQLite，`upload_files`
對照表）、`agent-certs`（mTLS 憑證）、`seaweed-data`（**證據檔本體**）。
即「reset 前上傳的證據檔全部消失，且雲端那些 `upload_uid` 變成取不回的孤兒」。
這比指紋漂移嚴重得多，**必須寫進 T-7.3 指引的警告**。

### 7.3 T-7.3 指引須寫入的 SOP 與警告

1. **重置前**：Docker Desktop 的 Reset 會**刪除全部證據檔**，不是只重置設定。
   若需保留，先做 volume 匯出（工具未實作，屬缺口）。
2. **重置後**：兩行指令重開通（見 §7.2），**設定不用重填**。
3. **雲端側善後**：舊的那筆 agent 需人工處理（停用／刪除），否則清單裡兩筆同名。
   ⚠️ **未查證**：雲端是否有「停用舊 agent」的 UI 操作、以及舊筆是否會自動轉
   inactive——這屬 BE/FE 既有功能範圍，建議 T-7.3 前確認。
4. **哪些操作安全**：重開機、Docker Desktop Restart、`wsl --shutdown`、RDP 斷線
   ——皆**不會**動到指紋與資料。只有 Settings→Reset（及等效的刪 vhdx／重灌
   Docker Desktop）才會。

---

## 8. 🆕 新識別的擋路項：使用者登出 → agent 全滅（P0）

**T-4.3 評估文件完全未涵蓋此項**，但它是 Docker Desktop 路線最重的結構性限制。

### 8.1 實測

```
logoff <session>
→ quser: No User exists for *
→ Get-Process 'Docker Desktop','com.docker.backend' → 0 個
→ docker version → 連不上 npipe
→ 雲端 curl https://192.168.50.160:8443/health → HTTP=000（agent 離線）
```

容器的 `restart: unless-stopped` 在此**毫無作用**——不是容器掛了，是**整個 docker
engine 不存在了**。

### 8.2 兩條「無登入自起」路徑實測皆失敗

| 候選 | 實測結果 |
|------|---------|
| `com.docker.service` 設 `Automatic` 並啟動 | 服務 `Running`，但 `docker version` 仍連不上。該服務只是特權 helper，真正的 engine 在使用者態的 `com.docker.backend` |
| SYSTEM 身分的 AtStartup 排程跑 `Docker Desktop.exe` | 進程起得來（`SessionId=0`）但 **WSL distro 起不來**（`wsl -l --running` 為空），engine 永不就緒 |
| Interactive 身分排程（無登入 session 時） | 排程回報成功（`LastTaskResult=0`）但**進程數 0**——Interactive 排程在沒有互動 session 時無處可跑 |

### 8.3 為什麼這是 P0

落地版 agent 的部署想定是「裝在客戶的一台伺服器上、長期無人值守跑」。若前提變成
「必須有一個使用者始終保持登入狀態」，則：

- 伺服器重開機後（更新、停電）**agent 不會自己回來**，要有人登入才活
- 資安政策常要求閒置自動登出——那等於定期讓 agent 死一次
- 這與 systemd 版「開機自起、無人值守」的產品體驗有實質落差

### 8.4 可能解法（**皆未實測**，供 T-7.2 決策）

1. **自動登入專用帳號**（`autologon` + 該帳號 `HKCU\Run` 保留 Docker Desktop）
   ——最貼近「一直有 session」的做法，但要求機器保持一個常駐登入 session，
   資安上要客戶接受（等同該帳號的桌面永遠開著）。
2. **改用 Docker Engine on WSL2**（不用 Docker Desktop）＋WSL 的
   `systemd` 支援＋`wsl.exe --exec` 由排程觸發——繞開 Desktop 的 session 綁定，
   但**違背 D17「承載選 Docker Desktop、客戶不碰 Linux 層」的裁示**。
3. **接受此限制**，在 T-7.3 指引寫明「本形態需保持一個登入 session」，
   並提供「重開機後如何恢復」的 SOP。
4. **Windows 原生版**（D17 已裁示暫不討論）——此限制正是原生路線的價值之一。

🔴 **這是需要決策者裁示的項目，不是 runner 可自行選擇的實作細節**。建議在
T-7.2 開工前拍板，因為選 1 或 2 會實質改變 install.ps1 的形狀。

---

## 8A. 🆕 第二個新識別擋路項：宿主時鐘偏差 → 資料面全掛（2026-08-23 補記）

> 本節是 T-7.1 收工後、決策者實際操作雲端上傳時撞到的問題，當場查到根因，補記於此。

### 8A.1 症狀與根因

**症狀**：雲端「上傳檔案到 agent」失敗，UI 無明確錯誤。agent log：

```
WARNING:agent:data-plane JWT rejected on /blob: Signature has expired
ERROR:error_handler:Error in UnauthorizedError: 授權憑證無效
INFO:werkzeug:172.18.0.1 - - "POST /blob HTTP/1.1" 401 -
```

**根因**：160 的 Windows 時鐘與雲端相差 **+16 小時**（時區設在 `Pacific Standard Time`
＋CMOS 時鐘本身也快）。而資料面 JWT 的有效期是
**`exp ≈ now + 60s`**（`core/agent_auth.py:8`），且 `pyjwt.decode()` **未設 `leeway`**
（`core/agent_auth.py:99-105`，零容忍）。

**⚠️ 所以：宿主時鐘與雲端相差超過 60 秒，資料面（`/blob`、`/detection`）就 100% 失效。**
控制面（enroll、heartbeat、任務 ack/result）**不受影響**——那些是 agent 主動撥出、
用 mTLS 憑證，不驗 JWT `exp`。這造成一個很難查的表象：
**agent 在雲端顯示 active、心跳正常、檢測任務跑得完，唯獨上傳/下載檔案壞掉。**

### 8A.2 為什麼 Windows 特別容易踩到

Linux 版部署在客戶伺服器上，NTP 通常已由系統管理員配好。Windows 這台的實況：

- 時區預設值錯（`Pacific Standard Time`）——重灌／新機常見
- **這台是網域控制站（`DomainRole=5`）**，DC 預設**不對外找時間伺服器**，
  時間來源是 `Local CMOS Clock`（自己的主機板電池）。所以 GUI 按「立即同步」
  會顯示**成功**，但那是「跟自己同步」，時間永遠不會被修正——這是最誤導人的一點。
- 偏差達 16 小時時，`w32tm /resync` 會回
  「電腦並未 resync，因為沒有可用的時間資料」——**訊息完全不指向真正原因**
  （實際是 NTP 取得成功、但偏差超過內建上限而拒絕套用）。
  用 `w32tm /stripchart /computer:<ntp> /samples:2 /dataonly` 可證明 NTP 其實通、
  並讀出精確偏差（本次量到 `-57596.2s`）。

### 8A.2b 診斷軌跡（錯誤訊息會隨偏差方向改變，這是判定根因的關鍵線索）

| 階段 | log 錯誤 | 偏差 |
|------|---------|------|
| 發現時 | `Signature has expired` | 160 **快** 16 小時 |
| 修時區＋硬設時間後 | `The token is not yet valid (iat)` | 修過頭，160 **慢** 1.7 秒 |
| 精準對齊後 | 無錯誤，**上傳成功** | 容器**快**雲端 0.7 秒 |

**兩個方向都會 401**，且訊息不同——`expired` 是本機太快、`not yet valid (iat)` 是本機太慢。
看到這兩句其中任一句，都可直接判定是時鐘問題，不必往憑證／網路／儲存設定查。

⚠️ **容差極小**：`iat` 與 `exp` 皆 `leeway=0`，實測**慢 1.7 秒就被拒**。
最終停在「容器略快於雲端 0.7 秒」——刻意讓本機稍快，因為快一點只會少用掉一點
`exp` 的 60 秒額度，慢一點卻會直接撞 `iat`。

### 8A.3 修法（實測有效）

```powershell
# ① 時區
Set-TimeZone -Id "Taipei Standard Time"

# ② 時鐘（偏差過大時 w32tm 拒絕自動跳，直接設最快）
Set-Date -Date (Get-Date "<正確的當地時間>")
```

**⚠️ 還有第三步，容易漏**：Docker Desktop 的 Linux VM **有自己的時鐘**，
宿主時間跳動後**不會自動跟上**。實測本次 VM 時鐘反而是對的（`Aug 22 15:09 UTC`），
但若 VM 也偏了，需：

```powershell
docker run --rm --privileged --pid=host --entrypoint /usr/bin/bash <agent-image> `
  -c "nsenter -t 1 -m -u -i -n -- date -u -s '<正確 UTC>'"
```

🔴 **不要用 `hwclock -s`**——那是「從硬體時鐘讀回系統」，宿主 CMOS 本身錯的時候
會把 VM 的正確時間**覆蓋成錯的**（本次實測踩到，需再手動設回）。

**⚠️ VM 時鐘要單獨對**：本次實測宿主校到 +57ms 之後，**容器內仍慢 602ms**
（VM 與宿主是兩個獨立時鐘，宿主 `Set-Date` 不會傳遞下去）。必須對 VM 再做一次
`date -u -s`，容器才會跟上。install.ps1 的時鐘檢查若只驗宿主，會漏掉這一層。

**✅ 修完驗證**：容器快雲端 0.7 秒，決策者實際操作雲端上傳**成功**（2026-08-23）。

### 8A.4 T-7.2 規格輸入（建議列為必做）

1. **環境檢查加一項時鐘偏差檢查**：install.ps1 向 `CLOUD_ENDPOINT` 取
   HTTP `Date` 標頭（或呼叫一個雲端端點），與本機 UTC 比對；
   **偏差 > 30 秒即擋下並明確說明**（不是警告，是擋——因為裝完也不能用）。
   這比讓客戶事後撞到「上傳壞掉」再回頭查時鐘，成本低太多。
2. **錯誤訊息要指名道姓**：「本機時間與雲端相差 X 小時 Y 分，資料面認證會全部失敗。
   請先校時：`Set-TimeZone -Id "Taipei Standard Time"` 後 `w32tm /resync`」。
3. **T-7.3 指引加一節「校時」**，並註明 DC 的特殊情況（不對外同步、GUI 按同步無效）。
4. **考慮向雲端端反映**：`exp = now + 60s` 且 `leeway=0` 對落地部署過於嚴苛
   （客戶機時鐘漂移是常態）。**這屬雲端側設計，不在本卡 scope**，僅記錄供決策者評估
   ——若要放寬，`leeway` 給 30~60 秒即可大幅降低此類客訴，安全性影響很小。

### 8A.5 對「未實測假設」的更新

本節同時**實測到**了原 §11 第 6 條列為未查證的一項：
**雲端不會自動把離線 agent 轉狀態**——重置後遺留的舊 agent（id=6）
在離線 **9 小時**後仍顯示 `active`。決策者已手動刪除該筆。

---

## 9. 全鏈驗收證據

### 9.1 重置前（agent id=6，指紋 `9e382ac8…`）

```
POST /api/1.0/agents/tasks/5f0e3e63-…/ack    → 200
POST /api/1.0/agents/tasks/5f0e3e63-…/result → 200
INFO:agent:task 5f0e3e63-… 執行成功，上傳 1/1 份報告
```
雲端 `compliance.agent_tasks` id=13：`succeeded`，
`result_ref` → `Nmap掃描報告_192.168.50.123_20260823.html`

### 9.2 重置後（agent id=7，指紋 `8e579991…`）

雲端 `compliance.agent_tasks` id=14：`succeeded`，同樣產出報告。

### 9.3 證據落點確認

- SeaweedFS 容器內 `/data` 有 `guidant-evidence_10.idx` 等 volume 檔
- agent SQLite 在 `/data/db/agent.db`
- ✅ **證據檔存在 160 本機**，未上傳雲端（符合設計）

### 9.4 顯示名稱

`AGENT_HOSTNAME=JEDI-WIN2025-01-AD` → 雲端 `remote_agents.name` 正確顯示機器名
（非容器 ID）。compose 的 `hostname:` 設計在 Windows 同樣生效。

---

## 10. T-7.2 規格輸入彙總（本卡的主要交付）

| # | 規格 | 來源 |
|---|------|------|
| 1 | 出 **Windows 專用 compose 檔**（無 product_uuid 掛載），不動態改寫客戶的檔 | §3.5 |
| 2 | 環境檢查**不可照抄** install.sh 的指紋二源硬擋；改查「Docker Desktop 裝了沒／engine 通不通／版本」 | §3.1、§4 |
| 3 | 內建**防火牆規則建立**（idempotent、跟著埠號、解安裝時移除、失敗要明確報錯） | §5.3 |
| 4 | 第三題預設值用 `Find-NetRoute`，偵測值只當預設仍需確認 | §6 |
| 5 | 解包用內建 `tar.exe`；sha256 用 `Get-FileHash` 並 `.ToLower()` 比對 | §2 |
| 6 | `docker load` 要有進度提示（約 6 分鐘） | §2 |
| 7 | **流程中不可有任何 registry pull**（credential helper 在非互動 session 會炸） | §2.1 |
| 8 | **自起策略需決策者裁示後才能定案** | §8 |
| 9 | 維運子命令（status/logs/restart/fingerprint/configure-storage…）在 Windows 的載體 | T-4.3 §3 建議 binary 子命令化；本卡未實測 |
| 10 | TOFU 憑證確認的 PowerShell 實作（本卡走預先放 `certs-in/cloud-ca.pem` 的權宜路徑，**未實測互動式指紋確認**） | ⚠️ 未實測 |

---

## 11. 未實測假設（逐條列出，不當成已驗）

1. **TOFU 互動式憑證確認流程**——本卡直接把 `cloud-ca.pem` 放進 `certs-in/`
   （⚠️探路權宜），**未實測** PowerShell 抓憑證→印 SHA-256 指紋→人工確認→存錨的流程。
2. **`upgrade` 換版路徑**——未測。同一顆 onepack 在 Windows 的升級行為（資料存活）未驗。
3. **`configure-storage` 恢復循環**——未測（內建→自有→恢復內建）。
4. **維運子命令**（status/logs/fingerprint 等經 `docker compose exec` 轉呼叫）——未測。
5. **防火牆規則在 GPO 網域環境**能否建立成功——160 在網域內但未測 GPO 限制情境。
6. **雲端側舊 agent 的善後**——重置後舊筆是否會自動轉 inactive、UI 有無停用操作，未查。
7. **§8.4 的四個自起解法**——皆未實測，僅列為候選。
8. **`docker_data.vhdx` 以外的 reset 路徑**（GUI Settings→Reset 本尊、重灌 Docker Desktop）
   ——未實測，推定行為一致（同樣重建該磁碟）。
9. **Windows 11 上的行為**——本機是 Server 2025。推定一致（同一個 Docker Desktop），未驗。
10. **agent 與 WinRM 受測角色長期並存**——本次短時間並存無異狀，未做長時間觀察。

---

## 12. 160 機上的探路殘留物（請決策者裁示是否清除）

| 項目 | 位置 | 用途 | 建議 |
|------|------|------|------|
| 交付包與解開的目錄 | `C:\onepack\` | 安裝來源＋現行安裝目錄 | **保留**（T-7.2 要繼續用）。`.tar.gz`（1.4GB）可刪 |
| 執行中的 agent | 兩個容器 | 現正註冊在 DEV 雲端（agent id=7） | 依 T-7.2 排程決定 |
| 防火牆規則 | `Guidant AI Agent data plane (8443)` | 查證② 建立 | **保留**（T-7.2 要用） |
| 排程工作 | `T71-StartDockerDesktop` | 探路時從 SSH 起 Docker Desktop 用 | **可刪**（探路權宜，非產品的一部分） |
| 188 的 SSH 公鑰 | `C:\ProgramData\ssh\administrators_authorized_keys` | 讓 188 免密 scp 傳包 | 依安全政策決定；T-7.2 若還要傳檔則保留 |
| 探路用小腳本 | `C:\onepack\probe.sh`、`calc.sh`、`nsprobe.sh`、`nsprobe2.sh` | 指紋溯源用 | **可刪** |
| DEV 雲端測試資料 | `agent_tasks` id=13/14、`remote_agents` id=6/7 | 全鏈驗收證據 | 建議保留至 T-7.3 驗收後一併清 |

**⚠️ 目前狀態**：本次探路結束時 Docker Desktop 因無登入 session 而未運行
（見 §8），**agent 目前離線**。要恢復需有人登入 160，或由 SSH 起
`Docker Desktop.exe -Autostart`（但該進程會隨 SSH session 結束而死）。

---

## 13. 對 T-4.3 評估文件的修正建議

`windows-feasibility.md` 是**評估**、本文件是**實測**，兩者衝突處以本文件為準。
建議在 T-4.3 文件 §2.2 加一行指向本文件的註記（不改寫原文——那是當時的推理紀錄，
有保留價值），指出：

1. §2.2 的「docker 自動建空目錄 → 靜默算錯」後果鏈**未發生**，實際是啟動硬失敗（§3.2）。
2. §0 結論總表「④docker 過渡模式：卡點等級**高（指紋）**」——實測後，指紋本身的
   卡點等級應**下修為中**（硬失敗、可預期、處理方式明確），但**新增一項「自起」
   為高**（§8）。
3. §2.3 的「待實測項：`ip route get` 在 WSL2 必錯」——正確，但正式路線根本不進 WSL，
   改用 PowerShell `Find-NetRoute` 即無此問題（§6）。
