---
title: "Agent 獨立更新：包分家、各自更新、進度即時 — 需求討論稿 (FR-172)"
brand: "Guidant AI · **FR-172** Agent 獨立更新"
eyebrow: "FR-172 · 需求討論稿 v1 · 2026-11-01（與 FR-171 同週期拍板）"
h1: "修 Agent 不必再出一包主系統"
subtitle: "安裝包、CI、R2 路徑、更新站版本線全部分家；主系統更新中心與 Agent 主控台都能發動 Agent 更新；進度與結果當下回報"
lede: "今天修一個 Agent 的小 bug，要重出一包 1.7 GB 的主系統、客戶停機 7 分鐘升主系統，再等心跳推 Agent，升完又等 5 分鐘才知道結果，畫面上的八格進度還停在第一格。原因只有一個：**Agent 的包、版本線、推送、回報全部掛在主系統身上**。本稿把它們一件件拆開——Agent 自己出包上傳、更新站多一條版本線、主系統更新中心能單獨抓 Agent 新版、Agent 主控台多一頁「更新」、進度與結果走 FR-171 定的專用通道。四個決策者已先裁的題目直接寫進設計，其餘 D1～D12 等拍板。"
chips: [
  {text: "決策者已裁：Agent 不連外網、包只從主系統拿", kind: ok},
  {text: "決策者已裁：主控台按一下＝主系統幫這台更新", kind: ok},
  {text: "D1～D12 待裁", kind: warn},
  {text: "與 FR-171 一起拍板、兩棵卡樹", kind: accent},
  {text: "跨 5 repo：update-center／BE／FE／evidence-agent／jedi-agent-protocol", kind: accent},
  {text: "陷阱：Agent 的章與主系統的章不可互相冒充", kind: crit},
]
footer: "FR-172 需求討論稿 v1 · 首腦撰寫 · 2026-11-01 · 前作 FR-167（線上更新）、FR-171（通道）"
---

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

**整案一句話**：Agent 的「貨」（安裝包、版本線、下載、推送畫面）從主系統身上拆下來自己走，「線」（主系統叫 Agent 升級、Agent 回報進度）用 FR-171 定的通道，不再借心跳。

| 階段 | 做什麼 | 產出 | 完成怎麼判定（決策者檢查法） |
|---|---|---|---|
| P1 出貨分家 | Agent repo 自己 CI 出包、登記更新站、上 R2 自己的路徑；主系統包不再帶 `agent/`；更新站多一條 Agent 版本線；蓋章 CLI 多 `--product agent` | Agent `.gitlab-ci.yml`、更新站 D1 加欄＋後台 Agent 分頁、CLI | 更新站後台看到「主系統」「Agent」兩個版本分頁，各自列各自的版號；R2 多一個 `agent/v1/` 資料夾；主系統包解開沒有 `agent/` 目錄、小了 1.7 GB |
| P2 主系統側 Agent 區 | 更新中心 Agent 區改成自己查 Agent 新版、先下載再推、相容兩向檢查、移到 `agent-bundles/<Agent 版>/` | BE product_update Agent 線、FE Agent 區 v3（照雛形第 1～5 頁） | 主系統不升版，Agent 區按「立即檢查」看到 Agent 新版、按「預先下載」、按「更新選取的台」，整段不碰主系統版本 |
| P3 進度與結果即時 | Agent 升級每一步當下送事件給主系統；畫面八格跟著走；失敗能看到主機端的原因 | Agent 事件推送元件（用 FR-171 專用端點）、BE 收事件寫表、FE 八格改讀表 | 推一台，八格在 3 秒內開始往前走；拔網路線再插回，事件補送、最後一格仍會亮；故意塞滿磁碟推一次，「看原因」顯示 `no space left on device` |
| P4 Agent 主控台更新頁 | 主控台新增「更新」頁：看版本、看主系統上有沒有新版、按「請主系統更新這一台」、離線上傳更新包 | 主控台 `Update.vue`（照雛形第 6～9 頁）、Agent 兩支端點、主系統一支端點 | 登入 Agent 主控台按更新，主系統更新中心同時出現這一台的進度；拔掉主系統、在主控台上傳離線包，Agent 自己升上去 |
| P5 收尾 | 更新站後台 Agent 三欄改比 Agent 版本線；舊客戶過渡；相容矩陣四格測試；文件 | 後台修正、過渡文件、e2e | 後台「Agent 在版」數字等於 Agent 區實際台數 |

