---
title: FR-193 Docker rootless — 設計 v1.3（Agent 先行）
brand: Guidant AI · **FR-193** Docker rootless
eyebrow: FR-193 · design v1.3 · 2026-11-11
h1: Agent 主機改 rootless Docker
subtitle: 主機準備層、docker 呼叫切身分、uid 換算、升級重驗；主系統後續另拆
lede: "Agent 安裝包改成以專用帳號 `guidant` 跑 rootless Docker。程式碼與 image 不動，改動全在 `deploy/install.sh` 與 `scripts/install/lib/`。十張卡七批，第一棒乾淨機手動踩坑，第二批起改安裝程式。"
chips: [{text: v1.3 已被 v1.4 取代, kind: plain}, {text: Agent 先行, kind: accent}, {text: 10 卡 7 批, kind: plain}]
footer: 討論與決策沿革見 discussion.md 與 git log；相對 v1.0 的差異見末節
---

> **本版已被 [design-v1.4.md](design-v1.4.md) 取代**（A4／A5／A6 合併成一張卡，設計內容不變）。此檔凍結不改。

# FR-193 設計 v1.3（已被 v1.4 取代）

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

**整案一句話**：Agent 被掃主機上的 Docker 引擎改成普通帳號 `guidant` 執行，三顆容器全由它開；客戶照常 `sudo ./install.sh`，多出來的主機設定全由安裝程式做。

| 階段 | 做什麼 | 產出 | 完成怎麼判定（決策者檢查法） |
|---|---|---|---|
| P0 探路 | 乾淨機手動裝 rootless，列壞掉清單、量吞吐 | 證據頁 `evidence/agent-rootless-probe.md` | 打開證據頁，看得到十二項驗證清單每項的實際輸出、上傳 100 MB 的秒數、traceroute 兩種模式的結果 |
| P1 改安裝程式 | 主機準備層、Docker 三段式安裝、docker 呼叫切身分、uid 換算、升級回滾重驗、收尾小項 | `install.sh` 與 `lib/` 四支新檔 | 乾淨機 `sudo ./install.sh` 一路到底不手動介入；裝完 `ps -eo user,comm \| grep dockerd` 沒有 root；重開機 Agent 自起；主系統發更新後 Agent 換版成功 |
| P2 收口 | 手冊、資安報告、出貨包總驗 | README 兩節、SR-09.5 改寫、兩個 OS 的驗收證據頁 | 拿出貨包在 Ubuntu 24.04 與 Debian 12 各裝一次，`audit_nonroot_stack.sh --agent` 全 ✔，主系統派掃描收結果正常 |

**依賴**：決策者功課只有一件——A0 需要一台乾淨 Ubuntu 24.04 VM（有 TPM 2.0、能連 DEV 主系統）。主系統那套之後開 FR-193.1，照這份的做法抄。

## 1. 決策定案表 {#decisions nav="決策"}

