# FR-191 Agent 安裝包自助下載：交接紀錄（append-only）

> 每棒追加一個 block，既有 block 不改。發現舊 block 有誤，在新 block 的「推翻了什麼」寫明。

---

## 首腦棒（2026-11-09～11-10，session「[R3]Agent下載點」，Fable 5.1）

**做了什麼**

- 探脈：Agent 包在主系統 `agent-bundles/<版>/`（更新中心預先下載後落地，旁有 `.sha256` 與簽章清單）但人拿不到；更新站預簽網址寫死 15 分鐘、客戶主機多無外網；既有短效簽章票（軸⑤）ttl 是驗證參數；FE `navigator.clipboard` 在 http＋IP 不存在。
- design.md D1～D11、FR 登記、FR-172 README 標「新需求另開」。
- Notion 母卡 CM-3761、子卡 CM-3762（BE／批 1／opus high）、CM-3763（FE／批 2／opus high／Repo fe）、CM-3764（文件／批 3／sonnet medium），四欄齊。
- 決策者問「要不要擋 user 一直點下載」→ 補 D11（FE 10 秒鎖＋先探測；BE 同票 2 條、全站 3 條、發票每分鐘 10 次），append 進兩張子卡。
- 決策者問「可以跟 FR-188 一起跑嗎」→ 逐張撈 FR-188 十九張 main／fe 卡的改檔清單，撞四處；BE 卡改成完全不碰那六個檔（三端點全在新檔、時限改獨立端點、稽核 reuse 事件碼 6251）、FE 卡加「CM-3741 合回才動 AgentRolloutPanel」前置。design 加 §6。
- 工作坊 53 跑完三棒後抽查驗收：三個 commit 均在 `feature/FR-172` HEAD、禁改檔零異動、BE 141 條與 FE 136 條測試綠；DEV 放測試包 1.5.0 實打十四項全過（細節在 FINAL-SPEC §4.2）。
- 重啟 DEV BE（kill 舊 PID 86499 後起新的），FE vite 熱更新未重啟。
- 預先收尾：FINAL-SPEC、LOG、README、STATE。

**commits（BE，feature/FR-172）**

- `79e58a387` design D1～D10、README、STATE、FR 登記、FR-172 補註
- `1d1c9b105` design D11 與 §6 避撞；STATE 同步
- 本棒收尾 commit（FINAL-SPEC、LOG、README、STATE）

**關鍵決策**

- 包從主系統本機供檔，不給更新站 R2 連結（寫死 15 分、客戶主機多無外網）。
- 兩個入口共用一個元件；產生連結只要讀權限（下載不改系統狀態）；時限參數放更新中心。
- 票綁 `<版>:<kind>`，時限不寫進票、由驗票當下設定決定（調整時限立刻影響已發連結，接受這個副作用，換來不用另立 token 表）。
- 併發限「同時幾條」不限「總共幾次」，續傷不受影響；Redis 連不上放行（防佔線程，不是授權）。
- 不動前門 nginx、不做 X-Accel 直送（頻寬真被吃滿再議）。
- 編號原登記 FR-190，與同日另一 session 的「Agent 主控台權限能力點化」撞號，改 FR-191。

**推翻了什麼**

- 卡主文「`agent_overview()` 加 `bundle_link_ttl_minutes`」與「新增事件碼 6252」被 FR-188 避撞段推翻，改獨立端點與 reuse 6251。
- 卡主文「`AgentRolloutPanel.vue` 只加兩行」實作為三行（動作列包在 `canWrite && !updating` 的 v-if 內，唯讀管理員會看不到鈕，另加一行 v-else）；首腦接受。
- 卡主文「時限欄位放 Agent 區說明旁」實作改放進「取得安裝包」對話框內（Panel 接近 800 行上限，且改時限與拿連結是同一件事）；首腦接受。

**踩到的坑**

- Flask `send_file` 預設 direct_passthrough，`call_on_close` 不會被叫，名額放不回去；開發伺服器在客戶端中途斷線時也跳過 close。修法是串流包一層 generator 在 finally 釋放，並補「中途斷線」測試。單元測試原本假綠。
- PrimeVue 3.53 `InputNumber` 先 emit blur 才寫回 v-model，blur 當下讀到上一個值，要 `await nextTick()`；也不能設 min／max，會在 blur 時把 0 夾成 1 默默送出。
- 前端到期判斷不能直接比伺服器 `expires_at`，使用者電腦時鐘快了就一拿到就過期。
- 登入端點欄位是 `username` 不是 `login_name`；FE dev server 在 5180 不是 5173。
- Redis 密碼含 `!`，zsh 在 `-a` 參數會吃掉，用 `REDISCLI_AUTH` 環境變數。
- 驗收開始時 repo 正處於另一 session 的 merge 衝突中（`docs/features/index.html`），我的 design 改動退出暫存區等它收完再 commit。