**依賴一句**：P1 與 P2 不依賴 FR-171，可立刻開；P3 與 P4 的「主系統→Agent 叫升級」「Agent→主系統送事件」兩條線用 FR-171 批 1～2 定的通道與契約，等它合回才開。**決策者功課**：裁 D1～D12；其中 D1（更新站存法）與 D2（章怎麼分）影響 P1 第一張卡。

---

## 1. 為什麼要做 {#why nav="為什麼"}

> 這邊之後要改成主系統跟 agent 要分開更新。主系統跟 Agent 要分開來，不要像現在都包在同一包裡面；要可以各別分開更新；Agent 那邊也要有更新中心，可以做離線更新，或是跟主系統發動更新需求；R2 上面的安裝包要分開來；不要再用心跳帶資料。
> [— 決策者原話（2026-11-01，189 推 Agent 1.4.8 時）]{.attr}

2026-11-01 為修兩個 Agent 問題實際走一次的帳：

::: statgrid
::: stat
[20 分]{.v}[出 Agent 1.4.8]{.k}
:::
::: {.stat .crit}
[36 分]{.v}[只為帶 Agent 而重出主系統 1.27.10]{.k}
:::
::: {.stat .crit}
[7 分]{.v}[客戶停機升主系統（主系統本身沒事）]{.k}
:::
::: {.stat .warn}
[5 分]{.v}[升完等下一跳心跳才知道結果]{.k}
:::
:::

::: grid2
::: {.card .crit}
#### Agent 的修正節奏被主系統綁住
Agent 包 1.8 GB 被塞進主系統包 `agent/` 底下一起簽、一起上 R2 `v1/<主系統版>/`。更新站只有一條版本線。Agent 改一行就得重出主系統、客戶就得停機。Agent 包還占主系統包一半以上的下載量。
:::
::: {.card .warn}
#### 推送、進度、結果全靠心跳搬
推送後最長等 5 分鐘 Agent 才來拿；FR-167 補了「叫醒心跳」等於再疊一層。結果只隨心跳上報，容器一換狀態就丟。八格進度是主系統畫面刷新時去拉 Agent，結果一到就清掉，失敗時常常看不到卡在哪一步。60.167 那次真因是磁碟滿，主系統只拿到「無法讀取新映像」一句。
:::
:::

---

## 2. 現況：Agent 的貨怎麼走 {#current nav="現況"}

```{.mermaid cap="圖 1 — 現況。Agent 包從 build 到客戶 Agent 主機，每一段都掛在主系統身上。"}
%%{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
  subgraph build["188 build 機（人手跑）"]
    A1["evidence-agent<br/>build_all.sh --with-compose<br/>→ guidant-agent-compose-1.4.8.tar.gz"]
    B1["compliance-manager-be CI package<br/>build_bundle.sh 去抓最新一支 Agent 包<br/>塞進 guidant-ai-1.27.10/agent/"]
  end
  subgraph site["更新站（一條版本線）"]
    S1["product_releases 1.27.10<br/>min_agent_version"]
    R1["R2 v1/1.27.10/guidant-ai-1.27.10.tar.gz<br/>（內含 agent/，1.7 GB）"]
  end
  subgraph host["客戶主系統"]
    H1["升級主系統 → 解到<br/>upgrade/bundles/guidant-ai-1.27.10/agent/"]
    H2["更新中心 Agent 區<br/>只能推『這版主系統附的那個 Agent』"]
  end
  subgraph agent["客戶 Agent"]
    G1["心跳回應 update 鍵 → 下載 → 寫升級單"]
    G2["主機 systemd 升級 → 結果等下一跳心跳帶回"]
  end
  A1 --> B1 --> S1 --> R1 --> H1 --> H2 --> G1 --> G2
```