| 編號 | 定案 | 理由 | 被排除 |
|---|---|---|---|
| D1 | 對外一律 8443，不貼能力、不改 sysctl；80 的 http 轉址拿掉 | PM 2026-11-10 裁：還沒出給客戶過，改預設沒有相容成本；省掉 capability 維護與主機掃描解釋 | 預設 443 貼 `rootlesskit` 能力（v1.0 路三）；全機 sysctl。**只影響主系統，本份不做，列入 FR-193.1** |
| D2 | ping sysctl 全開 `0 2147483647` | 與 Ubuntu／RHEL 預設相同；放寬的只有核心代填的 ICMP echo | 只開 `guidant` 子 gid 區段（每台數字不同、客戶會蓋掉） |
| D3 | 網路驅動用預設 `slirp4netns` | Agent 流量單向上傳、對吞吐不敏感；A0 留數字 | `pasta`（實驗性，吞吐不夠才換）；`bypass4netns`（要自訂 seccomp） |
| D4 | 出貨包含 Docker Engine 完整一組，install.sh 三段式 | 客戶拿到包就能裝比省 500 MB 重要 | 只包 rootless 附加三支 |
| D5 | 帳號名預設 `guidant`，設定檔 `GUIDANT_RUN_USER` 可改；已存在就沿用並檢查三項 | 有些客戶帳號命名有規範；撞名不能死 | 固定名稱直接 `useradd` |
| D6 | 三支升級 systemd 單元仍以 root 跑，docker 操作經 `DOCKER_HOST` 指到 `guidant` 的 daemon；`Wants`／`After` 都改等 `user@<uid>.service` | staging 目錄只有 root 進得去、簽章驗證用的是 root 持有的現行版執行檔，整支改 user unit 會失去這層隔離 | 整支改成 `guidant` 的 user unit |
| D7 | 不留 root Docker 相容路徑 | 沒有舊客戶（FR-180 同一判準） | 偵測模式雙路徑 |
| D9 | docker 呼叫一律是 root 的 CLI 加 `DOCKER_HOST=unix:///run/user/<uid>/docker.sock`，install.sh 開頭 export 一次；不用 `sudo -u guidant` | 實測 `sudo -u` 會讓三個基本動作失敗（從映像取檔到 root 的暫存目錄、讀 staging 目錄的映像檔、讀 `/run/guidant-agent/.env`）；daemon 仍是 `guidant`，容器拿到的權限沒有變多。`/run/guidant-agent` 因此維持 root 700 不開放 | `sudo -u guidant` 包一層（v1.2） |
| D10 | 維運程式不從 `guidant` 的映像庫取來執行：安裝與升級時把 `install-lib` 存一份到 `/usr/local/lib/guidant-agent/install-lib`（root 755），維運子命令與升級單元讀這份；升級時新版的 lib 在驗章通過後才換上 | rootless 後映像庫屬 `guidant`，root 去 source 映像裡的腳本等於執行 `guidant` 改得到的東西。主機副本的做法 `secrets_cmds.sh` 與 `upgrade_order.sh` 已在用 | 維持從映像讀（root Docker 時代的做法） |
| D11 | 容器內 ping 靠 compose 的容器 sysctl `net.ipv4.ping_group_range: "1000 1000"`，`cap_add: [NET_RAW]` 保留；主機 sysctl 照 D2 全開 | rootless 的容器網路不帶主機的值，容器內預設是「沒有人」；映像裡的 ping 帶檔案能力，拿掉 NET_RAW 連執行都失敗 | 拿掉 NET_RAW 只靠主機 sysctl（v1.2） |
| D8 | **終態一律官方 docker-ce，`docker.io`／`podman-docker` 不視為合格的 Docker**。發行版自帶的：沒有任何容器與 volume 就移除、換裝官方組；有就停下列清單請客戶處理 | `docker.io` 沒有對齊版本的 rootless 附加套件、compose plugin 另裝且版本凍結、與出貨包那組不一致出問題無從重現；判準「有沒有人在用」，有就不代客戶決定 | 一律停下（乾淨機裝了 docker.io 就卡住）；一律移除（可能砍掉客戶在用的） |

## 2. 現況與目標 {#now nav="現況"}

### 2.1 三層身分

| 層 | 現況 | 目標 |
|---|---|---|
| 容器裡的程式 | uid 1000（FR-180 做完） | 不動 |
| Docker 引擎 | 主機 root | 主機帳號 `guidant` |
| 容器 uid 在主機上的實際身分 | 1000 就是 1000 | 子 uid 起點 + 1000（如 101000），每台不同 |

### 2.2 現況接入點盤點