**DEV 環境留下的東西**

- `.env` 末尾新增 `GUIDANT_UPGRADE_DIR=~/guidant-dev/upgrade`、`GUIDANT_RELEASE_KEYS_DIR=~/guidant-dev/release-keys`（路徑，非憑證），備份 `.env.bak-20261110102535`。
- `~/guidant-dev/upgrade/agent-bundles/1.5.0/` 三個檔與 `~/guidant-dev/release-keys/eb5eb520d9c608cb.pub`，來源 `test/fixtures/agent_release/`。要清就連目錄刪掉並把 `.env` 兩行拿掉。
- runner 測 token 對話框時建的 4 把註冊 token 已撤銷；時限已回 30。

**下一棒接手要知道**

- 母卡 CM-3761 是否收 Done 等決策者。
- 手冊截圖等真包；決策者已裁無圖版先收。
- 真包三項驗證（FINAL-SPEC §4.2 打折處）等 STG／POC 預先下載後跑。

---

## 首腦棒續（2026-11-10，同 session，Fable 5.1 → Opus 5.5）

**發生什麼**

- 決策者實測 v1.0 畫面，指出三件事：時限欄位不該在下載對話框、不該讓客戶改；大小 1 KB 不對（那是首腦放的測試包）；連結應該直接給 Cloudflare R2。
- 首腦先回「兩條都給」的折衷，決策者澄清情境：「客戶還沒裝 Agent 的時候，要有地方可以下載安裝包」，裁直接給 R2。
- 決策者：「你重新開卡跟改文件，差太多了」。

**推翻了什麼**

- v1.0 D1（主系統本機供檔）：誤判情境。以為是「主系統已有包、給人拿」，實際是「還沒裝 Agent、主系統通常也沒包」。v1.0 要先預先下載 1.7 GB 才有東西給，最需要時最不好用。首腦 11-09 排除 R2 的兩個理由（寫死 15 分鐘、客戶主機沒外網）都不成立：時限可以改更新站；下載者本來就是管理員的電腦，不是客戶 Agent 主機。
- v1.0 D4（主系統可調時限）：時限該由網址簽發方（原廠）控，不是客戶。
- v1.0 D11 的併發上限與前端探測：下載不經主系統後不需要。
- 首腦先前的「兩條都給」折衷：多一套來源與時限，客戶看到兩組網址不知道選哪個。

**做了什麼**

- 實查：更新站 `/v1/download-url` 只認 artifact=ops 且 Agent 不准帶；預簽時限常數 15 分鐘；R2 上 Agent 1.7.4 的包、`.sha256`、`release.json` 都在；更新站後台有 Access 保護、無設定表；主系統 `update_site_client.download_url()` 有 R2 主機白名單；`AgentReleaseService.check()` 會寫 `agent_last_check`。
- 寫 `design-v2.0.md`（D1～D10＋與 v1.0 的異動表），`design.md` 加「已被推翻」提示。
- 開 CM-3800～3803（批 4～7），母卡 append 改判說明。
- FINAL-SPEC 改成「v2.0 實作中」短版（v1.0 版的定版內容已失效，不能留著當現況）；README 三段、STATE 改寫。
- 移除 DEV 為 v1.0 驗收加的 `.env` 兩行與 `~/guidant-dev/` 測試包，重啟 BE。

**教訓**

- 需求一句話裡的「下載點」要先問「誰、在什麼時候、手上有什麼」。首腦用「主系統已經有包」當前提，整套設計跟著歪。
- 「可以調整的參數」要問「誰可以調」。首腦預設是客戶管理員。
- 首腦用「沒外網」排除 R2 時，把下載者想成 Agent 主機；實際下載者是管理員的電腦。
- 「產品管理員」在專案沒有對應角色，首腦假設是原廠。待決策者確認。

---

## v2 第 4 棒 文件棒（2026-11-10，CM-3803，Sonnet 5.5）

**做了什麼**

