FR-073 檢測 Agent 安裝形態比照主產品——設計定案

FR-073 檢測 Agent 安裝形態改造 — 設計

0. 分工概述(30 秒版)

現在 Agent 的「解壓目錄就是安裝目錄」,客戶刪掉解壓目錄=服務設定、憑證掛載點、CLI 全滅; 換版走新目錄還會撞固定 container_name 靜默失敗(CM-1533 實證)。

本案把 Agent 改成跟主產品同一套形態

現在(0.2.32) 改造後
安裝目錄 解壓目錄(家目錄,隨版號變) /srv/guidant-ai-agent(固定)
設定檔 .env 解壓目錄內 /srv/guidant-ai-agent/.env
bind mount(content/certs-in/factory) 解壓目錄內相對路徑 /srv/guidant-ai-agent/ 下絕對路徑
安裝紀錄 /etc/guidant-agent-compose/install.conf /etc/guidant-ai-agent/install.conf(改名對齊,並與原生版 /etc/guidant-agent/ 分家)
安裝 log 解壓目錄內(目錄一刪就沒) /var/log/guidant-ai-agent/(FHS,活過 uninstall)
CLI /usr/local/bin/guidant-agent-compose(內容是 install.sh 副本) 同左(名字不改,見 D5)
compose project 隨目錄名(...-0231 / ...-0232,會分裂) name: guidant-ai-agent(釘死)
volume 名 guidant-agent-compose-0232_agent-db(隨目錄變) guidant-ai-agent_agent-db(固定)
解壓目錄 🔴 請勿刪除 ✅ 裝完可整個刪

四段工:① install.sh 改成「複製到固定目錄再啟動」;② compose 加 name: 釘 project; ③ 升級/舊形態遷移邏輯;④ installer ⑥ 補「容器實際 image tag == 本次要裝的 tag」判定。


1. 現況與問題(實查佐證)

1.1 190 現況(2026-09-03 唯讀實查)

/etc/guidant-agent-compose/install.conf
  GUIDANT_AGENT_INSTALL_DIR=/home/guidantai/guidant-agent-compose-0.2.32

docker volume ls
  guidant-agent-compose-0231_agent-certs   ← 0.2.31 那套的憑證,孤兒
  guidant-agent-compose-0231_agent-db
  guidant-agent-compose-0231_agent-upload
  guidant-agent-compose-0231_seaweed-data
  guidant-agent-compose-0232_agent-certs   ← 現役(憑證是 runner 手動從 0231 複製過來的)
  guidant-agent-compose-0232_agent-db
  guidant-agent-compose-0232_agent-upload
  guidant-agent-compose-0232_seaweed-data

docker ps -a --format '{{.Names}}\t{{.Label "com.docker.compose.project"}}'
  guidant-ai-agent            guidant-agent-compose-0232
  guidant-agent-seaweedfs     guidant-agent-compose-0232

一台機器上有兩組 volume,其中一組(0231)是純孤兒但佔著磁碟;憑證是人手搬過去的。 這就是 CM-1533 事故的殘骸——不是特例,是現行設計在「換版」這個必然動作下的預設結果

1.2 188 現況(同日實查)——第二個活證據

/etc/guidant-agent-compose/install.conf
  GUIDANT_AGENT_INSTALL_DIR=/tmp/t66-final/guidant-agent-compose-0.2.28
docker ps | grep agent
  zen_sutherland  guidant-ai-agent:0.2.30  Up 7 days     ← 連 compose 都不是,裸 docker run
  guidant-agent-seaweedfs  chrislusf/seaweedfs:3.99  Up 12 days

安裝紀錄指向 /tmp/tmp 重開機清空 → 所有維運子命令(status/logs/upgrade/ uninstall)當場全滅,且錯誤訊息會是「這台主機看起來還沒安裝過」。這台是我們自己的 build 機、由熟悉系統的人裝的,尚且如此;客戶端只會更糟。

1.3 問題歸納