| 元件 | 現況 | 本案動作 |
|---|---|---|
| `deploy/install.sh` `check_environment()` | 只查 docker 在不在、版本、compose plugin | 加 cgroup v2／systemd／核心版本；Docker 三段式（沒有→從包裝；夠新→沿用補附加；太舊→停） |
| `deploy/install.sh` `require_root()` | 全程 root | 不變（安裝程式整支仍 sudo 跑），但 docker 操作內部切身分 |
| `deploy/install.sh` 全部 docker 呼叫（60 處，另 `lib/` 21 處） | root CLI 連 `/var/run/docker.sock` | install.sh 開頭 export `DOCKER_HOST`（D9），既有呼叫不逐行改 |
| `deploy/install.sh:2286` `AGENT_UID=1000`、`:2333` `_storage_key_owner`、`:2358` `prepare_agent_host_paths`（product_uuid／content/cache／factory-defaults.env） | 主機側 chown 給 1000 | 全部走換算；重跑 install.sh 不得蓋回 1000 |
| `lib/upgrade_order.sh` `_UO_APP_ID=1000`、`:91` chown | 升級單目錄 | 走換算 |
| `lib/local_admin.sh:28` `AGENT_UID:-1000` | volume 修 owner | 走換算 |
| `lib/secrets.sh` `SEC_RUN_DIR` 700 | `/run/guidant-agent` root 700 | 不動（D9：讀它的是 root 的 CLI） |
| `lib/systemd/*.service` 三支＋secrets 單元 | root、`Wants`／`After`／`Before=docker.service` | 仍 root；三個欄位都改成 `user@<uid>.service`；`Environment=DOCKER_HOST=…` |
| 維運子命令與升級單元取 `install-lib` | `source <(image_read_file …)` 從映像讀 | 讀主機副本（D10） |
| `check_environment` 的錯誤訊息與磁碟檢查 | 叫客戶 `systemctl start docker`、查 `/var` | 訊息改掉；磁碟查 `guidant` 的資料目錄 |
| `deploy/docker-compose.yml:162` `cap_add: [NET_RAW]` | 給 ping | 保留，另加容器 sysctl（D11） |
| `scripts/build/build_agent_bundle.sh` | 帶 tpm2 debs | 多帶 Docker 一組（`fetch_docker_debs.sh`） |
| 主機 OS | 無 | 六項設定（§3.1） |

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

### 3.1 主機準備層 `lib/rootless_host.sh`（A1）

install.sh 第①步環境檢查之後、寫設定之前呼叫 `rootless_host_prepare`，冪等，重跑無害。

1. **前提**：`stat -fc %T /sys/fs/cgroup` 要 `cgroup2fs`；`systemctl --version` 有輸出；`uname -r` ≥ 5.11。任一不符 `check_fail`。
2. **Docker 四段式**（D8）：先辨識來源——`dpkg -s docker-ce`／`rpm -q docker-ce` 有＝官方；`dpkg -s docker.io`、`podman-docker` 有＝發行版自帶。①沒有 docker → 從包 `debs/docker/<目標>/` 裝（deb 用 `dpkg -i` 加 `apt-get -f`，el-9 把目錄當本機 repo 給 dnf）。②官方且 `Server.Version` ≥ `MIN_DOCKER_MAJOR` → 只補 `docker-ce-rootless-extras`、`uidmap`、`slirp4netns`、`dbus-user-session`。③官方但太舊 → `fail` 印「請自行升級或移除後重跑」。④發行版自帶 → `docker ps -aq` 與 `docker volume ls -q` 都空 → 印一行「移除發行版自帶 docker.io，改裝官方 Docker Engine」後 `apt-get remove -y docker.io docker-compose docker-doc`（el：`podman-docker`），接①；任一非空 → `fail` 印出容器與 volume 清單，請客戶確認後自行移除再重跑。裝完 `systemctl disable --now docker.service docker.socket containerd.service`（containerd 是獨立單元，不停會留一支 root 行程）。
3. **帳號**（D5）：`GUIDANT_RUN_USER` 預設 `guidant`。不存在 → `useradd --system --create-home --shell nologin`。不論新建或既有都查 subuid：Ubuntu 24.04 的 `useradd --system` 不配，且 `usermod` 不檢查重疊。沒有就補，起點＝`/etc/subuid` 與 `/etc/subgid` 所有區段 `start+count` 的最大值（存在 → 同樣檢查、shell 是否 nologin（否則 warn 不改）、是否在 sudo／wheel 群組（是則 `fail`）。
4. **linger**：`loginctl enable-linger`。
5. **sysctl**：寫 `/etc/sysctl.d/90-guidant-rootless.conf` 一行 `net.ipv4.ping_group_range = 0 2147483647`，`sysctl --system`。
6. **cgroup 委派**：寫 `/etc/systemd/system/user@.service.d/delegate.conf`（`Delegate=cpu cpuset io memory pids`），`daemon-reload`；user manager 已在跑的話 `systemctl restart user@<uid>.service`，等 socket 回應（最多約 30 秒）。不能只 `terminate-user`，linger 不會把它拉回來。
7. **啟用 rootless**：`machinectl shell guidant@ /bin/bash -c 'XDG_RUNTIME_DIR=/run/user/$(id -u) dockerd-rootless-setuptool.sh install && systemctl --user enable docker'`。沒有 `machinectl` 就裝 `systemd-container`。
8. **驗**：`machinectl shell` 不回傳內層結束碼，成敗只看這一步。以 `docker info --format '{{.SecurityOptions}} {{.CgroupDriver}}'` 要含 `rootless` 與 `systemd`，否則 `fail`。