| 環節 | 現況 | 掛在主系統的哪裡 |
|---|---|---|
| 出包 | Agent repo **沒有 CI**，人在 188 手跑；主系統 CI `package` 段去 `/opt/guidant-ai-agent/.build/compose/` 抓最新一支 | `build_bundle.sh` ⑧ |
| 簽章 | Agent 包沒有自己的發布章；靠主系統 `release.json` 的 sha256 間接涵蓋 | `payload_type=release_manifest` 只有一種 |
| 上傳 | 只有主系統三個檔上 R2 `v1/<主系統版>/` | `publish_draft.sh` |
| 更新站 | `product_releases` 一條版本線；`min_agent_version` 只有「主系統要求 Agent 最低版」一向；後台 Agent 三欄拿 Agent 版號 1.4.x 比主系統版號 1.27.x 永遠 0 | `releases.ts` |
| 主系統本機 | `upgrade/bundles/guidant-ai-<主系統版>/agent/`，`host_version` 不得比現行主系統新；刪掉就沒有復原路徑 | `agent_rollout_service.py` |
| 推送 | 寫 rollout → `/update/notify` 叫醒心跳 → 心跳回應 `update` 鍵下發 | `_agent_update.py`、`update_sync.py` |
| 進度 | 畫面刷新時主系統去拉 `GET /update/progress`，快取在 worker 行程各一份 | `_PROGRESS_CACHE` |
| 結果 | 心跳 payload `upgrade_result`，最晚一個心跳週期 | `record_upgrade_result` |
| Agent 主控台 | 沒有更新頁；能力點只有 `configure／files／run-local-scan／net-diag／restart／download-files` | `CONSOLE_SCOPES` |
| 離線 | 只有主系統的離線單一檔；Agent 離線只能人到主機跑 `install.sh --upgrade` | — |

---

## 3. 目標設計 {#target nav="目標設計"}

### 3.1 一句話版 {#target-oneline}

- **兩條版本線、兩個包、兩個 R2 資料夾、兩種章**：主系統 `v<主版>/<版>/`＋`release_manifest`；Agent `agent/v<主版>/<版>/`＋`agent_release_manifest`。
- **Agent 不連外網**（決策者裁）：Agent 包一律由主系統從更新站拿、或由人離線上傳，主系統是唯一中繼。
- **主系統更新中心 Agent 區自足**：自己查 Agent 新版、預先下載、相容檢查、推給選定的台；不碰主系統版本。
- **Agent 主控台多一頁「更新」**：看版本、看主系統那邊有沒有新版、按「請主系統更新這一台」（主系統建單推這一台）、離線上傳更新包。
- **叫升級與回報走 FR-171 的通道**：主系統 `POST /update` 直推；Agent 升級每一步 `POST /agents/update/events` 當下送，送不到進 outbox。心跳回到只表存活。
- **相容兩向**：主系統清單 `min_agent_version`、Agent 清單 `min_host_version`；不相容在畫面擋。

```{.mermaid cap="圖 2 — 目標。兩條線分家；主系統仍是 Agent 包的唯一中繼。"}
%%{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
  subgraph ci["兩條 CI"]
    C1["BE CI → guidant-ai-1.28.0.tar.gz<br/>（不含 agent/）"]
    C2["Agent CI（新）→ guidant-agent-compose-1.5.0.tar.gz<br/>register-draft --product agent"]
  end
  subgraph site["更新站（兩條版本線）"]
    S1["product_releases (host, 1.28.0)<br/>min_agent_version"]
    S2["product_releases (agent, 1.5.0)<br/>min_host_version"]
    R1["R2 v1/1.28.0/"]
    R2["R2 agent/v1/1.5.0/"]
  end
  subgraph mac["決策者電腦"]
    K["guidant-release publish --product host|agent<br/>兩種 payload_type，各驗各的"]
  end
  subgraph host["客戶主系統"]
    H1["更新中心：主系統卡（不變）"]
    H2["更新中心：Agent 卡<br/>立即檢查 → 預先下載 → 相容檢查 → 推選定的台<br/>upgrade/agent-bundles/1.5.0/"]
    H3["收事件表 agent_update_events<br/>八格進度、失敗原因"]
  end
  subgraph agent["客戶 Agent"]
    G1["POST /update（FR-171 直推）→ 下載 → 寫升級單"]
    G2["主機 systemd 升級每步 → 事件當下送 POST /agents/update/events"]
    G3["主控台「更新」頁<br/>請主系統更新這一台／離線上傳"]
  end
  C1 --> S1 --> R1
  C2 --> S2 --> R2
  K -.蓋章.-> S1
  K -.蓋章.-> S2
  R1 --> H1
  R2 --> H2
  H2 --> G1 --> G2 --> H3
  G3 -- "請主系統更新這一台" --> H2
```