# 問題 後果 現行設計為何必然導致
P1 解壓目錄=安裝目錄 客戶刪目錄 → .env/compose/bind mount 全沒,CLI 報「還沒安裝過」 write_config 直接 ENV_FILE="${BUNDLE_DIR}/.env"INSTALL_DIR="$BUNDLE_DIR"(install.sh:1577,1624)
P2 安裝目錄在家目錄 帳號停用/家目錄配額/權限變更都波及生產服務;188 甚至落在 /tmp 沒有固定落點,客戶解到哪就是哪
P3 compose project 隨目錄名 新版目錄=新 project → 撞固定 container_name靜默失敗(fallback 把「舊容器還在跑」讀成成功) compose 無 name:,project 取目錄名
P4 volume 名隨目錄名 新 project 產生一組空 volume → 憑證消失 → 雲端 409 拒絕重註冊 同 P3,named volume 前綴=project 名
P5 fallback 判定不看 image tag 換版失敗被判成功,status.env 宣告值也顯示新版 dc_up() 只檢查 container_state == running
P6 完成畫面印「🔴 請勿刪除」 與客戶心智模型對抗——這是設計缺陷的訊號,不是可以用文案解決的事 P1 的下游

P3~P5 是同一個事故的三個環節,任一個修掉都能讓 CM-1533 不再靜默;三個都修才叫解決。


2. 方案對照(卡上七點逐項)

D1 目錄落點:/srv/guidant-ai-agent vs /opt/guidant-agent

選項 論據 判斷
**A. /srv/guidant-ai-agent 主產品用 /srv/guidant-aiGUIDANT_DATA_DIR 預設)。FHS:/srv 是「本機對外提供之服務的資料」——agent 的 .env、bind mount、log 正是服務資料。同族命名,客戶一眼看得出是同一家。 採用**
B. /opt/guidant-agent FHS:/opt 是「附加軟體套件」。agent 的程式本體在 docker image 內,宿主上放的全是資料與設定,語意對不上。另外 /opt/guidant-ai-be/opt/guidant-ai-agent 已被我們自己的 build 機源碼目錄佔用(188),同名不同義會製造混淆。 不採用

決定:/srv/guidant-ai-agent,可用環境變數 GUIDANT_AI_AGENT_DIR 覆寫(比照主產品 GUIDANT_DATA_DIR)。

為什麼是 guidant-ai-agent 而不是 guidant-agent(2026-09-03 user 指正,設計初稿寫錯): 檔案系統路徑的既定慣例是 kebab-case 的 guidant-ai(實查:/srv/guidant-ai 67 處、 /etc/guidant-ai 8 處、image guidant-ai-beguidant-ai-feguidant-ai-agent)。 全 repo 只有一處寫成連寫的 guidantai,那是網域名 guidantai-spec.jedicotech.com ——DNS 標籤不能有底線、大小寫不敏感,與檔案系統無關,不構成先例。 初稿的 guidant-agentai 吃掉了,與 image 名 guidant-ai-agent 不一致。

🔴 而且 /etc/guidant-agent/ 這個名字已被佔用(本次改名連帶修掉的真問題): 原生 tarball 版 agent 的設定就住在那裡(scripts/install/install.sh:113 CONF_DIR="${CONF_DIR:-/etc/guidant-agent}",內含 agent.env)。兩種形態裝在 同一台機器時,compose 版的 install.conf 會與原生版的 agent.env 混在同一個目錄, 而 uninstall 清目錄時會掃到對方的設定。改成 /etc/guidant-ai-agent/ 之後兩者 天然隔離。

連帶影響:compose project 名為 guidant-ai-agent,於是 volume 是 guidant-ai-agent_agent-db 等四個。project 名與 agent 容器的 container_nameguidant-ai-agent)同字——不同命名空間,技術上無衝突docker ps 只是兩個欄位 顯示同一串字)。接受這個小重複,而不是為它去改 container_name:那個名字已寫進 10-troubleshooting.md 的實際指令裡,改它的成本遠大於美觀收益。

配套落點(全部比照主產品三分離):

/srv/guidant-ai-agent/                 資料與設定(客戶不需進去,但備份要含它)
  ├── .env                            600,含註冊 token 與儲存憑證
  ├── docker-compose.yml              服務定義(升級時被新版覆蓋,舊版留 .bak)
  ├── content/                        檢測 profile(唯讀掛載)+ content/cache(可寫)
  ├── certs-in/                       雲端自簽憑證信任錨
  └── factory/                        出廠儲存設定快照