### 3.2 docker 指到 guidant 的 daemon（A3、D9、D10）

install.sh 開頭（讀到 `GUIDANT_RUN_USER` 之後）：

```bash
RUN_UID="$(id -u "$GUIDANT_RUN_USER")"
export DOCKER_HOST="unix:///run/user/${RUN_UID}/docker.sock"
```

之後所有 `docker`、`dc()` 原樣可用，不必逐行改。維運入口與三支升級單元是另外的行程，各自要拿到同一個值：維運入口在同一段讀安裝紀錄後 export；單元檔加 `Environment=DOCKER_HOST=…`（安裝時代入 uid）。守衛 `scripts/install/check_docker_host.sh`：每支會被獨立執行的入口都要有這一行 export。

主機副本（D10）：`/usr/local/lib/guidant-agent/install-lib/`，root 755。首裝在映像驗完完整性後存一份；升級在 `verify_new_image_tree` 通過後換上新版。所有 `source <(image_read_file … install-lib/…)` 改讀這份。既有的 `/usr/local/lib/guidant-agent-secrets` 與 `guidant-agent-upgrade` 併進同一個目錄。

### 3.3 uid 換算 `lib/subuid_map.sh`（A4）

```bash
subuid_base() { awk -F: -v u="$GUIDANT_RUN_USER" '$1==u{print $2; exit}' /etc/subuid; }
host_uid_for() { echo $(( $(subuid_base) + $1 )); }   # host_uid_for 1000 → 101000
```

rootless 的對映是 `0 → guidant 本人`、`N → 起點 + N − 1`（N≥1）。主機側六處走換算：`_storage_key_owner`、`prepare_agent_host_paths` 的 `product_uuid`／`content/cache`／`factory-defaults.env`、`upgrade/agent-order`、`AGENT_UID` 常數本身。`product_uuid` 設 `guidant:<容器 1000 的主機 uid>` 0440，服務與 `fingerprint` 子命令（容器內 uid 0）才都讀得到；只設群組的話兩邊算出不同指紋，主系統會多一台陌生 Agent 且沒有任何錯誤。容器內以 `-u 0` 跑的 chown 1000 不改（容器裡看到的仍是 1000）。重跑 install.sh 不得把擁有者蓋回 1000。

### 3.4 升級單元（A5、D6）

三支單元維持 `User=root`。`Wants=docker.service` 與 `After=docker.service` 都改成 `user@<uid>.service`（留著 `Wants=docker.service` 會在升級單觸發時把 root Docker 拉起來）；secrets 單元的 `Before=docker.service` 同樣改。單元檔帶 `Environment=DOCKER_HOST=…`。staging 目錄仍 root 700，解包、驗簽在 root 做完才 `docker load`。升級腳本偵測到容器屬於 root Docker 時停下，印「本版起改 rootless，請 uninstall 後重新安裝」（D7）。

### 3.5 出貨包（A2）

`fetch_docker_debs.sh` 照 `fetch_tpm2_debs.sh`：同一組目標 `ubuntu-22.04 ubuntu-24.04 debian-12 el-9`，在對應版本最精簡容器加 Docker 官方 repo 後 `apt-get install --download-only`／`dnf download --resolve`，輸出 `debs/docker/<目標>/`。釘版寫在腳本頂部一個變數，bump 走 commit。平台固定 `linux/amd64`。

## 4. 主機六項設定一覽 {#host nav="主機設定"}