### 3.2 出貨分家（P1） {#p1}

| 項 | 現況 | 改成 | repo |
|---|---|---|---|
| Agent CI | 無，人手在 188 跑 | 新增 `.gitlab-ci.yml`，同 `guidantai-build` runner：`build_all.sh --with-compose` → smoke → `register-draft --product agent` → 上 R2 三檔（包、`.sha256`、未簽 `release.json`）。主控台前端在 node 容器內 build，不靠 188 裝 node（D4） | evidence-agent |
| 主系統包 | `build_bundle.sh` ⑧ 收 Agent 包進 `agent/` | 拿掉 ⑧ 與「正式包必須帶 Agent」守門；`--skip-prod-key-check` 不再有 Agent 那半邊 | BE |
| 更新站資料 | `product_releases` 一條線 | 加 `product` 欄（`host`／`agent`），主鍵改 `(product, version)`；Agent 列多 `min_host_version`（D1） | update-center |
| R2 路徑 | `v<主版>/<版>/` | 主系統不動；Agent `agent/v<主版>/<版>/<檔>`，一樣只由 `r2Key()` 拼（主版號資料夾仍是授權邊界） | update-center |
| 章 | `payload_type=release_manifest` | Agent 用 `agent_release_manifest`，payload 多 `min_host_version`；主系統端驗 Agent 包只認 `agent_release_manifest`、驗主系統包只認 `release_manifest`（D2） | update-center CLI、BE、evidence-agent |
| 客戶端端點 | `/v1/check`、`/v1/download-url`、`/v1/report` | 三支加 `product` 參數，不帶＝`host`（舊主系統不用改）；`/v1/report target=agent` 已有，改對 Agent 版本線比 | update-center |
| 後台 | 一張版本表 | 分「主系統」「Agent」兩個分頁；Agent 三欄改比 Agent 版本線（FR-167 待辦 63） | update-center |
| 版本說明 | Agent `notes.json` 隨包 | 改與主系統一樣：發布前貼更新站後台的 Agent 列 | update-center |
| 離線單一檔 | 只有主系統 | `guidant-agent-<版>.offline.tar.gz`（包＋簽好的 `release.json`），`publish --product agent` 順帶產 | update-center CLI |

### 3.3 主系統側 Agent 區（P2） {#p2}

雛形第 1～5 頁。規則：

- **檢查**：每 6 小時那次檢查同時問 `product=host` 與 `product=agent`；「立即檢查」兩個都問。
- **預先下載**：Agent 卡的「預先下載」把 Agent 包下到 `upgrade/agent-bundles/<Agent 版>/`，驗章（`agent_release_manifest`）、驗 sha256；下載人寫稽核。保留現行版與最新版兩份，其餘清掉（D7）。
- **相容檢查兩向**：
  - Agent 新版 `min_host_version` 高於主系統現行版 → Agent 卡顯示「需要先把主系統升到 X」，推送鈕灰。
  - 主系統新版 `min_agent_version` 高於某台 Agent 現行版 → 主系統卡標「必須更新 Agent」（FR-167 D16 既有行為），該台在 Agent 卡標「必須更新」。