/etc/guidant-ai-agent/install.conf    安裝紀錄(644,不含密碼)
/var/log/guidant-ai-agent/            install-*.log/upgrade-*.log(見 D8,755)
/usr/local/bin/guidant-agent-compose  維運 CLI(install.sh 副本,名字不改)

D2 搬遷 vs 就地

選項 論據 判斷
A. install.sh 複製所需檔到固定目錄 解壓目錄與 tar.gz 用完可刪,這才是客戶的心智模型。與主產品 install_ops_cli() 完全同構(它就是 cp compose → 資料目錄install guidant → /usr/local/bin)。 採用
B. 就地安裝+/srv/guidant-ai-agent symlink 指回去 symlink 只是把「不能刪」這個約束藏起來——刪了目錄,symlink 變 dangling,錯誤訊息比現在更難懂(No such file or directory 指向一個看起來存在的路徑)。且解壓目錄仍在家目錄,P2 未解。等於什麼都沒修 不採用
C. 搬移(mv)而非複製 客戶重跑安裝時來源沒了;且 tar.gz 還在,「來源不可變」比較好推理。差異只有 3.4GB image tar 的磁碟(那些本來就該刪)。 不採用

要複製的東西(來自解壓目錄):docker-compose.ymlcontent/(含 cache/)、 certs-in/(若安裝當下產生了 cloud-ca.pem)、.env.exampleREADME.md不複製*.image.tar(已 docker load 進 docker)、install.sh(改為 install 到 /usr/local/bin/guidant-agent-compose,同現行做法)、image-manifest.sha256INSTALL.txt

冪等:所有複製都是覆蓋寫;content/cp -rn不覆蓋既有檔)——那裡面可能有 客戶自己投放的檢測 profile,升級時絕不可被包內的空目錄蓋掉。

D3 升級/遷移策略(本案承重牆)

D3-a compose project name 釘死

deploy/docker-compose.yml 頂層加:

# project 名顯式釘住(FR-073)。不釘會取「compose 檔所在目錄名」,於是
# 每個版本的解壓目錄各自成為一個 project,而本檔用的是固定 container_name——
# 新 project 建容器時撞上舊 project 還佔著的名字,compose 報 Conflict,
# 而 installer 的 fallback 會把「舊容器還在跑」讀成安裝成功(CM-1533 實證:
# 190 換 0.2.32 時整次換版被靜默吞掉,跑的仍是 0.2.31 的 image)。
# 連帶:named volume 前綴也是 project 名,不釘的話換版會產生一組空 volume,
# 憑證消失 → 雲端以 409(設備指紋已被另一台在線 agent 使用)拒絕重新註冊。
name: guidant-ai-agent

同時對齊主產品 docker/production/docker-compose.yml:93name: guidant——同一個 理由、同一個寫法,不另外發明。

D3-b volume 名固定

釘 project name 後 volume 自動變成 guidant-ai-agent_agent-db 等四個,不隨目錄變。 不額外用 external: truename: 硬釘:那會讓 uninstalldown -v 失效 (external volume compose 不刪),反而製造「以為刪乾淨其實沒有」。

D3-c 三條路徑的判定與行為

install.sh 開頭先判「這台機器現在是什麼狀態」,三分流:

狀態 判定依據 行為
N. 全新 /etc/guidant-ai-agent/install.conf 不存在 ∧ 舊紀錄不存在 ∧ /srv/guidant-ai-agent/.env 不存在 正常首裝:問四題 → 建 /srv/guidant-ai-agent → 複製 → 寫 .env → up
U. 已是新形態 /srv/guidant-ai-agent/.env 存在(或新 install.conf 指向它) 走升級語意:沿用 .env(只換 AGENT_IMAGE)、覆蓋 compose、cp -rn content、up -d 換版。volume 因 project 固定而完全沿用
**M. 舊形態(家目錄/任意目錄) 舊紀錄 /etc/guidant-agent-compose/install.conf 存在,且其指向目錄有 .env 遷移**(見 D3-d)