| # | 設定 | 落在哪 | 全機生效 | uninstall 還原 |
|---|---|---|---|---|
| 1 | `guidant` 帳號＋子 uid 65536 | `/etc/passwd`、`/etc/subuid`、`/etc/subgid` | 多一個帳號 | `userdel -r` |
| 2 | linger | `loginctl` | 否 | `disable-linger` |
| 3 | Docker Engine＋rootless 附加套件；系統層 docker／containerd 三支單元停用 | 套件、systemd | 是 | 套件不移除（客戶可能另有用途），印提示；單元恢復原狀 |
| 4 | `ping_group_range` | `/etc/sysctl.d/90-guidant-rootless.conf` | 是 | 刪檔、`sysctl --system` |
| 5 | cgroup 委派 | `/etc/systemd/system/user@.service.d/delegate.conf` | 是 | 刪檔、`daemon-reload` |
| 6 | 資料目錄 owner（六處換算） | `/srv/guidant-ai-agent` | 否 | 隨資料目錄 |

## 5. 不做什麼 {#out nav="不做"}

- 主系統那套（對外改 8443、四支單元、資料庫目錄 owner）→ FR-193.1，等 Agent 這輪踩完坑再開。
- 容器內任何改動、image 重打（FR-180 的成果沿用）。
- 多 Agent 同主機、非 x86_64。
- `pasta` 驅動切換（A0 量出吞吐不夠才開卡）。

## 6. 拆卡表 {#cards nav="拆卡表"}

批＝可同時開 session 的一組；階段＝給 PM 看的分組。A1 到 A6 都在同一台 VM（192）上實測、都改 `deploy/install.sh`，所以除了 A1／A2、A5／A6 兩組之外一張一批。A0 已完成。由 `handoff/cards.py` 產生並驗過依賴與撞檔。A8 在 BE repo（資安報告），其餘在 Agent repo。

