---
title: FR-193 紀錄（LOG）
h1: FR-193 紀錄
subtitle: append-only；每棒追加一個 block，寫完不改
---

## 第 1 棒　2026-11-10（首腦：需求釐清 → 設計 → 開卡 → 驗收 → 真包首裝修正）

**做了什麼**

從決策者一句「本體跟 agent docker 都有改成 rootless 嗎」開始。查出 FR-180 只做到容器內非 root、引擎仍是 root，當時刻意不做 rootless。決策者改判要做。寫討論稿、design v1.0～v1.4、十張卡、Agent 主機手冊（Claude Docs）。工作坊跑完 A0～A8。之後首腦親手：分析正式發版各關卡、合回 `ws-CM-3804`、在 188 出測試包、協助決策者在 192 用真包首裝、把 16 個維運指令逐一驗過並修掉抓到的問題。

**commits**

BE（`feature/FR-172`，都已推）：`80ff36282` 討論稿 → `49ee845c6` D4 → `8976dabd6` design v1.0＋cards.py → `5363671c1` v1.1（D1 改 8443）→ `9c5f3091b` v1.2（D8）→ `da36b6d20` → `cbe63588f` 開卡 → `495d360e6` → `bf9e8cdb6` v1.3（A0 實測後 D9～D11）→ `ce2b1cf56` v1.4（A4～A6 合併）。

Agent（`feature/FR-172`，都已推）：工作坊的 A1～A7（`0d466da` `bcae205` `e6d261d` `9a9588f` `88a19ac` `0dbcd41` `46b4703` `87b712c` `bdb2ea5` `cd9fbdf` `f73d523`）、合回 `62c23c3`，之後首腦親手六個：

| commit | 修什麼 | 起因 |
|---|---|---|
| `710467e` | 部署／演練／健康檢查三支腳本找對 Docker daemon | FR-193 漏掉 |
| `3ff9fa9` | 映像落地檢查在完整輸出裡找通過訊息，不只看最後 30 行 | FR-188 讓啟動輸出變長 |
| `9d6f577` | 包內容檢查不用「大輸出 \| grep -q」（pipefail 下 141） | FR-193 讓清單變長 |
| `753e4e2` | 離線裝 Docker 不碰主機 systemd：不用 machinectl、包內不帶 systemd 一組、裝前驗相依 | 真包首裝第一步就失敗 |
| `cd54c60` | 沒填主系統時完成步驟不空等 90 秒 | 註冊改到主控台後沒收乾淨 |
| `b0efcba` | 升級驗章改用 docker cp，不掛主機目錄 | rootless 下 daemon 進不了 root 700 目錄 |

未 commit：`deploy/install.sh` 的 uninstall 修正（見 STATE §1）。

**決策**

- D9（A0 實測後改）：維運指令用 root 的 CLI 加 `DOCKER_HOST` 指到 guidant 的 daemon，不用 `sudo -u guidant`。後者讓 `image_read_file`、升級 `docker load`、`--env-file` 都 permission denied。
- D10：維運程式改讀主機副本 `/usr/local/lib/guidant-agent/install-lib`，不以 root 去 source guidant 映像庫裡的腳本。
- D11：NET_RAW 保留，另加容器 sysctl `ping_group_range`。拿掉 NET_RAW 連 ping 都起不來。
- 切換帳號不用 `machinectl`，改 `sudo -u` 加 `XDG_RUNTIME_DIR` 與 bus 位址。192 實測可行，重開機後不登入也會自己起來。
- 包內不帶 Source 是 systemd 的套件。它們彼此要求同版號，「只裝主機缺的」會把新版配到舊版上。用 systemd 開機的主機一定已有。
- A4／A5／A6 合成一張（決策者問「為什麼不是全部開發完再測」）。同一支 install.sh、同一次裝機，分三張等於做三次且每次驗不完整。
- 出包走 188 手動 `build_all.sh --with-compose --internal-test`，不走 GitLab 直接部署。後者會自動裝到 190／188／189，那三台系統層 Docker 上有主系統，新版安裝程式會拒絕。

**教訓**