🔴 狀態 U 不再要求客戶記得打 --upgrade。現行設計裡「首裝 vs 升級」是靠客戶 選對指令來區分的,而 CM-1533 正是「客戶(我們自己)走了首裝路徑」造成的。改為 由機器狀態決定語意,客戶打什麼都不會壞。--upgradeupgrade <包> 仍保留 (行為等價,且 upgrade <包> 對「手上只有 tar.gz」的情境更順手)。

D3-d 舊形態遷移(狀態 M)

事故最容易發生的一段,故先停舊、再搬、再起新,且全程可回頭:

  1. 讀舊 install.conf 取得 OLD_DIR;確認 OLD_DIR/.env 存在。
  2. 明示告知並要求確認(互動)/--yes--configMIGRATE_CONFIRM=1(非互動): 「偵測到舊形態安裝於 <OLD_DIR>,將搬到 /srv/guidant-ai-agent,資料 volume 完整保留」。
  3. 記下舊 project 名(docker inspect 現役容器的 com.docker.compose.project label, 不是猜目錄名——目錄名可能被改過)。
  4. docker compose -p <舊project> --env-file <OLD_DIR>/.env -f <OLD_DIR>/docker-compose.yml down (🔴 絕不帶 -v)——停舊容器並釋放 container_name
  5. /srv/guidant-ai-agent,複製舊的 .env(保原權限 600)、content/certs-in/factory/; compose 用新版包內那份(含 name:)。
  6. 搬 volume 內容:舊 volume 名前綴是舊 project,新的是 guidant-ai-agent。用一顆掛兩個 volume 的臨時容器 cp -a 逐一搬(四個:agent-db/agent-certs/agent-upload/seaweed-data)。 已存在同名新 volume 且非空 → 停下報錯,不覆蓋。

    為什麼要真的搬而不是叫客戶接受重新註冊:agent-certs 內是舊雲端 CA 簽的身分憑證, 丟了就要重新 enroll,而雲端會以 409(設備指紋已被另一台在線 agent 使用)拒絕 ——CM-1533 實際撞到,最後靠人手複製救回。這條路必須由程式走完,不能留給客戶。

  7. 寫新 install.conf(/etc/guidant-ai-agent/install.conf),刪掉舊的 /etc/guidant-agent-compose/install.conf(留著會讓下一次執行又判成狀態 M)。
  8. up -d → wait_healthy → 驗 image tag(見 D4)→ 完成畫面告知 「舊目錄 <OLD_DIR> 已不再使用,可自行刪除」(不主動刪客戶的目錄)。
  9. 失敗時的回頭路印在畫面上:舊 volume 未刪、舊目錄未動, docker compose -p <舊project> -f <OLD_DIR>/docker-compose.yml --env-file <OLD_DIR>/.env up -d 即可還原。

🔴 舊 volume 不自動刪。搬完保留(_migrated-<日期> 不改名,就是原名留著), 完成畫面告知可手動清。理由同 uninstall 的資料 volume 政策:能被腳本自動觸發的 刪資料動作太危險,而磁碟成本遠低於「證據永久取不回來」。

D4 installer ⑥ fallback 補判定(CM-1533 runner 建議)

dc_up() 現行的 fallback 只問「容器在不在跑」。補一層:

啟動後(wait_healthy 通過後)逐一斷言:
  docker inspect --format '{{.Config.Image}}' guidant-ai-agent  ==  $AGENT_IMAGE_TAG
不符 → fail(退出碼 5),訊息點名「容器跑的是 <實際>,本次要裝的是 <預期>」,
       並提示最可能原因(另一個 compose project 的舊容器佔著同名 container)。

三個要點:

  • 判定放在 dc_up 之後、獨立成 assert_running_image(),不塞進 dc_up 內部—— dc_up 的職責是「容器有沒有被建起來」,image 對不對是另一件事,兩件混在一起下次還會漏。
  • seaweedfs 也驗(它的 tag 由 compose 寫死,驗它能抓到「compose 檔沒被更新」)。
  • 這條是安全網不是主修:D3-a 釘 project 之後理論上撞不到;但 CM-1533 教的正是 「靜默成功比失敗更貴」,安全網要留。

D5 子命令與周邊同步