| 卡 | 階段 | 批 | Repo | 做什麼 | reuse／extend／new＋理由 | 改哪些既有檔 | 新開檔 | 依賴 | 驗收 | Model／Effort |
|---|---|---|---|---|---|---|---|---|---|---|
| A0 | P0 | 1 | agent | 乾淨機手動跑 rootless：列壞掉清單 | **new**：不寫程式；後面的卡按實測清單校正 | — | `docs/features/FR-193-2611-docker-rootless/evidence/agent-rootless-probe.md（BE repo，證據）` | — | 證據頁有壞掉清單（訊息→原因→修法）、十二項輸出、traceroute 兩種模式結果 | sonnet／medium |
| A1 | P1 | 2 | agent | rootless 主機準備層：四段式 Docker 安裝、建帳號與子 uid、linger、sysctl、cgroup 委派、啟用 rootless | **new**：現有 install.sh 沒有這層，獨立一支 lib；離線裝法照 lib/tpm.sh | `deploy/install.sh（check_environment 段）`、`scripts/build/build_agent_bundle.sh（lib 複製清單）` | `scripts/install/lib/rootless_host.sh` | A0 | 把 192 還原成沒有 Docker、沒有 guidant 帳號後，單獨執行 rootless_host_prepare：六項主機設定到位、docker info 含 rootless 與 systemd、ps 無 root 的 dockerd／containerd；重跑一次無變化 | opus／high |
| A2 | P1 | 2 | agent | 出貨包帶 Docker Engine 完整一組（每個 OS 版本一份） | **extend**：照 BE fetch_tpm2_debs.sh；打包點是 build_compose_bundle.sh 第 396 行 tpm2 那段旁邊 | `scripts/build/build_compose_bundle.sh` | `scripts/build/fetch_docker_debs.sh` | A0 | 四個目標目錄各有完整套件；el-9 含 repodata；各起一個對應版本的乾淨容器離線裝得起來；image-manifest.sha256 有列 | sonnet／medium |
| A3 | P1 | 3 | agent | docker 指到 guidant 的 daemon：DOCKER_HOST 一次 export、主機準備層接進安裝流程、維運程式改讀主機副本 | **extend**：D9：export 一次，既有 81 處呼叫不逐行改；D10：主機副本的做法 secrets_cmds.sh 與 upgrade_order.sh 已在用，併成一個目錄 | `deploy/install.sh`、`scripts/install/lib/upgrade_order.sh`、`scripts/install/lib/secrets_cmds.sh` | `scripts/install/lib/host_lib.sh`、`scripts/install/check_docker_host.sh` | A1 | 192 上把 lib 與 install.sh 換進解開的包後 sudo ./install.sh 走到底、不帶任何環境變數；status／logs／start／stop／restart 正常；維運子命令不再從映像 source；ps 無 root 的 dockerd | opus／high |
| A4 | P1 | 4 | agent | 主機側擁有者六處換算成子 uid | **new**：換算獨立一支；公式 起點+N−1（A0 實測）；/run/guidant-agent 不動（D9） | `deploy/install.sh（AGENT_UID、_storage_key_owner、prepare_agent_host_paths）` | `scripts/install/lib/subuid_map.sh` | A3 | 重跑 install.sh 兩次擁有者都是換算後的值；三顆容器 healthy；服務 log 的 device_uuid 與 fingerprint 子命令印的一致 | sonnet／medium |
| A5 | P1 | 5 | agent | 升級全程 rootless：單元的 Wants／After 與 DOCKER_HOST、升級單目錄擁有者、舊版偵測 | **extend**：D6：單元仍 root；用維運指令 upgrade <包> 驗，不必接主系統 | `scripts/install/lib/upgrade_order.sh`、`scripts/install/lib/upgrade_runner.sh`、`scripts/install/lib/systemd/guidant-agent-upgrade-order.service`、`scripts/install/lib/systemd/guidant-agent-upgrade-boot-check.service`、`scripts/install/lib/systemd/guidant-agent-secrets.service` | — | A3、A4 | 192 上 guidant-agent-compose upgrade <同一包改版號> 換版成功；過程中 systemctl is-active docker 始終 inactive；升級單目錄容器可寫；硬重開後 boot-check 正常 | opus／high |
| A6 | P1 | 5 | agent | compose 加容器 ping sysctl；uninstall 還原主機設定 | **extend**：D11：NET_RAW 保留、加一行 sysctls；teardown 函式加在 A1 的 lib | `deploy/docker-compose.yml`、`deploy/install.sh（cmd_uninstall 段）`、`scripts/install/lib/rootless_host.sh` | — | A3、A4 | 容器內程式 fork 出的 ping／mtu／traceroute／traceroute -I 四項都通；uninstall 後帳號、sysctl 檔、委派檔、linger 清掉，docker／containerd 單元恢復原狀 | sonnet／medium |
| A7 | P2 | 6 | agent | deploy/README：裝機改了哪些主機設定、IT 怎麼看容器、常見錯誤 | **extend**：底稿是 Claude Docs 手冊加 A0 證據頁第 7 節的七處修正 | `deploy/README.md` | — | A5、A6 | README 新增兩節；前置需求補 cgroup v2／systemd；常見錯誤表併入；只寫現況 | sonnet／medium |
| A8 | P2 | 6 | main | 資安報告 SR-09.5 改寫為現況：Agent 端採用 rootless | **extend**：只改一段；依「資安報告只列我們交出去的東西」，主機設定細節留在手冊 | `docs/security-report/requirements/SR-09-agent.md` | — | A5、A6 | SR-09.5 讀起來是現況；grep 不採用／原本／改判 無命中 | sonnet／medium |
| A9 | P2 | 7 | agent | 出貨包全流程總驗：新裝、重開機、升級、uninstall（需決策者先出測試包） | **new**：唯一需要真包與主系統的一張；測試包由決策者在 GitLab 按，小弟不自己出包 | — | `docs/features/FR-193-2611-docker-rootless/evidence/agent-rootless-acceptance.md（BE repo，證據）` | A2、A7 | 192 還原成乾淨後用測試包 sudo ./install.sh 到底；audit_nonroot_stack.sh --agent 全 ✔；十二項過；主系統派掃描收結果、發更新換版成功；uninstall 清乾淨 | opus／high |

## 7. 端到端驗收（結案時首腦操作） {#e2e nav="驗收"}