- **卡的驗收寫「不必跑完整流程、留給總驗」就會漏。** CM-3809 的升級驗收只放假升級單看單元起得來，真正的升級走不到底，到決策者要求全跑指令才抓到。換檔進舊包驗 ≠ 真包驗：離線裝 Docker 與從新映像存主機副本這兩段只有真包走得到，第一次真包安裝就死在前者。
- **D9 只驗了「CLI 自己讀檔」，沒想到「把主機路徑交給 daemon 掛載」。** 定一個身分模型時要把「誰讀檔」的每一種形式列完。
- **「只裝主機缺的套件」遇到 `= 版號` 的相依會壞。** 連網機用 apt 驗不出來，apt 會連帶升級。離線路徑一定要離線驗。
- **建置檢查用 `tail -N` 或「大輸出 \| grep -q」是定時炸彈。** 輸出變長才壞，而且壞的時候產物是好的。
- **載入 lib 的條件要跟著儲存位置一起改。** lib 搬到主機副本後，`cmd_uninstall` 仍把載入綁在「讀得到映像名稱」上，`wipe` 之後整段還原沒執行卻印「移除完成」。
- **沒先看程式就對決策者下判斷，錯了兩次。** 說 ping 在 rootless 不可用（實際是 sysctl）；說 `wipe` 之後容器應該還在（實際設計就是整個刪）。
- **不帶路徑的 `git stash` 把別人未提交的檔一起收走。** 當場用完整 hash `apply`、比對一致後才 `drop`，沒有遺失。檢查用的指令不要夾帶會改工作區的動作。
- **在這台 Mac 上 bats 的中文測試名跑不了（bash 3.2），本機也沒有 dpkg。** 要到 188 跑。Agent 的 venv 本機沒有，全量測試要在 188。188 的 `.venv` 建置中不可動，另開獨立環境時記得指定 Python 3.11，預設會拿到 3.13。

**推翻了什麼**

- FR-180 design 與 SR-09.5 的「不做 rootless」。
- design v1.2 的 `dk()` 用 `sudo -u guidant`、「compose 拿掉 NET_RAW」、uid 換算「起點 + N」（實際是起點 + N − 1）。
- design v1.0～v1.3 的 D1「預設 443 貼 rootlesskit 能力」→ PM 裁一律 8443。
- CM-3806 A1 用 `machinectl shell` 啟用 rootless、必備套件含 `systemd-container`。
- 首腦當天說過的「rootless 下 ping／traceroute 不可用」「Docker 套件還沒裝上」「192 上的資料夾是空的」「wipe 之後容器應該還在」。

**怎麼不重編譯重出包**（188，`/opt/guidant-ai-agent`，先 `git pull`、`pgrep -af 'nuitka|build_all'` 確認沒人在建）

改動只在 `scripts/`、`deploy/`、`test/` 時編譯產物不變（`git diff --name-only <編譯時的 commit> HEAD` 核對）。安裝程式會被簽章並進映像，所以從打包那步重跑：

```bash
B=/opt/guidant-ai-agent/.build
export FE_CONSOLE_DIST=$B/console/dist-console
scripts/build/build_agent_bundle.sh $B/dist/evidence-agent-1.7.4
scripts/build/smoke_agent_bundle.sh $B/bundle/guidant-agent-1.7.4.tar.gz
scripts/build/build_all.sh --compose-only --internal-test
```

約 12 分鐘。`fetch_docker_debs.sh` 有改動時要先把 `$B/docker-debs` 移開，否則沿用舊快取。主控台前端要重建時：`FE_REF=feature/FR-172 NPMRC_FILE=/home/jedi/.npmrc scripts/build/build_console_dist.sh $B/console`。

**正式發版各關卡（2026-11-10 實測）**

套件弱點稽核通過；前端測試通過；映像弱點關卡通過（用更新後的判定程式，Agent 只擋 Critical，白名單 7 條命中、30 天內無到期）；編譯、簽章、冒煙六項通過；全量測試 2555 過但環境是 Python 3.13。會擋的：版號仍是已發過的 1.7.4、版本說明未寫。

**16 個維運指令在 192（rootless）的結果**