- **推送**：沿用 `agent_update_rollouts`（先 1 台、再其餘、任一失敗其餘暫停、暫停／繼續／稍後提醒），只換兩件事：包從 `agent-bundles/<Agent 版>/` 拿、下發改 FR-171 的 `POST /update` 直推（打不通退回心跳備援一個版本，之後拔）。
- **「隨主系統跟進」**（FR-167 D16 預設勾）保留：主系統升完若 `agent-bundles/` 已有相容的 Agent 新版就照舊自動建列。
- **`host_version` 欄位退役**：Agent 包不再綁主系統版，改記 `agent_bundle_version`。

### 3.4 進度與結果即時（P3） {#p3}

依 FR-171「心跳只表存活」：

| 事件 | 誰送 | 送到哪 | 內容 |
|---|---|---|---|
| 下載進度 | Agent 容器（每 5% 或每 10 秒） | `POST /agents/update/events` | `{job_id, kind:"download", bytes_done, bytes_total}` |
| 主機每一步開始／結束 | Agent 容器監看 `upgrade-progress.json` 變動即送 | 同上 | `{job_id, kind:"step", n, name, status, started, ended, detail}` |
| 最終結果 | 新容器啟動時讀升級單結果即送（容器換掉那幾步由新容器補） | 同上 | `{job_id, kind:"result", status, version, error, detail}` |
| 送不到 | 進 Agent 既有 outbox（新 kind `update_event`），連上就補送、順序照 `created_at` | — | — |

- 主系統收進 `agent_update_events`（每台每 job 一列 upsert，存最後進度與全部步驟），畫面 3 秒輪詢改讀這張表，**不再去拉 Agent**；`_PROGRESS_CACHE` 與 `GET /update/progress` 退役。
- **失敗細節回得來**：`upgrade_order.sh` 不再丟 stderr，每步失敗把 stderr 最後 20 行寫進進度檔 `detail`（≤ 4 KB）；主系統存、畫面「看原因」顯示；仍過濾不可列印字元（D8）。
- 心跳 payload 的 `upgrade_result` 與回應的 `update` 鍵：主系統**接受但不再依賴**，留一個版本給舊 Agent，下一 minor 拔（跟 FR-171 的 `/update/notify` 同節奏）。

### 3.5 Agent 主控台「更新」頁（P4） {#p4}

雛形第 6～9 頁。決策者已裁「按一下＝主系統幫這一台更新」。

| 畫面元素 | 資料從哪來 | 動作 |
|---|---|---|
| 目前版本、上次更新時間與結果 | Agent 本機（升級單結果檔） | — |
| 「主系統上有 Agent 新版 X」／「已是最新」／「主系統不相容，要先升主系統」 | Agent 向主系統 `GET /agents/update/available`（控制面通行證），主系統回 `agent-bundles/` 裡相容的最新版與版本說明 | 按「請主系統更新這一台」→ Agent 打主系統 `POST /agents/update/request` → 主系統建一張 `manual` rollout 只含這台（按更新的人記 `agent-console:<操作者>`）→ 走 P2／P3 同一條路 |
| 進度八格、失敗原因 | Agent 本機進度檔（與送給主系統的是同一份） | — |
| 離線上傳 | 上傳 `guidant-agent-<版>.offline.tar.gz` → Agent 驗章（`agent_release_manifest`，公鑰隨 Agent 包帶 `release-keys/`）、驗 sha256、寫升級單 → 主機 systemd 照舊接手 | 要先過「主系統不相容」檢查：Agent 端拿不到主系統版本時只提示不擋（D5） |
| 權限 | 新能力點 `update`（D6） | 預設 local-admin 有；離線操作者可由主系統勾給 |

Agent 主控台事件（誰按了更新、上傳了什麼）照既有 `console_events` 送主系統稽核。

---

## 4. 與 FR-171 的分工 {#split nav="與 171 分工"}