1. 拿 A2 出的真包，乾淨 Ubuntu 24.04 與 Debian 12 各一台（有 TPM），`sudo ./install.sh` 到底，中間不手動介入。
2. `scripts/build/audit_nonroot_stack.sh --agent` 全 ✔；`ps -eo user,comm | grep -E 'dockerd|containerd'` 無 root。
3. 手冊第 5 節十二項逐項過。
4. 主系統核准 Agent → 派一次掃描 → 收到結果；網路診斷 ping／traceroute／mtu 三項有值。
5. 主系統發 Agent 更新 → 新版起來；中途拔電重開 → boot-check 退回舊版。
6. `uninstall` 後 `grep guidant /etc/passwd /etc/subuid` 空、sysctl 回預設、無 dockerd。

## 8. 相對上一版改了什麼 {#delta nav="版本差異"}

### v1.3（相對 v1.2）

A0 在乾淨 Ubuntu 24.04 實測後修正五處，證據在 `evidence/agent-rootless-probe.md`。

- **D9 新增**：docker 呼叫改成 root CLI 加 `DOCKER_HOST`。v1.2 的 `sudo -u guidant` 實測讓 install.sh 三個基本動作失敗。連帶 `/run/guidant-agent` 不必開放給 `guidant`。
- **D10 新增**：維運程式改讀主機副本，不從 `guidant` 的映像庫取來以 root 執行。
- **D11 新增**：NET_RAW 保留、加容器 sysctl。v1.2 寫「拿掉 NET_RAW」實測連 ping 都起不來；`traceroute -I` 不必改。
- **§3.3 換算公式**：`起點 + N − 1`，不是 `起點 + N`；換算點從三處變六處（v1.2 漏了 `prepare_agent_host_paths`）。
- **§3.1、§3.4**：containerd 一起停、子 uid 起點自己算、委派後重起 user manager 並等 socket、`Wants=docker.service` 也要換。

卡仍是十張，批從六變七：A3 與 A4 都改 `deploy/install.sh` 且共用同一台 VM，拆成前後兩批。A3、A4、A5、A6 的做法改寫；A1 到 A6 的驗收改成「把改過的檔換進 192 上解開的包」，不必每張卡出一次包；A9 的測試包由決策者出。

### v1.2（相對 v1.1）

新增 **D8** 發行版自帶 Docker 的處理。決策者 2026-11-10 問「Ubuntu 內建的會不會移除」，首腦查到 v1.1 的三段式會讓裝了 `docker.io` 的主機撞牆：版本號過門檻被判「沿用」，但 `docker.io` 沒有 rootless 附加套件、與官方 extras 互斥。改四段式：發行版自帶且沒有容器沒有 volume 就移除換官方組，有就停下列清單。改到 §1 D8、§3.1 第 2 步、§6 A1 驗收；卡的批次不變。

### v1.1（相對 v1.0）

**只改 D1**。PM 2026-11-10 裁示：主系統對外一律 8443，不走「預設 443 貼能力、客戶可切」的路三。理由是產品還沒出給客戶，改預設沒有相容成本；省下 `rootlesskit` capability 的升級重貼與主機掃描報告解釋。

這條只影響主系統（FR-193.1），Agent 這輪十張卡的內容與批次完全不變。FR-193.1 開卡時要涵蓋：

| 要改 | 在哪 | 說明 |
|---|---|---|
| 預設值 | `scripts/installer/install.sh:102-103`、`install.conf.example:35-36`、`docker/production/docker-compose.yml:549-550` | HTTPS 預設 8443；HTTP 80 整條拿掉（80 也是低埠） |
| 網址判斷 | `install.sh:2671` `if GUIDANT_HTTPS_PORT == 443` | 刪掉分支，`SITE_URL` 一律帶 port |
| http 轉址 | FE `nginx.onprem.conf:134-137` 的 8080 server 段 | 拿掉；compose 不再映射 80 |
| Agent 註冊網址 | Agent `install.sh` 問主系統網址那題 | 預設值範例改 `https://<主機>:8443`，驗證接受帶 port |
| 手冊 | `docs/user-manual/` 八份寫 `https://<主機>` 的頁、`docs/spec-site/current/` | 全改帶 `:8443` |
| 測試 | test repo `e2e-env/` | 映射改 8443 |
| 客戶防火牆說明 | 安裝手冊前置需求 | 放行 8443 取代 443 |