正常：`status` `logs` `fingerprint` `--help` `stop` `start` `restart` `reset-admin-password` `console-allow-cidr` `configure-storage`。可執行但沒有主系統只驗到「失敗時不弄壞」：`set-base-url` `re-enroll`。`upgrade` 原本壞、已修（`b0efcba`）。`console-login` 壞、未修、非 rootless 造成（CM-3868）。`wipe` 正常。`uninstall` 在 `wipe` 之後不還原主機設定，修正未驗完。

### 補記（同一棒，22:40）

交接文件寫完後把做到一半的 uninstall 修正收掉：Agent `3ea579d`，已推。

第一次請決策者在 192 重跑 `uninstall` 時被 `resolve_installed_paths` 擋下（安裝目錄已被前一次刪掉，「看起來還沒安裝過」），沒走到修正。這暴露修正本身的漏洞：印「移除未完成，請再跑一次」但再跑會被拒。補 `host_leftovers_exist`，主機有殘留就放行只清殘留。第二次重跑後上機核對，帳號、rootless Docker、設定檔、升級單元、主機副本都清掉，系統層 Docker 恢復 enabled。

教訓：**修「失敗時叫人重試」的訊息前，先確認重試那條路走得通。** 以及確認題可以用答 no 的方式試跑到提示出現為止，不必真的刪，就能驗「有沒有走到那一段」。

本棒 Agent 親手 commit 共七個：`710467e` `3ff9fa9` `9d6f577` `753e4e2` `cd54c60` `b0efcba` `3ea579d`。


## 第 2 棒　2026-11-10 晚～11-11（接手：重出包 → 指令改名 → 192 首裝 → Agent 1.7.5 進版發版）

**做了什麼**

接手盤點後決策者裁三件事：CM-3867 改名照建議做（包檔名不改、`guidant-agent-ctl` 不動）、舊指令名不保留（「1.7.x 都還沒發出去」）、手冊只改 Claude Docs 那份。順序改成「先改名、再一起重出包」，出包期間改文件。188 從打包步重出測試包（不重編譯，6 分半）；決策者還原 192 後用新包首裝通過。接著走 `version-bump` skill 進 1.7.5：白名單複查、188 清磁碟、映像弱點掃描、全量 pytest、進版 commit、打 tag、API 觸發 release pipeline（決策者裁「你直接發，不用我按」）。

**commits**

Agent（`feature/FR-172`，都已推）：`b8b1158` 改名 21 檔 → `597a2d9` 進版 1.7.5（tag `v1.7.5`）。BE：`8211b2ab5` 憑證到期提醒指令。FE：`1a297c7d` 同（中英）。Claude Docs 安裝手冊兩處直接改。

**決策**

- 改名用「後面不接 `-` 或 `/`」當判準做整批替換：包檔名前綴 `guidant-agent-compose-` 與舊安裝紀錄 `/etc/guidant-agent-compose/` 保留，其餘全換。主機副本檔名跟 `basename "$OPS_ENTRY"` 走，不必另改。
- BE 客戶手冊十四章、spec-site、test repo 的改名延後（決策者「舊文件先不管」）。
- 188 清磁碟只刪 build cache 與測試包，`release-archive/` 不動（`deploy_rehearse.sh` 標「客戶在用的版本不可刪」）。
- pytest 9 條 ERROR（audit queue 沒排空／SQLite locked）與弱點掃描併跑時出現，閒機重跑那 5 檔 130 條全過，判偶發。
- BE 映像 2 條白名單外 High（net-ssh 7.3.3）不擋 Agent（Agent 只擋 Critical），留給 BE 下次進版。

**教訓**