| | FR-171 管「線」 | FR-172 管「貨」 |
|---|---|---|
| 做什麼 | 主系統→Agent 指令直推（派工／重啟／升級／換證／取消）、Agent→主系統事件專用端點、心跳只表存活、憑證自動換、守門與 client 收一份 | 出包／CI／R2／更新站版本線分家、相容兩向、主系統 Agent 區改版、進度事件的內容與畫面、Agent 主控台更新頁、離線上傳 |
| 172 直接用 171 的 | `POST /update` 指令端點與 `AgentDataPlaneClient`；Agent→主系統事件端點的簽章與守門規則；契約套件 `jedi-agent-protocol` 的版號 | — |
| 172 自己定的 | — | `agent_release_manifest` 章、`min_host_version`、`agent_update_events` 表與事件形狀、`GET /agents/update/available`、`POST /agents/update/request`、主控台頁與能力點 |
| 順序 | 批 1 守門與 client 收斂 → 批 2 指令通道與事件端點 | P1、P2 立刻開（不等 171）；P3、P4 等 171 批 2 合回 |
| 不互踩 | 171 不動 `agent_rollout_service` 的包來源與畫面 | 172 不另開第二條直推 client、不自己定事件簽章 |

兩案同一個討論週期拍板、各自一棵卡樹（兩張母卡）、工作坊可同時跑兩個；172 的 P3／P4 卡在 Stage 排在 171 批 2 之後。

---

## 5. 待決策 {#decisions nav="待決策"}

::: {.callout .decided}
**已裁（2026-11-01 決策者）**

- Agent **不**直接連更新站；包只從主系統拿、離線靠主控台上傳。
- Agent 主控台「向主系統發動更新」＝主系統幫這一台下載並推送，不是跳轉連結。
- 171 與 172 討論稿一起看、一起拍板。
- 172 討論稿附可點的雛形。
:::

::: {.callout .pending}
**D1 — 更新站怎麼存 Agent 版本線：加 `product` 欄，還是另開一張表？**

[**建議**：加欄。`product_releases` 加 `product TEXT NOT NULL DEFAULT 'host'`，主鍵改 `(product, version)`，Agent 列多 `min_host_version`。`visibleReleases`、`releaseAdoption`、後台、CLI 全部多一個 `product` 條件。另開表會讓發布流程、撤回、保留期、下載紀錄每支都寫兩份。]{.rec}
:::

::: {.callout .pending}
**D2 — Agent 的章怎麼分：同 `payload_type` 加 `product` 欄，還是另一個 `payload_type`？**

[**建議**：另一個 `agent_release_manifest`。理由：已出貨的主系統 1.27.x 驗章只認 `release_manifest`，拿到 Agent 清單會直接 `bad_signature` 拒收；若共用型別只加欄位，舊主系統會把 Agent 1.5.0 當成「主系統 1.5.0」走到版號比較才擋，差一道門。同一把發布私鑰簽兩種型別，不用多一把章。]{.rec}
:::

::: {.callout .pending}
**D3 — 相容矩陣兩向的擋法**

Agent 新版 `min_host_version` > 主系統現行版 → 擋推送（灰鈕＋說明）；主系統新版 `min_agent_version` > 某台 Agent → 標「必須更新」但不擋主系統升級（FR-167 D16 既有）。

[**建議**：照此。Agent 比主系統新是允許的（兩條線不綁版，FR-171 相容矩陣同一精神），只擋「Agent 要求的主系統最低版沒到」。`min_host_version` 由 Agent repo `scripts/build/min_host_version` 一行檔決定，沒有＝不限。]{.rec}
:::

::: {.callout .pending}
**D4 — Agent CI 跑在哪、主控台前端怎麼 build**

188 runner 沒有 node，主控台前端現在在 Mac build 再傳過去（runbook 2.2）。

[**建議**：Agent CI 跑同一台 188 runner（`guidantai-build` tag），主控台前端在 `node:20` 容器內 build（跟 BE CI 的 `build_fe_image.sh` 同手法），FE repo 以 submodule 或 CI 變數指定 commit 拉進來。188 不裝 node。]{.rec}
:::

::: {.callout .pending}
**D5 — Agent 主控台離線上傳的驗證與相容檢查**

Agent 端要能驗 `agent_release_manifest` 的章，公鑰要隨 Agent 包帶（現在只在主系統包 `release-keys/`）。相容檢查需要主系統版本，離線時拿不到。

[**建議**：Agent compose 包加 `release-keys/`（與主系統同一批公鑰與 `PROD_KIDS`），升級時換新；離線上傳時 Agent 拿不到主系統版本就只提示「無法確認相容性，請確認主系統 ≥ X」不擋——離線場景本來就是人在現場。]{.rec}
:::