- 手冊：快速版第 3 步與 03 章 3.2 取包句換成標準句（R2 預簽網址、下載電腦要能連外網、時限由原廠設定、未連線更新站向原廠索取）；失敗句改「回主系統重新產生連結下載」（03／10／11、快速版三處）；07 開頭、README 的「從主系統下載」改成「取得」；04 章提示補「原廠」。FR-189 README 取包來源一行同步。
- FINAL-SPEC 整份改寫成 v2.0 現況；README 進度、STATE 更新。

**推翻了什麼**

- 沒有推翻前棒結論。FINAL-SPEC「v2.0 實作中」短版被定版取代。

**坑／打折**

- 手冊截圖不放：CM-3802 的兩張是模擬環境（示意 R2 主機、icon 字型缺），決策者已裁先不做。
- 驗證都沒有對真 R2 與真授權照；FINAL-SPEC §4、§5 明寫。
- grep 剩一處「從主系統下載」在 07 章 7.8（Agent 升級包由主系統供應），與取包無關，保留。

---

## 首腦總驗與結案（2026-11-10，Opus 5.5）

**做了什麼**

- v2 四張卡（CM-3800～3803）工作坊跑完。首腦核：commit 只在三個 repo 的 `ws-CM-3761`、未合回；試合併三 repo 無衝突；舊 v1 程式拿乾淨；BE 376／FE 145／更新站 187＋101 條測試綠；DEV `agent_last_check` 未被動到、稽核 6251 有一筆 source=r2。端到端實打採信 CM-3801 回寫（同環境，未重做）。
- 決策者裁合回、push、母卡 Done。合回前確認三 repo 無進行中 merge、別人未提交的改動與合回檔不重疊。三 repo `merge --no-ff ws-CM-3761` 無衝突；重啟 DEV 後端，實打新端點回 412001（DEV 沒設更新站，預期）、兩支舊端點 404。
- push 前 fetch 三 repo，遠端無他人新 commit。BE push 時多帶一個別的 session 在合回後提交的 `a8583fe29`（重產出貨基線補 FR-190 能力點，決策者裁示），確認 FR-191 合回 commit 在內。
- 母卡 CM-3761 改 Done（四張 v2 子卡原本已 Done）。

**查到的事**

- 正式更新站最後部署 2026-11-04，FR-191 的後台「設定」分頁與 `artifact=sha256` 都還沒上線。決策者裁不另部署 dev Worker、隨下一次上 PRD。
- 守衛 `test_module_boundaries.py` 的紅與 FR-191 無關：主線兩支是 FR-190 能力點未進出貨基線（已被 `a8583fe29` 補上），工作坊分支多一支是落後主線、對不上新釘的 `jedi-agent-protocol 0.6.0`。

**上 PRD 的陷阱**

- 正式 D1 要先套 `0007_site_settings.sql` 再部署 Worker，否則讀設定時表不存在，所有下載網址（含主系統升級包）都會 500。

---

## 正式更新站部署與上線缺陷修正（2026-11-10，Opus 5.5）

**做了什麼**

- 決策者裁「部署上去」。照 `docs/system-design/update-center/04-作業程序.md`：正式 D1 先套 `0007_site_settings.sql`，再 `npm run deploy`。部署前用真 R2 金鑰簽帶 `response-content-disposition` 的網址實打 R2，確認接受、續傳正常（這是唯一會影響主系統升級包下載的改動）。
- 部署第一次被 predeploy 擋下：vitest 主設定自訂 `exclude` 蓋掉預設排除，掃進工作坊 `.worktrees/` 各自的 node_modules，434 檔 6006 條、72 條失敗。update-center `9ed50a1` 加排除 `.worktrees/**` 後 187＋101 條綠，重新部署成功（版本 `80ccaecb`）。
- 用 190 主機授權身分（從 190 DB 唯讀取授權原文、容器 machine-id 算指紋，190 本身零改動）實打正式站：主系統 check、升級包網址、真 R2 下載與續傳都正常；Agent 線 `0.0.0` 回 none。
- 決策者裁「開卡、直接修」：開 CM-3819（批 8），BE `3dd1547de` 抽常數 `NO_AGENT_BASE = "1.0.0"` 三處共用，正式站用主系統真程式碼實打通過，push，DEV 重啟，卡改修正待驗證。

**踩到的坑**