對象 改動
statuslogsstartstoprestartfingerprintconfigure-storagere-enrollupgradeuninstall 全部經 resolve_installed_paths(),只需改該函式的優先序:① 環境變數 GUIDANT_AI_AGENT_DIR ② 新 install.conf ③ 舊 install.conf(相容) ④ 預設 /srv/guidant-ai-agent不再 fallback 到 $BUNDLE_DIR——那正是 P1 的來源
uninstall /srv/guidant-ai-agent(逐項,比照主產品的一級系統目錄防護 case 已存在)+ /etc/guidant-ai-agent/+ CLI;volume 仍逐項確認;同時清舊 /etc/guidant-agent-compose/ 殘留(🔴 不可誤清 /etc/guidant-agent/ —— 那是原生版 agent 的設定目錄)。/var/log/guidant-ai-agent/ 不刪(見 D8)
CLI 名 guidant-agent-compose 不改名。理由:已寫進 user-manual 五章、FR-072 影片、客戶肌肉記憶;改名的收益只有美觀。(/etc 路徑改名是必要的——它是內部路徑,且要與 /srv/guidant-ai-agent 同族才不會下次又找錯)
reset-190.sh(test repo training/scripts/console/ 探測與清理路徑加 /srv/guidant-ai-agent/etc/guidant-ai-agent/var/log/guidant-ai-agent;既有的 /etc/guidant-agent/opt/guidant-agent/var/lib/guidant-agent(原生版落點)保留
完成畫面 刪掉「🔴 請勿刪除」,改為「解壓目錄與 tar.gz 已可刪除」+列出實際安裝落點

D6 影響面清單

evidence-agent repo

  • deploy/install.shwrite_configresolve_installed_pathsrun_upgradecmd_uninstallprint_next_stepsinstall_ops_entry/檔頭說明段(usage() 直接印檔頭,改了要一併檢查 sed -n '3,91p' 的行號範圍)+新增 detect_install_statemigrate_from_legacyassert_running_imagestage_into_install_dir
  • deploy/docker-compose.yml:加 name: guidant-ai-agent;三處 bind mount 由 ./content 等 改為 ${GUIDANT_AI_AGENT_DIR:-/srv/guidant-ai-agent}/content絕對路徑——compose 的相對 路徑基準是 compose 檔所在目錄,而該檔現在住在固定目錄,理論上等價;但寫絕對路徑可讓 「有人把 compose 檔複製到別處執行」不會靜默指到別的資料)
  • deploy/README.md:安裝形態段全改
  • pyproject.toml:版號 → 1.0.0(見 D7)
  • scripts/build/build_compose_bundle.sh幾乎不動——它只負責把 deploy/ 下的檔複製進 staging,落點改變發生在客戶端執行期。要確認的是 INSTALL.txt 的文案(第 363~395 行附近) 與 image-prepare 相容性檢查(第 163 行 grep)不受影響

BE repo

  • docs/spec-site/current/user-manual/agent/03-install.md(第 50、187、237 行:請勿刪除、 安裝目錄範例、log 落點)
  • docs/spec-site/current/user-manual/agent/07-upgrade.md(7.2 表格第③列「覆蓋到既有安裝目錄」 仍成立,但要補「首裝指令對已安裝機器等同升級」)
  • docs/spec-site/current/user-manual/agent/05-operations.md(「不需要知道安裝目錄在哪」—— 這句本來就是承諾,改造後才真正成立,可留)
  • docs/spec-site/current/user-manual/agent/08-uninstall.md10-troubleshooting.md11-certificates.md:路徑字樣
  • docs/spec-site/current/user-manual/onprem/10-optional-features.md §10.5
  • 各檔檔頭「變更紀錄」加一行

test repo

  • training/scripts/console/reset-190.sh

FR-072 影片④(CM-1527,Agent 安裝):畫面上會出現「安裝目錄 /srv/guidant-ai-agent」與 「解壓目錄可刪」——與現有腳本敘述不同,需重錄。在 FR-072 卡上標記,不在本卡執行。

D7 版號:1.0.0(2026-09-03 user 裁示;初版寫 0.3.0 是我判斷錯)

現行 0.2.32 → 1.0.0