::: {.callout .pending}
**D6 — 主控台權限：新能力點 `update`，還是沿用 `restart`？**

[**建議**：新 `update`。重啟是分鐘級可逆、更新是換版本不可逆，給離線操作者的授權面不同；`CONSOLE_SCOPES` 加一項、主系統 Agent 管理頁勾選清單多一格、local-admin 預設有。]{.rec}
:::

::: {.callout .pending}
**D7 — 主系統本機 Agent 包目錄與保留**

`upgrade/bundles/guidant-ai-<主系統版>/agent/` → `upgrade/agent-bundles/<Agent 版>/`。

[**建議**：保留「各台現行版中最舊的那版」與「最新版」兩份，其餘清；升級到 1.28 時把舊 `bundles/<版>/agent/` 裡的那支搬過去當第一份。這同時解掉 FR-167 待辦 42（被刪就重下載）。]{.rec}
:::

::: {.callout .pending}
**D8 — 失敗原因回多少**

[**建議**：每步 `detail` ≤ 4 KB、只收 stderr 最後 20 行、過濾不可列印字元、主系統再截一次；畫面預設收起、「看原因」展開；診斷包照舊收全量 journal。不送整段 journal（主機路徑與帳號會進主系統稽核表）。]{.rec}
:::

::: {.callout .pending}
**D9 — Agent 區「預先下載」要不要獨立成一步**

主系統卡現在是「預先下載 → 更新」兩步（FR-167 已裁名稱不改）。Agent 卡現況是「推送」一步，包早就在主系統包裡。

[**建議**：對齊成兩步。Agent 包 1.8 GB 不小，下載誰按、何時下載要有紀錄；且離線客戶走上傳到主系統這條路時本來就分兩步。雛形第 1～2 頁照這樣畫。]{.rec}
:::

::: {.callout .pending}
**D10 — 舊客戶怎麼過渡**

現場 1.27.10 的主系統包裡還有 `agent/`；下一版主系統 1.28.0 不帶。

[**建議**：1.28.0 主系統升級第 7 步把 `bundles/1.27.10/agent/` 那支搬到 `agent-bundles/1.4.8/`；更新站 Agent 版本線第一筆登 1.4.8（補登既有包，不重出）；1.5.0 起走新 CI。舊主系統 1.27.x 問 `/v1/check` 不帶 `product` 照舊拿主系統線，不受影響。]{.rec}
:::

::: {.callout .pending}
**D11 — FR-167 搬過來的小待辦哪些併入本案**

| 待辦 | 內容 | 建議 |
|---|---|---|
| 60 | 八格讀錯欄位（FE 已修未出包）、結果晚 5 分 | 併入 P3，整段重做 |
| 63 | 更新站後台 Agent 三欄永遠 0 | 併入 P1 |
| 42 | 刪掉 `bundles/<版>/agent/` 沒復原路徑 | D7 順帶解 |
| 58 | Agent 主控台檔案頁把 PDF 快取列成證據 | 併入本案當一張 Agent repo 小卡（同 repo、不撞檔） |
| 61 | 主控台 CINC 網址型 Profile 下拉空 | **不併**：B 方向要改主系統 Profile 匯入（解析時留檔），屬檢測 Profile 功能，另開小 FR 或掛 FR-060 續作 |
| 62 | 升級換接後 compose 標籤指舊目錄 | **不併**：主系統自升級的事，留 FR-167 下一版 |

[**建議**：照表。]{.rec}
:::

::: {.callout .pending}
**D12 — 拆卡順序與批（約 18 張、5 repo）**