- Agent 線所有版本 `min_base_version` 由更新站補成 `<主版>.0.0`；零 Agent 時報 `0.0.0` 會被 `planPath` 濾光。DEV fixture 的 min_base 是 0.0.0，四棒驗收都假綠。教訓：版本路徑類邏輯要拿正式站真資料驗一次。
- Cloudflare 機器人防護擋 Python urllib 預設 User-Agent（1010）。主系統用 httpx 不受影響，手寫驗證腳本要自帶 UA。
- update-center 主設定 `exclude` 會蓋掉 vitest 預設排除清單；工作坊在 repo 內開工作區後才會踩到。

---

## 本機驗收環境與結案（2026-11-10，Opus 5.5）

- 決策者要在本機看畫面。DEV 授權與身分票都綁別台機器（`f2ef9354…`），本機是 `67ac366f…`，正式站也不認 DEV 金鑰。走方案 A：本機起 wrangler dev 模擬更新站（灌正式站 Agent 1.7.4 的真清單與簽章、模擬 R2 放真 `.sha256` 與佔位包）；用本機 DEV 授權中心私鑰簽一張綁本機的身分票（不寫 LC 資料庫），只替換 DEV 授權列的 `update_ticket` 一欄；`.env` 加 `UPDATE_SITE_URL` 與 `GUIDANT_RELEASE_KEYS_DIR`（後者指 repo 內附公鑰，`.env.sample` 明載開發機這樣設）。後端 API 與無頭瀏覽器都打通。
- 決策者「不用了，直接合併回去標 Done」。還原：原票寫回（md5 核對）、`.env` 兩行拿掉（與加入前逐字相同）、wrangler 程序強制結束（一般 kill 沒收掉 npm exec 父程序）、暫存票檔刪除、後端重啟。CM-3819 與母卡 Done。
- 推送時發現本地多 48／21／4 筆是另一個 session 的 FR-188 合回，未代推。

**踩到的坑**
- DEV `.env` 根本沒有 `UPDATE_SITE_URL` 行，不是空值；改 `.env` 前的檢查要分「缺行」與「空值」。
- DEV 授權的 `update_ticket` 是 JSON 信封，payload 為 zlib 壓縮的 base64，不是 armor 格式。
- 前端登入後立刻切路由會被守衛導回首頁，要等 `/users/me/capabilities` 回來；整頁 reload 會掉登入態。
- `npm exec wrangler dev` 的父程序不吃 SIGTERM，要對確切 PID 補 SIGKILL。

---

## 文件收尾（2026-11-10，Opus 5.5）

決策者「要收尾了，文件都有做了嗎」。逐類開檔核對，補了五處：

- **FINAL-SPEC**：CM-3803 在正式站部署前寫的，四處過時（真 R2 待補、驗證打折、沒做完三項）。改寫為上線後現況，驗證改列正式站實打七項。
- **design-v2.1**：v2.0 的 D3、現況、流程圖寫零 Agent 送 0.0.0，正是 CM-3819 修掉的錯。定稿不回改，開 v2.1（完整可獨讀＋「與 v2.0 的異動」），v2.0／v1.0 頂部指向 v2.1。
- **頁面規格（spec-site）**：更新中心與 Agent 管理兩頁原本完全沒寫這功能。更新中心補功能總覽 #13、UC-PU-05、權限、API、資料對應、檔案地圖、外部依賴、兩條已知坑；Agent 管理補功能 #3a、檔案地圖。`mkdocs build --strict` 通過。
- **更新站系統說明**：原本寫死 900 秒、artifact 只有 ops、五個分頁、18 條路由、migration 到 0006。對照實作補 05／06／07／08 四章，04 章新增「調整下載網址有效時間」程序，02／03／12／README 同步數字。
- **README**：「先看哪一份」補頁面規格、客戶手冊、原廠維運三類文件位置。

核對過不用改的：手冊 03 章 3.2、04 章 4.1、快速版第 3 步已是 v2 說法；07 章 7.8「Agent 只從主系統下載」講的是 Agent 升級，屬事實。

**踩到的坑**
- 錨點腳本 `fix_anchor_ids.py` 會順手改別人的頁（這次在 FR-190 的 `agent-console.md` 補一個 id，因為被本案跨頁連結到）；spec-site `site/` 產物有其他 session 改 md 沒 build 的殘留，只收本案頁面的產物。
- git 對中文檔名預設八進位轉義，批次 `git add` 要用 `status --porcelain -z`。