我原本寫 0.3.0,錯在沒查 repo 實際慣例就下判斷。 實查版號史: 0.2.16 → … → 0.2.28 → 0.2.29 → 0.2.30 → 0.2.31 → 0.2.32——中間那位從頭到尾 沒動過,連 re-enroll 子命令、SonarQube connector、心跳自報 capabilities 這些 真功能也都只 +1 patch。0.3.0 是這個 repo 從未發生過的跳法。 user 裁示:這版準備對外發佈,不該再掛 0.x,直接進 1.0.0。

🔴 改版號必須重跑完整 build,不能只重打包:版號在 Nuitka 編譯之前就烙進 config/version_bake.pybuild_agent_release.sh Step 2,產物要一起編進 binary), 另外還烙在 binary 的 --product-version、image tag 與交付包檔名。 --compose-only 會沿用既有的 version_bake.py,出來的包會被命名成舊版號 ——CM-1533 那棒實際踩過。故一律走完整 --with-compose

D8 一併處理的小事(低風險、順手做)

  • 安裝 log 落點改為 /var/log/guidant-ai-agent/install-<ts>.log(升級為 upgrade-*.log)。 現行落在解壓目錄,目錄一刪 log 就沒了——而那正是回報問題時最需要的東西。

    為什麼不比照主產品的 <資料目錄>/log/(2026-09-03 user 提案,經評估採納): 初稿抄了主產品,但主產品那個選擇有個它自己沒發現的缺陷——uninstall 會刪資料目錄, 於是 install/uninstall 的 transcript 在最需要事後檢討的那一刻一起消失。 /var/log/ 三個好處: ① FHS 正解,維運人員不必學新地方,logrotate 天然接管(現行 log 完全不受輪替管理); ② 活過 uninstall —— 「上次移除為什麼失敗」的紀錄還在; ③ 接得住早期失敗 —— installer 一律 root,可在做任何事之前先 mkdir -p /var/log/guidant-ai-agent,於是連「安裝目錄都還沒建就失敗」也有完整 log。 現行那種情況掉到 /tmp(重開機清空,而客戶回報問題往往在重開機之後)。

    代價:與主產品 installer 分歧一處。主產品是範本不是戰場,本案不動它; 列為 follow-up(見 §6)。

    uninstall 政策/var/log/guidant-ai-agent/ 不刪(連同上面 ② 的理由), 完成畫面告知路徑與「可自行刪除」。這與「資料 volume 逐項確認」同一個原則—— 能自動觸發的刪除動作要保守,何況這裡刪掉的正是排查證據。

  • 檔頭「🔴 交付包目錄就是安裝目錄,裝完不要刪」整段改寫。


3. 風險與反悔條件

風險 評估 緩解
遷移搬 volume 出錯 → 憑證掉 → 409 掉線 承重牆。這是全案最危險的一段 先停舊再搬;舊 volume 不刪;失敗回頭路印在畫面;190 實測必須真的裝兩輪對照(驗收條件②③)
已裝客戶受影響 實查:目前無外部客戶安裝。188 是 build 機(/tmp 那套是測試殘骸)、190 是測試機、189 POC 未裝 agent 遷移邏輯仍要做(我們自己的 190/188 就要用它,且它是客戶端唯一的相容保證),但可以在真實環境反覆重裝驗證,不必一次到位
/srv/guidant-ai-agent 已被佔用 比照主產品:偵測到既有 .env 走升級語意;偵測到非本系統的檔案 → 停下報錯不覆寫 D3-c 狀態判定
compose 檔 bind mount 改絕對路徑後,舊 compose 與新 compose 混用 升級時 compose 一律換新版;舊版 .bak 保留 已在 run_upgrade 內(現行邏輯沿用)
改了檔頭註解導致 usage() 印錯 usage()sed -n '3,91p' 寫死行號 改檔頭後必須實跑 ./install.sh --help 目視確認

反悔條件:若 190 實測發現「舊形態遷移」在真實資料上不可靠(volume 搬移出現資料損壞或 權限問題無解),退而求其次=不做自動遷移,改為偵測到舊形態即停下、印出人工遷移步驟 (因為目前無外部客戶,人工路徑可接受);但 D3-a/D3-b/D4(釘 project/固定 volume/ image tag 斷言)無論如何都要做——那三條才是 CM-1533 的真正修法。

3.1 實作階段才浮現的三個缺陷(設計時沒想到,記錄在此免得下次重蹈)