P1：更新站 D1 加欄＋端點 `product` 參數（1）→ CLI `--product`＋`agent_release_manifest`＋離線檔（1）→ 後台兩分頁＋三欄（1）→ Agent CI＋`min_host_version`＋`release-keys/`（1）→ BE 拿掉 ⑧（1）。
P2：BE Agent 線（檢查／下載／相容／`agent-bundles/`／`host_version` 退役）（2）→ FE Agent 區 v3（1）→ 主系統升級第 7 步搬包（1）。
P3（等 171 批 2）：契約加事件形狀（1）→ Agent 事件推送元件＋outbox kind＋`upgrade_order.sh` detail（2）→ BE 收事件表＋退役拉進度（1）→ FE 八格改讀表＋看原因（1）。
P4（等 171 批 2）：主系統 `available`／`request` 兩支（1）→ Agent 兩支端點＋能力點（1）→ 主控台 `Update.vue`（1）。
P5：e2e＋文件＋結案出包（1～2）。

[**建議**：照此。釘版套件 `jedi-agent-protocol` 有改動，P3 批 1 前要環境卡（可與 171 共用同一張）。詳細拆卡表等裁完在 design.md 用 `cards.py` 驗過再寫。]{.rec}
:::

---

## 6. 不做什麼 {#out-of-scope nav="不做"}

| 不做 | 理由 |
|---|---|
| Agent 直接連更新站 | 決策者裁；Agent 是客戶內網掃描機，不出網是賣點也是資安前提 |
| Agent 差異包、只換單顆容器 | FR-167 已裁不做；Agent 包本來就是整組映像 |
| Agent 自動裝新版 | 與主系統同原則：任何版本都不自動裝，要人按 |
| 主系統版本線改名或搬 R2 資料夾 | 已有 10 版在 `v1/`，搬了舊主系統找不到 |
| 第二把發布私鑰專給 Agent | 一把章兩種 `payload_type` 已能防互相冒充；多一把章多一個保管問題 |
| 心跳備援長期並存 | 心跳的 `update`／`upgrade_result` 只留一個版本給舊 Agent，與 FR-171 `/update/notify` 同節奏拔 |

---

## 7. 風險與陷阱 {#risks nav="風險"}

| 風險 | 怎麼防 |
|---|---|
| 兩條版本線被互相冒充（拿 Agent 1.5.0 清單餵給主系統） | 兩種 `payload_type` 各驗各的；R2 路徑不同；更新站 `product` 欄進主鍵 |
| 171 通道卡延後，172 的 P3／P4 跟著卡 | P1、P2 先跑；P3／P4 卡在 Stage 排 171 批 2 之後，工作坊自己等 |
| 容器換掉那幾步事件沒人送 | 新容器啟動時讀進度檔補送「步驟＋結果」；主系統按 `job_id` upsert 不怕重送 |
| 事件送到但主系統多 worker 各寫各的 | 寫 DB 不寫行程快取；畫面只讀 DB |
| Agent CI 跟 BE CI 同時在 188 build 互撞（Nuitka linker crash） | CI 設 `resource_group: build-188`，兩條 CI 串行 |
| 1.28 主系統沒帶 Agent，客戶以為「Agent 不見了」 | 第 7 步搬包＋更新站補登 1.4.8；Agent 卡文案「隨主系統附帶」改「由主系統下載」 |
| 離線上傳的包被人塞假的 | Agent 端驗章＋sha256，公鑰隨包帶；`PROD_KIDS` 同一批 |
| 登記型共用檔（`CONSOLE_SCOPES`、`result_outbox` kind、contract pyproject、BE 路由表、FE console `router.js`）同批撞 | 拆卡時各自不同批，或最早那張卡一次掛完 |

---

## 8. 參考 {#refs nav="參考"}

- 需求原話與現況盤點：[requirement-notes.md](requirement-notes.md)
- 雛形：[prototype/index.html](prototype/index.html)
- FR-167 現況規格：[Agent 跟進規格](../FR-167-2610-online-update-channel/agent-rollout-spec.md)、[更新站資料表](../FR-167-2610-online-update-channel/update-site-schema-spec.md)、[簽章規格](../FR-167-2610-online-update-channel/release-signing-spec.md)、[發版手冊](../FR-167-2610-online-update-channel/release-runbook.md)
- FR-171 討論稿：[通道、守門、憑證](../FR-171-2610-agent-command-channel/discussion.md)
- 裁示記憶：`docs/claude/memory/feedback_no_heartbeat_piggyback_messaging.md`