- **`str.index("## 下一版")` 命中的是寫法說明段裡的那句**，把說明段尾吃掉；要用 `"\n## 下一版\n"` 鎖整行。寫完 `grep "^## "` 看標題清單再 commit。
- **188 磁碟是正式發版的隱形門檻**。弱點掃描 trivy 要把 4 GB 的 Agent 映像解到 `/tmp`，剩 13 GB 就「no space left」；CI 同一步會死在同一處，而且 log 可能一行都沒有。進版第一步先 `df -h /`。
- **188 的 bats 1.2.1 沒有 `BATS_TEST_TMPDIR`**，`test/shell/` 在那台永遠 61 條紅；要判「是不是我弄壞的」拿改動前的 commit 開 `git worktree` 跑同一支對照，不要看絕對數字。
- **接手盤點要連 Notion 卡狀態一起查**：STATE 寫「驗收通過待確認」的卡，決策者已經收成 Done；文件與 Notion 不符時以 Notion 為準。
- `perl -pi` 接換行分隔的檔名清單會把整段當一個檔名，要 `tr '\n' '\0' | xargs -0`。

**推翻了什麼**

- 第 1 棒 STATE「Notion 卡」表的狀態（A1～A9 已 Done、母卡 Not started、A10 修正待驗證）。
- 第 1 棒「改名要等 FR-193 真包驗收告一段落再開工」：決策者裁改名與六個修正合進同一顆包一起驗。
- CM-3867 卡上「舊指令名保留成捷徑」的建議：決策者裁不保留。

### 補記（第 2 棒續，2026-11-11 上午）

1.7.5 pipeline 3050615877 全綠、已發布。決策者要 Ubuntu 26.04 離線安裝檔：Docker 官方有 resolute 套件，188 實抓 Docker 34 個 deb（閉包無 systemd）、tpm2 41 個；改 Agent `fetch_docker_debs.sh`／`tpm.sh`／`build_compose_bundle.sh`／`tpm_gate.bats`（`790063d`）與 BE `fetch_tpm2_debs.sh`（`88f3228fa`），直接進版 1.7.6（`a0428ee`，tag `v1.7.6`，pipeline 3052691926）。

教訓：**發版 pipeline 跑完 188 又少 15 GB**（compose 包、離線包、`bundle/stage/<版>` 骨架各一份），`bundle/stage/` 從 1.5.4 起每版留一份共 20 GB 沒人清；連發兩版第二版打 tag 前要再 `df`。RELEASE_NOTES 已發布段落（1.7.3／1.7.4）的 OS 清單不能跟著改，會動到簽過章的版本說明。

### 補記（第 2 棒續，2026-11-11 下午）

1.7.6 發布（3052691926 全綠）後決策者在還原後的 192 與 193 實裝，抓到兩件：

- `./install.sh --check-only` 在全新主機 ✗「找不到 docker」——`check_environment` 的 Docker 區塊是 rootless 之前的判定，上方 `rootless_host_check` 已判過「安裝時從出貨包裝」，兩者打架。修掉後露出兩個被它擋在前面的 `set -e` 死法：`(( ok_fp == 1 )) && ok …`（ok_fp=0 整句非零）、`ru_home="$(getent passwd guidant | cut …)"`（帳號不存在 getent 回 2，pipefail 整句非零）。畫面只剩「中止（退出碼 2）」沒有任何 ✗，很難看出是腳本被殺。`07cfa00`。
- 本機管理員密碼那題只有一個 `>`、打字不顯示、錯誤訊息（stderr）黏在提示（stdout）後面。改成 ══ 標題、粗體「▶ 現在請輸入…不會顯示字元」、第 1 次／第 2 次標籤、錯誤走 stdout 加 ✗ 自己一行。`1af05b4`。

進版 1.7.7（`ff5e827`，tag `v1.7.7`，pipeline 3052828200）。決策者裁 1.7.6 不撤回。

教訓：**修掉第一個 ✗ 之後要把整條路跑到底**——`CHECK_FAILED` 一旦為 1，後面的 `set -e` 死法永遠看不到；全新主機（沒 docker、沒 guidant 帳號）是 `--check-only` 的主要客群，卻是最沒被跑過的路徑。**`set -Eeuo pipefail` 下「`(( ))&&` 不是最後一句」與「`$(可能失敗的指令 | …)` 直接賦值」都是地雷**，要寫 `if` 或 `|| true`。另：決策者已實裝 193（主機位址 192.168.50.193）。

### 補記（第 2 棒續，2026-11-11 14:10）