# 缺陷 為什麼設計時看不出來 修法
遷移用 docker run alpine:3 搬 volume——封閉網路下拉不到 寫「用臨時容器搬 volume」時預設 alpine 隨手可得,那是連網開發機的直覺。但本產品的前提就是離線安裝,交付包只帶 agent 與 SeaweedFS 兩顆 image。190 實查確認機器上沒有 alpine 改用 AGENT_IMAGE_TAG(遷移前必然已 docker load,是唯一保證存在的)。已在 190 實測權限與擁有者完整保留(600 1000:1000
--check-only 沒判定狀態 → 全新機器跳過埠佔用檢查 check_environment 改成依 INSTALL_STATE 分流時,只想到主流程會先呼叫 detect_install_state,忘了 --check-only 是另一條入口。空字串讓 "!= fresh" 恆為真 --check-only 補呼叫 detect_install_state(只讀不寫,不違反「不做任何變更」)
INSTALL.txt(客戶第一眼看的檔)仍寫「🔴 請勿刪除」 它不在 deploy/ 下、是 build_compose_bundle.sh 用 heredoc 生成的,改 deploy/ 時整個沒掃到。出貨包會自打嘴巴:INSTALL.txt 說不能刪、完成畫面說可以刪 樣板同步反轉,並補「已裝過的機器同一條指令自動走升級」

共通教訓:「改文案」這件事的範圍不等於「文件目錄」——生成式產物(build 腳本內的 heredoc)與執行期輸出同樣是客戶讀到的文字。下次做這類反轉時,判準是 「客戶會在哪些地方讀到這句話」,而不是「哪些 .md 提到它」。


4. 驗收條件(第二段執行,每項貼實際輸出)

190 上,四條路徑:

  1. 乾淨首裝+刪解壓目錄:清掉兩套舊安裝(volume 先備份一份)→ 新包首裝 → rm -rf ~/guidant-agent-compose-1.0.0*guidant-agent-compose status 仍正常、 容器 healthy、雲端心跳 200。
  2. 升級沿用:在①之上再裝一次同包(或次版)→ docker volume ls 名稱不變agent-certs 內憑證未變、無 409、docker inspect 的 image tag 換新。
  3. 舊形態遷移:用 0.2.32 家目錄形態裝好(含註冊成功)→ 跑新 install.sh → 偵測為狀態 M → 遷移完成 → 憑證沿用(log 出現 already enrolled ... skip register)、 雲端 remote_agents 版號更新、status=active
  4. uninstall 乾淨/srv/guidant-ai-agent/etc/guidant-agent、CLI 全清; volume 依提示保留或刪除;舊 /etc/guidant-agent-compose 殘留一併清掉。

外加:負面驗證——刻意讓一顆舊 tag 的容器佔住 guidant-ai-agent 這個名字後跑安裝, 確認 D4 的 assert_running_image報失敗而不是報成功(證明安全網有牙齒)。


5. 不做的事(劃界)

  • 主產品 installer 不動——它是範本不是戰場。
  • CLI 不改名(見 D5)。
  • 不引入 systemd unit——容器 restart: unless-stopped + docker 開機自起已等價, 多一層只是多一個會壞的地方(與現行檔頭立場一致)。
  • 不做自動刪除客戶的舊目錄/舊 volume——只告知。
  • FR-072 影片④重錄不在本卡,只在 FR-072 卡上標記。

6. Follow-up(不在本卡做)

  • 主產品 installer 的 log 落點:現行 <資料目錄>/log/,會被自己的 uninstall 刪掉 (見 D8 的評估)。本案不動主產品,但同一個缺陷在那裡也成立,值得日後對齊到 /var/log/guidant-ai/
  • FR-072 影片④(CM-1527)重錄:安裝形態改變後畫面對不上。由 FR-072 原 session 處理 (2026-09-03 user 指示),本卡不碰該卡。
  • 原生 tarball 版 agent 是否比照:原生版用 /opt/guidant-agent/etc/guidant-agent/var/lib/guidant-agent,同樣沒對齊 guidant-ai 命名。本案只動 compose 形態 (那是現行出貨形態);原生版命名對齊屬獨立議題,且它沒有「解壓目錄=安裝目錄」這個 病根,優先度低。