1.7.7 發布完成（更新站 `published`）。中途兩次環境事故：① build 前置檢查「簽發站 189:5062 不可達」——189 整台不在網路上，決策者重開後 retry build；② retry 到一半 188 資源耗盡重開，GitLab 上那個 build job 停在 `canceling` 卻握著 resource_group `nuitka-188`，新 retry 永遠 `waiting_for_resource`、188 上沒任何行程。`POST /jobs/<id>/cancel` 加 `force=true` 才把它踢成 canceled，鎖才放。

教訓：**job 顯示 running／canceling 但主機沒行程＝runner 丟了這個 job**，看 `resource_groups/<key>/upcoming_jobs` 與主機 `pgrep` 對照；一般 cancel 等 runner 回 ack，runner 已重啟就永遠等不到，要 force。188 同時是 STG 主機、build 機、runner，記憶體吃滿就整台倒，決策者當場關了部分服務。

### 補記（第 2 棒續，2026-11-11 傍晚）

決策者要「合併 FR-195 資安修正再出一版」。FR-195 已由前一棒合回 FR-172（Agent `b011adc`，在 v1.7.7 之後）。出版前檢查：三 repo 本機＝遠端；README 標 jedi-ldap 0.1.1 未發版→跑它 40 條測試後發上 Nexus，Agent `poetry update jedi-ldap` 只動 lock 一行；188 清到 44 GB；HEAD 全量 pytest 2587 過。進版 1.7.8（`518b586`，tag `v1.7.8`，pipeline 3053882594），同時帶 FE `b61597bc` 隱藏線上開通。F10／F11 暫緩不在本版。

教訓：**先問「哪一條修正」再找 branch**——這次先抓到 FR-192（同樣在工作坊、同樣 16 張 Done、同樣三 repo 乾淨可合），差點把一個套件沒發、主系統端點沒上的半套功能當成「順手合進去」；決策者一句「不對吧」才轉回 FR-195。工作坊 branch 全 Done 不等於可以出貨，結案卡（發版／改釘）被跳過的要單獨確認。

### 補記（第 2 棒續，2026-11-11 晚）

1.7.8 pipeline（3053882594）verify／build 綠、package 做完簽章與推 Harbor，最後 `publish_draft.sh register --publish` 被更新站回 400 `invalid_request`。原因：版本說明英文第一條 809 字，更新站 `src/releases.ts` `MAX_NOTE_LENGTH = 500`（每條）。tag 不移，進 1.7.9（`dca3c62`，tag `v1.7.9`，pipeline 3054021918），四條中英各條 ≤500（最長 454）；RELEASE_NOTES 的 1.7.8 段留著標「未發布」。188 清掉 1.7.8 的 stage／compose／offline／映像（Harbor 上 `1.7.8-518b5863` 留著不動），剩 37 GB。

教訓：**版本說明每條 ≤500 字是更新站的硬限制，register-draft 才擋，而且是在簽章、推 Harbor 都做完之後**——一條超長就白跑 50 分鐘外加一個燒掉的版號。寫完先量：`awk '/^## <版>/{p=1;next} /^## /{p=0} p && /^- /' RELEASE_NOTES.md | while read l; do printf '%s ' "$(printf '%s' "$l" | wc -m)"; done`，中英都量。`version-bump` skill 寫了「每條 500 字」但只在主系統那段，Agent 段沒寫。

### 補記（第 2 棒續，2026-11-11 20:05）

1.7.9 發布完成（3054021918 全綠）。下載連結、桌面文件、安裝手冊（版本 18）都換到 1.7.9。188 清到 30 GB（連發三版後每版留 stage／compose／offline 約 7 GB，`release-archive/` 不動）。

本棒一天內發了 1.7.5、1.7.6、1.7.7、1.7.9 四版（1.7.8 燒掉）。回頭看，1.7.6→1.7.7→1.7.9 三次都是「發完才在真機／pipeline 末端發現」：環境檢查全新主機壞、密碼提示看不出要打字、版本說明超長。共同點是**客戶真正會走的那條路（乾淨主機、第一次裝、更新站登記）在發版前都沒走過**。下次進版前固定做三件事：乾淨機 `--check-only` 非 root 跑到底、版本說明逐條量長度、`release_notes` 格式先用更新站的 validator 本機驗。
