FR-064 交接 LOG(append-only)

每棒收工 append 一個 block,寫完永不改。發現舊 block 有誤 → 在新 block「推翻了什麼」欄更正。

§1

第 1 棒 — 2026-08-15 晚 ~ 2026-08-16 午 — 首任首腦(協調者)

commits: BE eba7f5a0..4fe7638a 共 16 支(含 design 7ceb7188 前置);LC 6ce339e/57c6566/076c06b。兩 repo 皆已 push 與 origin 同步。 Notion: CM-1208~1229 卡樹建立;CM-1215~1223/1226/1227/1230 → 修正待驗證;CM-1224/1225/1228/1229 未派;CM-1188 母案續掛。

做了什麼:

  • 接手戰線 → CM-1207 驗收收 Done → big-feature-workflow 全程(探脈×2 → 討論稿 90828118 → D1–D10 拍板 → design.md 7ceb7188 → 22 張卡樹 CM-1208~1229)
  • 五波 runner 派工與驗收(.1/.2/.3/.4/.6ab 全過,含夜間授權自主兩棒)
  • 188 build 事故鏈排除(branch 未切/LC 三連炸/token 未進 shell)
  • 簽章鏈首次全通實證(真 dist manifest → LC 簽 → BE 驗 → 抽檔 sha256 全中)

決策: D1–D10(詳 design.md §2)。D8 由 45min 改 4h+jitter(user 裁示,容錯靠 D10 二次確認與 jitter 防節奏);D9/D10 討論中新增;unlock 走離線信封、棄 activation_code 線上模型。manifest 缺席行為=dist 強制/源碼跳過(runner 自定,user 未明示追認——接手注意)。

教訓:

  1. 「本機測試綠」≠「部署機會產出」——188 停舊 branch 讓 build 跑舊腳本,驗收漏了「碼到得了現場」這環,被 user 指正「呼嚨」;未實證清單要集中列給 user 看,不散落各棒回報。
  2. env 變數自創撞名(LC_API_TOKEN vs 既有 LICENSE_CENTER_API_TOKEN)+錯誤預設 port——已固化 CLAUDE.md 鐵則(f8d1632b)。
  3. bash 腳本不讀 .env——user token 填了卻被判未設定,已修 fallback(4fe7638a)。
  4. LC 部署三連炸(依賴/unit 過時 exit0/缺 migration)→ 四步 SOP 固化(memory+LC CLAUDE.md 076c06b)。
  5. 稽核判準「套件內有 getenv ≠ 產品要填」(GITLAB 誤收案例)。

推翻了什麼: C 棒自創參數名(6f37019e 廢除);.env 稽核首輪誤收 9 支(4b2234ac 移除)。

在途: user 正重跑 188 build(本次起自動送簽);AGENT_CERT_VALID_DAYS 已處置(9508c7f8)但其他 env 項 user 另 session 處理中。

留給下一棒: build 完先做真產物集中驗證(smoke+gate 真驗+改檔拒啟),再派 .5/.6.3。


§2

第 2 棒(T-6.3/CM-1228)— 2026-08-16 午後 — runner

commits: LC 7c2974d(tamper 事件接收端點+migration+tamper_report 模組);BE d181fdc6(best-effort 回報模組+兩處掛點)。兩邊皆未 push。

做了什麼:

  • LC:POST /api/internal/report-tamper(走 62.10 internal API 形狀),新表 tamper_events,migration 4328af2b0ece 已套 DEV。
  • BE:common/integrity/lc_report.py(stdlib-only、3s timeout、全域 except),掛進 startup_gate._record_and_refuseruntime_check._respond_to_tamper
  • 三情境實測(非模擬):源碼模式模擬 tamper → LC dev 收到並落 DB(清除測試資料);LC 位址不通 → 3.01s 內返回無 traceback;未設 token → 0.0003s 靜默跳過。
  • 回歸:BE 50 支 integrity 測試全綠;LC 360 支通過(另 2 支失敗屬其他並行 session 進行中改動,非本卡)。
  • CM-1228 回寫「修正待驗證」+白話補充。

教訓:

  1. 同一 LC checkout 有另一 session 並行動 modules/integrity/(FR-064.5 T-5.1 unlock token 簽發)——非 worktree,即時撞了 models.py/repository.py 兩次被覆寫。發現後把本卡模組改放 modules/tamper_report/,未動對方任何檔案。後續派工若可能撞路徑,建議指定 worktree 或明確劃分模組命名
  2. Alembic autogenerate 在多 session 併發改 model 時會把「尚未落地的別人的表」一併抓進來——已手動裁剪 migration 只留自己的 tamper_events

推翻了什麼: 無。

留給下一棒: .5 unlock 全鏈(CM-1224/1225)看起來已有另一 session 在跑(modules/integrity/unlock.pycommon/integrity/unlock.py 皆已出現改動但未經本棒驗證,接手者需自行盤點其進度)。T-6.3 完成後 .6 子需求全數修正待驗證,可推進 .7 E2E 收口前的整體 push 齊備檢查。


§3

第 3 棒(T-8.4/CM-1234)— 2026-08-16 晚 — 文件 runner(D11 落地)

commits: BE(單支,純文件),git add 顯式列檔,訊息帶 CM-1234。

做了什麼:

  • 落地 2026-08-16 拍板的 D11 三件事到 design.md:①鎖定溝通改 B 方案(lockdown 殼,取代純 exit)②forensic 證據規格擴充(mtime/ctime/owner/size+依來源分級觸發脈絡)③邊界明文+契約措辭建議(「是誰改的」應用層不可知,條款綁「完整性驗證失敗」而非「查明行為人」)
  • design.md:§4.4 整段改寫(4.4.1–4.4.4);§2 補 D11 決策列(含被排除的 A 純 exit/C 半套方案與理由);§5 新增 FR-064.8 段(T-8.1–T-8.4,標注依賴關係);檔頭 lede/變更紀錄補一行
  • deployment-env.md(FR-063 資料夾,本案適用落點——找不到 FR-064 自己的部署文件,沿用 FR-063 落地版部署文件)新增 §9「客戶端主機層留證建議」:auditd 監控規則範例、log 外送建議、邊界明文;原 §9 交付前檢查清單順移為 §10
  • 跑 doc-site-build 重 build FR-064 站,成功

決策: 無新決策,純落地既定拍板內容,不加料。

教訓: 無。

推翻了什麼: 無。

留給下一棒: T-8.1(lockdown 殼實作)/T-8.3(forensic 擴充)依賴 CM-1224/1225/1228 主鏈程式碼完成後才能開工;T-8.2(FE 認錯誤碼跳轉)契約已定可先行派工。


§4

第 4 棒(T-8.2/CM-1232)— 2026-08-16 晚 — FE runner

commits: FE repo(compliance-manager-fe,branch feature/FR-064)單支 de23772,git add 顯式列檔,訊息帶 CM-1232。未 push。

做了什麼:

  • src/config/axios/axios-interceptors.js:response interceptor 新增 INTEGRITY_503001 分支(形狀對齊既有 LICENSE_403001 靜默分支)——不 emit axios-error(不 toast)、把 BE lockdown 殼回應 data.machine_fingerprint/tamper_event_id/detected_at 透過 query 帶進 router.push({name:'system-locked', query:{fp,eid,at}});模組級旗標防同一瞬間多隻請求重複導向。
  • src/config/router/index.js 新增 /system-locked route(不掛 AppLayout、無 requiresAuth——被鎖時可能拿不到 token)。
  • 新頁 src/views/default/SystemLocked.vue:視覺沿用 NotFound.vue 品牌光暈骨架,顯示「系統已鎖定」+聯繫原廠 hint-bar+三值(機器指紋/事件編號/偵測時間,前兩者帶複製按鈕,複製邏輯抄 LicenseActivation.vuecopyMachineCode 形狀);query 缺值時 fallback「請洽系統管理員查閱伺服器紀錄」;頁面不發任何 API。
  • 新 i18n 檔 system-locked.json(en/zh-tw,system_locked namespace),已在 config/locales/index.js 註冊;error-code.json(en/zh-tw)補 INTEGRITY_503001 條目(雖不 toast,仍照 BE error code 同步 FE i18n 慣例補齊)。

決策: 無新決策,純照派工 prompt 定死契約實作。

驗收(dev server + chrome devtools MCP 實測):

  1. 直接 GET /system-locked?fp=...&eid=...&at=...——三值正確渲染,複製按鈕點擊成功彈「已複製到剪貼簿」toast,console 無 error。
  2. 直接 GET /system-locked(無 query)——正確落 fallback 文案。
  3. 直接呼叫 axios 已註冊的 response interceptor rejected handler、餵入模擬 BE lockdown 殼回應(error_code: INTEGRITY_503001)的 fake error——確認未 emit axios-error(無 toast)且 router 正確跳轉並帶對三個 query 值,證實「BE 回 503 → FE 認碼 → 跳鎖定頁」整條路徑成立。
  4. 登入頁 smoke(渲染、無 console error)確認未影響既有流程。

教訓: 無。

推翻了什麼: 無。

留給下一棒: T-8.1(lockdown 殼實作,BE)/T-8.3(forensic 擴充)仍待 CM-1224/1225/1228 主鏈程式碼完成後才能開工;T-8.2 完成後,一旦 T-8.1 殼落地即可整鏈端到端驗(真殼回 503 → 真瀏覽器打真 API → 跳鎖定頁)。


§5

第 5 棒(T-5.1+T-5.2/CM-1224+CM-1225)— 2026-08-16 晚 — unlock 全鏈 runner

commits: LC 651db9c(CM-1224)/BE f8133e72(CM-1225)。兩 repo 皆 branch feature/FR-064,顯式 git add,未 push。

做了什麼:

LC 側(T-5.1)——新 modules/integrity/(models/repository/service)+SSR 後台頁 /unlock + CLI issue-unlock-token 保底+ migration 49629d9e5d99(表 unlock_token_issuance只套 DEV)。payload 依 design §4.3 定死,走 6ce339e 的泛用 sign_payload(無第二套簽章實作)。效期預設 72h(1–336h)。service 層守門(三入口共享):指紋 64 hex、event_id evt- 前綴、效期範圍。下載檔名固定 unlock.token不套 v3 armor(armor 標頭寫 GUIDANT LICENSE,會讓客戶誤當授權照上傳)。log 依安全鐵律:指紋只記 fingerprint_digest 前 12 碼、nonce 不記。

BE 側(T-5.2)——新 common/integrity/unlock.py(同套件約束:只 stdlib+無依賴純函式)。核銷四關(驗章→指紋→event_id→nonce)+效期關,掛進 startup_gate.run_startup_gate() 的 FS 標記分支。成功則清標記+記 nonce+刪 token 檔+留回執,繼續往下跑 manifest 比對(不 return)。DB 側 unlocked_at/unlock_noncesync_unlock_receipt_to_db()repo.mark_unlocked() 回填,掛在 scheduler 首輪同一 tick。

決策(三項,皆屬派工單留給實作棒的形式選擇):

  1. LC 入口選 SSR 後台頁,不做 internal API——要解鎖的機器正處於拒啟狀態,跑不起來也就打不了 API;行為者是坐在瀏覽器前接客戶電話的內部人員,形狀同「離線開通受理」。CLI 為 air-gapped 保底(同 issue/sign-manifest 雙軌)。沒有第三軌 API:沒有呼叫端的端點只是多開攻擊面。
  2. unlock 另立 modules/integrity/ 不塞 licensing——unlock token 不是授權照(無 Plan/訂單/展延/開通綁定),塞進去只會讓那模組變雜物間;共用的「同私鑰、同 sign_payload」本就在 core/crypto
  3. DB 回填走「回執檔」而非從 nonce 帳末筆推導——標記清除後 event_uid 就沒了,而那正是 UPDATE 的 WHERE 依據。回執寫不進去不阻斷解鎖(解鎖已成立,純紀錄失敗不該否定已成立的授權行為)。

幾個刻意取捨(避免下一棒「好心修掉」):

  • nonce 帳毀損取寬鬆側(回空清單):取嚴格側會讓一次檔案損毀就再也解不開只能重灌;寬鬆側最多讓某張已用 token 再用一次,仍受效期/指紋/事件三關拘束。與標記檔「毀損即視同鎖定」方向相反是刻意的
  • 標記毀損時一律不放行解鎖:否則「先把標記改亂碼、再拿任一張舊 token」就成了繞法。
  • nonce 寫不進去時不放行(記不住=失去防重放),但訊息明確標示為環境權限問題,避免現場誤判成 token 有問題而白跑一輪重簽。
  • 四種失敗訊息各自不同且含處置建議:含糊訊息會讓現場把「指紋抄錯」誤判成「原廠簽錯了」。

驗收(全部用 LC 真 DEV 鑰簽出的 token,非模擬信封):

  1. 正確 token → 核銷成功、標記清除、nonce 入帳、token 檔刪除、回執產生 ✅
  2. 重放同 nonce → 拒 ✅ 3. 事件編號不符 → 拒 ✅
  3. 另簽一張 fingerprint=b*64 的真 token → 指紋關擋下 ✅(不是靠改檔破壞簽章矇混——那只驗得到驗章關)
  4. 回溯 issued_at 簽一張真的過期 token → 效期關擋下 ✅
  5. 未放 token → 靜默拒啟不噪音 ✅
  6. DB 回填(真 DEV DB):FS→DB inserted=True → 回執回填 updated=True → 回執清除 → 重複回填 False(unlocked_at IS NULL 生效)→ 無回執 no-op;psql 查得兩欄正確落列,測試列已清除
  7. LC 後台頁走完整 HTTP 流程(登入→表單→簽發→結果頁→下載)簽出的檔,直接放進標記目錄被 BE 核銷成功——跨 repo 端到端

測試:LC 17 條(全站 378 綠)/BE 16 條(license+integrity 相關 120 綠)。突變驗證:BE 四關逐一停用各紅對應那一條、還原復綠(每關獨立可證);LC tamper_event_id 組錯→2 紅、預設效期 72→71→1 紅。

教訓:

  • 自證式斷言抓不到東西assert expires - issued == timedelta(hours=DEFAULT_VALID_HOURS) 拿常數比自己,把 72 改成 71 測試照樣綠。改成字面 72 才有牙齒——突變測試就是為了逼出這種。
  • 「用改壞的檔案驗某一關」是假驗證:改 payload 會同時破壞簽章,於是四關全被第一關攔下,看起來全過、實際只驗到驗章。指紋關與效期關各補簽一張真的才算數。
  • base.html 加導覽項會連帶打斷測試test_csrf / test_login_lockout 的最小 app fixture 逐一 stub 了每個 nav endpoint,新增一項就要同步補,否則 url_for BuildError。
  • load_dotenv() 是相對呼叫檔解析而非 cwd——臨時驗證腳本放 scratchpad 時要帶絕對路徑。

推翻了什麼: 無。design §4.3 的契約與派工單裁定(FS 通道、四關、nonce 落 FS、DB 補寫比照 T-3.3)全部照做,未偏離。

留給下一棒:

  • .5 只在源碼模式驗過GUIDANT_TAMPER_MARKER_DIR 指臨時目錄、手造標記檔)。真 Nuitka dist 的完整重啟循環(gate 拒啟 → 放 token → 重啟解鎖 → 檔案未還原立刻再鎖)未跑,建議併入「真產物集中驗證」一起做。
  • DB 回填走的是直接呼叫 service(真 DEV DB),非經 scheduler tick 實跑
  • LC migration 只套 DEV;STG/POC 上版時 alembic upgrade head 會一併帶 49629d9e5d99 與平行棒的 4328af2b0ece
  • 平行棒(CM-1228 T-6.3)同時段在 LC 動工,本棒的 migrations/env.py 一行 import 被掃進其 7c2974d——內容正確,僅歸屬提醒。
  • 零新增環境變數(兩側皆沿用既有),.env.sample / deployment-env.md 無須異動。

§6

第 6 棒(T-8.1+T-8.3/CM-1231+CM-1233)— 2026-08-16 晚 — .8 第二波 runner(合棒)

commits: BE 520fc5e3(CM-1231 lockdown 殼)/24834d73(CM-1233 forensic 擴充)/aeb2bd44(CM-1233 補 boot 的 app.log 尾段,review 後補)。branch feature/FR-064,顯式 git add,未 push。LC 側零改動(見下)。

做了什麼:

T-8.1 lockdown 殼——新 common/integrity/lockdown.py(stdlib http.server,無 Flask/DI/DB、零寫入面)。任何路徑任何方法回 HTTP 503;內容協商:Accept: text/html 或瀏覽器 UA → 繁中鎖定頁,其餘 → JSON envelope(code:0error_code:INTEGRITY_503001data 三欄),契約照 FE de23772 定死未動。startup_gate._refuse() 收斂全部拒啟路徑:印完整 log → 起殼 → 殼起不來才 SystemExit。runtime 抽查/埋點維持既有「寫落點+os._exit」不變。

T-8.3 forensic——新 common/integrity/forensic.pycollect_file_metadata(mtime/ctime/owner/size,個別 stat 失敗記 error 不阻斷)+collect_trigger_context 依來源分級(boot app.log 尾段/scheduler 活躍 session 快照/hook 當下帳號+IP)。三落點吃同一份 detail 物件(一處建構三處同步)。

決策(實作棒的形式選擇):

  1. 殼要放 CORS——派工單沒提,但不放行 preflight 的話跨源 FE 收到的是 network error 而非 INTEGRITY_503001,「FE 認碼跳鎖定頁」整條契約會斷。殼本來就沒有機密與寫入面,Allow-Origin: * 不放寬任何能力。
  2. 活躍 session 資料源選 public.api_logs——HTTP middleware 全自動寫入,是唯一「不必額外埋點就有帳號+IP+時間」的現成來源。不為一份旁證另建活躍 session 表。窗口 30 分鐘(抽查週期 4h,太長會把早已離線的人算進來)。排除 user_name 為空字串的未登入流量(那不是 session)。
  3. 不符檔案的「路徑清單」與「人類可讀描述」分開回傳_GateFailure.mismatched_paths_check_files 回 tuple)——從描述字串反解路徑會依賴訊息格式,訊息一改 forensic 就靜默失準。

確認不需要動的兩件事(派工單列為「若…則…」的分支,實查後都不成立):

  • DB 無須 migrationintegrity_tamper_events.detail 本來就是 JSONB(psql \d 確認)。
  • LC 端無須調整report-tamperdetailma.fields.Raw、schema unknown=EXCLUDE,實打真 LC dev 回 200 且新欄位完整落庫。

驗收(全部實跑,非模擬簽章):

  1. 用 LC DEV 真私鑰簽的 manifest(kid 2ce3bb59a6f5a2ce):改檔 → 拒啟起殼;curl / 與任意路徑得三欄 JSON、Accept: text/html 得 HTML 頁(三值渲染正確)、OPTIONS 得 204+preflight 標頭 ✅

  2. FS 標記存在(檔案已還原、只剩標記)重啟 → 同樣起殼,三欄取自標記 ✅

  3. 完整解鎖循環:以真私鑰簽 unlock token → 放進標記目錄重啟 → 核銷成功、標記清除、殼不起、gate 往下跑完 manifest 比對、正常啟動(exit 0)✅

  4. forensic 三落點:FS 標記含四欄 metadata;FS→DB 補同步後 psql 查得 detail 完整;D9 payload 先經 mock 端點原樣落檔確認擴充有送出,再以真 LC dev 打通(回 200,tamper_events.detail 查得新欄位)✅

  5. 三種來源分級各自實跑:boot(log_tail 取回 50 行,最後一行與 tail -1 log/app.log 逐字相同)/scheduler(接真 DEV DB 得具名快照 Billows Admin+IP+時間;無 DB 時落 unavailable 不 raise)/hook(真 Flask request context+真 UserContextDTO,得帳號與 XFF 首段 IP)✅

  6. socketio 模式:RUN_MODE=socketio SOCKET_PORT=18102 → 殼綁 18102、api port 空著 ✅

  7. 測試:新增 22(lockdown)+18(forensic)條,integrity+license+manifest 共 295 綠突變驗證:lockdown 六項(error_code/狀態碼/CORS 標頭/port 分流/內容協商/起不來回傳值)、forensic 六項(mtime 取值/owner 欄位/分級條件/XFF 首段/stat 容錯/上限)、log tail 四項(boot 不含 log_tail/不 seek 整檔讀/失敗回空清單/50 行上限失效)、wiring 三項(gate 不掛 forensic/runtime 不掛 metadata/gate 拒啟不起殼),逐一驗紅後還原。

  8. ddd-compliance-reviewer 掃過本棒 diff:PASS,唯一 Warning 即上述 log tail 落差,已補(aeb2bd44)。其餘(套件 stdlib-only 約束、lazy import+全域 except、無憑證誤入、INTEGRITY_503001 不走 common/code/ 是 D11 明文的合理例外)皆判合規。

測試資料已清除:DEV integrity_tamper_events、LC dev tamper_events 與臨時 api_token 皆已刪,count 回 0(已驗)。

教訓:

  1. 「測試全綠」可能是環境巧合:首次跑 gate 測試 0.57s 全綠,實情是本機 BE 佔著 port 8000 讓殼起不來、退回 SystemExit——port 空著時整份 suite 會被 serve_forever() 卡死。凡是新引入「會阻塞的東西」,要先問「為什麼它沒卡住」而不是「綠了就好」。
  2. 只攔不斷言=白攔:把 enter_lockdown monkeypatch 掉之後,wiring 突變(整行拿掉起殼)測試照樣全綠。fixture 改成記錄呼叫參數並斷言「拒啟必起殼+三欄傳對」才有牙齒。
  3. load_dotenv() 相對呼叫檔解析(第 5 棒已記過,本棒又踩):scratchpad 驗證腳本讀不到 repo 的 .env,engine 靜默用預設值連 localhost:5432,錯誤訊息指向 socket 而非設定。臨時腳本一律帶絕對路徑。
  4. 合棒要先想 commit 邊界:兩張卡的改動落在同一批檔案,第一次 commit 時把 forensic 的 hunk 一起 add 進 T-8.1。未 push,reset --soft 後拆成「T-8.1 只含殼」「T-8.3 只含 forensic」兩支,各自單獨可編譯可測。合棒時應在動手前就規劃好切分點。
  5. 派工單的簡述 ≠ 規格,要回 design 逐條對:派工單 T-8.3 寫 boot 分級「僅檔案 metadata(無 session 可查)」,照著做就漏了 design §4.4.2 表格明列的「+app.log 尾段」。被 ddd-compliance-reviewer 抓到才補(aeb2bd44)。派工單是導引,design 才是驗收基準。
  6. 突變測試也會寫出假的:驗「大檔只讀檔尾」時第一版斷言「最後 50 行對不對」——把 seek 整段拿掉照樣全綠(整檔讀進來再切最後 50 行結果一樣)。這條測的是記憶體行為,就得斷言實際讀進的 bytes 數。突變沒紅時要先懷疑斷言,不是慶幸。

推翻了什麼: 無。派工單裁定(殼只在 gate 拒啟路徑起、runtime 維持 exit)與 design §4.4 一致,未偏離。

留給下一棒:

  • 本棒全在源碼模式GUIDANT_INTEGRITY_FORCE=1+臨時 dist)驗過,真 Nuitka dist 與真容器未跑——併入「真產物集中驗證」。
  • BE↔︎FE 整鏈端到端未跑(真殼回 503 → 真瀏覽器打真 API → FE 跳鎖定頁)。T-8.1/T-8.2 兩端都已落地,條件成立。
  • 殼在真部署承載下的邊界:容器 healthcheck/LB 打到殼會拿 503(預期,但未觀察 orchestrator 反應);若正式環境有殘留 process 佔 port,殼起不來會退回 exit(不放行,但也沒有鎖定頁)。
  • 零新增環境變數(PORTSOCKET_PORTRUN_MODE 皆既有),.env.sampledeployment-env.md 無須異動。

§7

第 7 棒 — 2026-08-16 午~傍晚 — 第二任首腦(協調者,驗收+D11 拍板+部署定式)

commits: BE e1190f86(locale)/4e64ca07(殼協商修)/2d56edb6(compose)/f929abe7(build_all)/4c06ddc8(scripts 盤點)+docs 若干,皆已 push。LC/FE 無新 commit(第 2~5 棒的已入)。 Notion: CM-1231~1234 驗收完(修正待驗證);新開 CM-1236(scripts 盤點✅)/1237(build_all✅)/1238(compose✅)/1239(解鎖 SOP,未派)。

做了什麼:

  • 真產物集中驗證首通:188 真 dist smoke 七項+gate 真驗章(1304 檔)+改檔拒啟三段(改→拒/改回仍拒/清標記恢復)
  • 抓到兩個出貨阻斷 bug 並修復驗證:①ASCII locale 下 gate 印中文炸 UnicodeEncodeError(e1190f86:stdout errors=replace+image 釘 C.UTF-8)②殼內容協商 UA 嗅探讓 XHR 拿 HTML、FE 解不出 error_code 不跳鎖定頁(4e64ca07:只看 Accept)——後者是 user 真瀏覽器實測抓到的
  • D11 拍板(lockdown 殼+forensic 存證+行為人不可知邊界)→ 四棒派出並親驗全過(詳第 2~5 棒 block)
  • 部署定式化:docker-compose(含 pki volume 補洞——原 run 指令漏掛,rm 容器會洗掉鎖定標記)+build_all.sh 一鍵管線;188 已用 compose 起正式容器(read_only 生效,root 也寫不進 /app)
  • 188 實環境驗過:正式容器 gate 通過、D4 唯讀、locale C.UTF-8、pki 掛載;user 實測竄改→鎖定→FE(抓到殼 bug)→清標記復原
  • LC 部署四步走完(188 STG:unlock 簽發+tamper 接收,migration 至 head 49629d9e5d99)

決策:

  • D11(lockdown 殼 B 方案)+forensic 證據規格+契約措辭建議(綁「完整性驗證失敗」不綁「查明行為人」)——design §2/§4.4 已落
  • unlock 核銷通道=FS token 檔(放標記同目錄),不走 HTTP(被鎖時服務起不來)
  • 排程決策:T-8.1/8.3 等主鏈完成後合棒(同檔衝突);T-8.2 FE 契約定死先行——實證有效
  • 選 model 按任務認知形狀三軸(裁量/整合/錯誤隱蔽),不再一律最高階——已落 memory

教訓:

  1. 內容協商不能看 UA——XHR 的 UA 恆為瀏覽器,零鑑別力;活體驗證只 curl 過 JSON 路徑沒模擬「瀏覽器 UA+JSON Accept」組合,讓 bug 溜到 user 面前
  2. compose 的 read_only 讓「容器內改檔測竄改」不可行——測竄改一律用拋棄式可寫容器+隔離 pki 目錄(/tmp/pki-test),別打正式落點;user 誤打正式落點後標記殘留 /srv/guidant/pki,復原=清標記+compose 重起
  3. push 是 user 專屬動作(Claude 無權限,能力問題非授權問題)——已落 memory
  4. deployment run 指令漏掛 VOLUME 宣告的 pki——Dockerfile VOLUME 不等於部署有掛,匿名 volume 會讓鎖定被 docker rm 洗掉

推翻了什麼: 殼的 UA 嗅探分支(520fc5e3 引入、4e64ca07 廢除);STATE 第 5 棒 Notion 盤點表過期列(5084eeea 更正)。

在途: user 正在 188 rebuild(HEAD 4e64ca07,碼正確)——build 完換容器後,「FE 鎖定頁真瀏覽器整鏈」即可複驗。

留給下一棒(收尾棒):

  1. 殼修正後的 FE 整鏈複驗(rebuild 完成後:臨時可寫容器佔 8000 → 改檔 → FE 應跳鎖定頁)
  2. CM-1229 E2E 五條+license 迴歸+4h 抽查真承載(未實證清單見 STATE)
  3. CM-1239 解鎖 SOP 文件(卡已開未派)
  4. version-bump(含 FR-064 出貨前)+母案收尾(等 user 令收 Done)

§8

第 8 棒(T-7.1/CM-1229)— 2026-08-16 傍晚 — E2E 收口 runner

commits: 無程式碼異動(本棒是實測棒);docs 本 block+STATE 更新(帶 CM-1229)。 Notion: CM-1229 → 修正待驗證(A/B/E 成果成立,A-2 FAIL 如實在卡、A-4/C-1/24h 留複驗輪,卡不收 Done);新開 CM-1241(BUG-1 修復棒,調查與證據已轉載)。

做了什麼(真 image 1.14.0/HEAD 4e64ca07,全程拋棄式可寫容器+/tmp/pki-test 隔離落點,正式 compose 容器與 /srv/guidant/pki 未受影響):

  • 🔴 抓到 BUG-1(P0 出貨阻斷,本棒最大產出):真產物×gunicorn 下 hook 觸發 tamper 後服務沒死透——FATAL 印了、FS/DB 落點都寫了、fingerprint/event_id 正確,但 master 存活並 respawn,服務照常回 200;同一竄改重現 3 次(3 event/3 FATAL)服務都活著,容器全程 healthy、RestartCount=0。根因定位 runtime_check._terminate_process():容器內 PID1=gunicorn master、worker 與 master 同 PGID=1(killpg 確實送達),但 master 本就 catch SIGTERM(/proc/1/status SigCgt=0x108314e07,bit15 已設)走自己的 shutdown handler,實測反而 respawn。交 CM-1241 修,本棒不修(驗的人不修)。
  • A-1 改檔拒啟 PASS(改 manifest 內 .mo 一 byte → 拒啟+列不符檔+fingerprint/event_id;.integrity-tamper 5928B 含 T-8.3 forensic 三段:file_metadata/trigger_context.source=boot/log_tail)
  • A-1b 殼協商修正(4e64ca07)BE 側複驗 PASS——瀏覽器 UA+Accept: application/json503 application/jsonINTEGRITY_503001+三欄 data(正是修正前錯回 HTML 的組合);Accept: text/html 回鎖定頁
  • A-3 重啟被拒 PASS(restart 直接認標記不再逐檔比對;docker rm -f 重建容器→仍拒啟、同一 event_uid,FS 落點跨容器生命週期生效)
  • A-5 縮短版 PASS(乾淨容器 gate 1304 檔通過+多輪 API 操作零誤報;24h 保留未實證)
  • B License 迴歸 PASS:三環境三把 kid(DEV 2ce3bb59/STG 04b1e65f/POC f3b562a6)真實已簽發照全部驗章通過(皆止於商務層 400006/409002),兩條負控(簽章改 1 byte/payload zlib 解壓改內容再壓回)皆 LICENSE_400002 → 證明不是「什麼都放行」。四張真照 payload 皆payload_type,走「缺鍵視為 license」路徑 → D2 抽層對已出貨 license 零破壞
  • E orchestrator 觀察完成:鎖定殼被判 unhealthy(failing=4)但 Docker restart policy 只看 process 退出不看 health → 不進重啟迴圈,殼穩定停在鎖定態回 503(符合預期)

決策:

  • BUG-1 開新卡 CM-1241 交修復 runner,驗的人不修、修的人另驗(首腦裁定)
  • C-1(源碼模式縮短間隔×兩承載)併入 CM-1241 開工第一步——它回答「scheduler 路徑是否同病」,是決定修法範圍的關鍵輸入,不在本棒跑
  • A-4 unlock 真循環延到 BUG-1 修完的複驗輪(unlock 的意義是「解除鎖定」,而 BUG-1 動搖的正是「鎖定是否成立」這個前提)
  • B 的 license 素材從三環境 DB 唯讀 SELECT 取真照打 DEV 容器——比自簽假照更有迴歸價值,且不違反環境紀律(只讀)

教訓:

  1. docstring 的終止假設沒被任何測試驗過——_terminate_process() 檔頭洋洋灑灑寫明「killpg 讓 master 與所有 worker 同組收到終止……只 os._exit 的話 worker 死了 master 會再 respawn」,把失敗模式都預想到了,卻沒有任何測試在真承載上驗證這個假設;既有測試 monkeypatch 掉 _terminate_process 只驗落點副作用,等於把最關鍵的一環整個跳過。寫得越詳盡的設計註解越容易讓人以為「已經想清楚=已經成立」。
  2. Nuitka 產物的埋點選檔會退化成只驗 binary——_HOTPATH_PRIORITY_PREFIXEScommon/license/common/integrity/common/authz/)在真產物中不存在為獨立檔(全編進 binary),manifest grep 三個前綴皆零命中。埋點實際只驗 binary 一檔,與源碼模式行為差很大,設計時的「個位數檔案」預期在真產物是「1 檔」。
  3. 運行中的 binary 用 printf >> 改不動(Text file busy),但 unlink+rename 可以(Linux 語意)——測真產物竄改要用「複製→改→mv 換掉」,否則會誤以為 binary 天然免疫。
  4. 拋棄式容器沿用 guidant.env 會指向 STG DB(該庫無 integrity_tamper_events 表,符合環境紀律)——要驗 DB 落點必須 -e DB_NAME=guidant_ai_dev 覆寫;啟動 gate 本就不寫 DB,所以 A-1 不受影響,但 C-2 需要。
  5. K8s 下 E 的結論會反過來:liveness probe 失敗會 kill+重啟 pod,鎖定態變 CrashLoop。compose 落地版目前不受影響,FR-065 若上 K8s 需把鎖定態排除在 liveness 之外。

推翻了什麼: 「運行中偵測→服務立即終止」在真產物 gunicorn 下不成立(design §4.6/D8 的承諾、_terminate_process docstring 的三種承載死法宣稱,至少 api+gunicorn 這條是錯的);STATE 原「抽查真承載未驗」一項的隱含前提(以為只差時間窗,實為機制本身有洞)。

留給下一棒:

  1. CM-1241 修 BUG-1(開工第一步跑 C-1 定範圍;修完需複驗 C-2 死透/C-1 兩承載/A-4 unlock 真循環)
  2. CM-1239 解鎖 SOP 文件(卡已開未派)
  3. version-bump+母案 CM-1188 收尾(BUG-1 未修不得出貨

§9

第 9 棒(CM-1241)— 2026-08-16 — BUG-1 P0 修復 runner

commits: 7cc7989e fix(CM-1241) tamper 終止改依承載送訊號(common/integrity/runtime_check.pymain.pytest/test_integrity_runtime_check.py)。未 push(等 user)。 Notion: CM-1241 → 修正待驗證(含根因更正、修法、各承載證據、未實測假設)。

做了什麼:

  • 先跑 C-1 定範圍(首腦指定工序),但改用容器化 harness 而非 188 源碼環境:真 gunicorn 26 pre-fork、PID1=master、呼叫真的 _terminate_process,比改 SPOT_CHECK_INTERVAL_SECONDS 等 60s 更快且不佔 188。C-1 結論:scheduler 路徑不同病——master 執行緒自己呼叫時 os._exit(1) 直接讓 PID 1 退出,容器 exit=1 死透(修前修後皆然)。
  • 🔴 推翻卡上的根因假設(本棒最大產出):卡與 docstring 都寫「killpg 送達了 master,但 master catch SIGTERM 走 graceful handler 沒死」。實測:master 根本沒收到訊號。 容器內 pgid=1,而 killpg(1,sig)kill(-1,sig) 是 POSIX 廣播特例,Linux 明文排除 PID 1。真 image 1.14.0 內 perl 探針(image 無 python):killpg(-1,TERM) 送達對象數=0kill(1,QUIT) 送達=1 handler 有跑/kill(1,KILL) 回傳 1 但 PID1 仍存活(核心忽略無 handler 的訊號)。
  • 連帶推翻卡上的候選修法:「SIGTERM 改 SIGKILL(不可 catch 必死)」對 PID 1 無效,改下去會從「不死」變成「還是不死且更難查」。
  • 修法:_terminate_process依承載分流——gunicorn worker 指名對 master 送 SIGQUIT(有 handler 故 PID1 收得到;quick shutdown 直接 halt 不回主迴圈故不 respawn);容器 PID 1 與非 leader 不送訊號只 os._exit(1);bare metal 僅在 pgid == pid(自己是 group leader)才 killpg,保住 shell 不被波及。master pid 由 main.py post_fork 呼叫 register_gunicorn_master(server.pid) 登記而非猜。送訊號決策抽 _termination_signals() 純函式以便測試。

驗證(真 image 根因實證 ✅/harness 行為實證 ✅/真 image 端到端 ⏳ 待 push): | 項目 | 結果 | |---|---| | 真 image 1.14.0 三種送法可達性 | killpg=0 對象/kill(1,QUIT)=1 且 handler 跑/kill(1,KILL) PID1 存活 | | harness hook 路徑(修前) | 復現:worker 死、master respawn、ping 200 | | harness hook 路徑(修後) | master Handling signal: quitShutting down: Master → 容器 exited、ping 000 | | C-1 scheduler(修前/後) | 皆 exit=1 死透(非同病) | | C-1 Flask dev server 單 process | 死透 exit=1 | | bare metal | 子進程被 killpg 收掉、呼叫端 shell 存活(/proc 掃描確認) | | 負控(拿掉 post_fork 登記) | 舊病完整復現 ping 200 → 證明修正承重 | | 單元測試 | +6 支;三次突變(SIGQUIT→SIGTERM/移除 PID1 守門/移除 reparent 守門)各自翻紅、還原全綠 | | 迴歸 | integrity 全套 133 passed;乾淨容器多輪請求零誤殺 |

決策:

  • C-1 改用容器 harness 而非 188 源碼環境+縮短間隔:同樣回答「scheduler 是否同病」,但更貼近真相(真 PID1/真 pre-fork,源碼模式在 shell 下 pgid≠1 反而測不到本 bug 的關鍵條件),且不佔 188、不需臨時改寫死參數。
  • 選 SIGQUIT 不選 SIGTERM:後者走 graceful 要多等 graceful_timeout;不選 SIGKILL:對 PID1 無效(實測)。
  • master pid 用 post_fork 登記不用 getppid() 猜:猜錯時 bare metal 會把使用者 shell 當 master 殺掉。

教訓:

  1. 「訊號送出去」≠「送到了」——os.killpg 不 raise 不代表有對象收到(本例回 ESRCH 或 0 對象)。診斷訊號問題要驗接收端有沒有收到,不能只看發送端 syscall 成功。上一棒讀 /proc/1/status 看到 SigCgt bit15 已設,正確地證明了「master 有 handler」,卻被誤讀成「master 收到了訊號並用 handler 處理」——有能力接收 ≠ 實際收到
  2. PID 1 在容器內是特殊公民kill(-1) 廣播排除它、沒註冊 handler 的訊號(含 SIGKILL)核心直接忽略。任何「送訊號讓服務死」的設計,在容器內必須針對 PID 1 單獨驗證。
  3. 承接上一棒教訓 1 的下一層:docstring 假設沒測會錯,而卡片上「已定位的根因」同樣是假說——本棒若照卡直接改 SIGKILL 就會交出一個看似合理、實則無效的修正。派工卡的根因段要當待驗假說讀([[feedback_upstream_hint_is_hypothesis_not_conclusion]])。
  4. 真 image 無 python,但有 perl——要在出貨產物內做系統層探針時 perl 是可用的後路。

推翻了什麼: CM-1229/CM-1241 卡上的根因(「master 收到 SIGTERM 但走 graceful handler 沒死」)與候選修法(SIGKILL);_terminate_process 原 docstring 三種承載死法的整段敘述(已改寫並保留錯誤假設的說明,避免後人重蹈)。C-1 待答項銷案:scheduler 路徑不同病

留給下一棒:

  1. 真 image 端到端複驗(唯一未完項):需 user push 7cc7989e → 188 pull → rebuild image → 重現 CM-1229 C-2 步驟(unlink+rename 換 binary → 打 /api/1.0/license/upload)→ 應服務死透(master+worker 全滅、容器 exit、不 respawn)→ restart 被 gate 攔、殼回 503
  2. A-4 unlock 真循環(BUG-1 修完前提已恢復,可接著跑)
  3. CM-1239 解鎖 SOP 文件;version-bump+母案 CM-1188 收尾

§10

第 10 棒(CM-1242)— 2026-08-16 — BUG-2 P0 修復+A-4/socketio 複驗 runner

commits: 19a086af fix(CM-1242) 掛載宿主 /etc/machine-iddocker/production/docker-compose.ymldeployment-env.md)。未 push(等 user)。 Notion: CM-1242 → 修正待驗證(含修法/綁機回歸評估/V1–V5 證據/未實測假設 6 條)。

做了什麼:

  • 修法選項 A 落地,code 零改動:compose x-guidant-common volumes 加 - /etc/machine-id:/etc/machine-id:ro(anchor 共用,api+socketio 兩 service 同時生效)。先驗掛載是否足夠再決定要不要動 code——足夠,故 machine_fingerprint.py 一行未改(三級 fallback 邏輯本身沒錯,錯的是部署沒把宿主 machine-id 帶進容器)。
  • deployment-env.md 同步:三處 docker run fallback 各補一行、§4 掛載表六→七列、新增「宿主必須有 machine-id」前置檢查段(含 test -s 檢查與「machine-id 重生成會讓既有綁機憑證全失效」警語)。
  • 既有 license 綁機回歸評估(唯讀 SELECT,未改資料):DEV 25 列中 16 列綁舊指紋(4a837148…×15=舊測試容器 MAC、f2ef9354…×1=開發者本機 MAC 推導值,已用 sha256("mac:"+getnode()) 比對確認),但全部 is_current=false;三張現行照皆未綁機。STG 3 列/POC 1 列皆 null。結論:無回歸,不需資料遷移(真要處理也該是向 LC 重簽,改 DB 會讓照內簽章與 DB 值不一致)。

驗證(188 拋棄式容器;🔴 正式容器與 /srv/guidant/pki 全程未碰): | 項目 | 結果 | |---|---| | V1 跨 docker restart | MAC e2:5c:9a…→換,指紋 ff793254…a3da080 不變 | | V2 跨 docker rm+重 run | MAC a2:83:dc:f7:ba:78(全新),指紋 仍相同 | | 錨點驗證 | sha256(宿主 machine-id d478e208…)ff793254… ✅ | | 負控(不掛載) | restart 前後 67c6dd9d…63572f6d…bug 完整復現(證明修正承重) | | V3-① 觸發 | unlink+rename 換 binary→打 /license/upload服務死透Handling signal: quitShutting down: Master、容器 Exited、ping 000)← 順帶端到端證實 CM-1241 BUG-1 修正在真 image 1.14.0 上成立(第 9 棒留的唯一未完項銷案) | | V3-② 鎖定態 | restart→gate 攔、lockdown 殼 503、訊息含 fingerprint+event_id | | V3-③ 負面案 4 條 | 他機/過期/錯事件編號/重放 全擋,四種訊息各異 | | V3-④ 正解鎖 | 核銷成功、標記清、nonce 入帳、token 用完即刪、ping 200;receipt→scheduler→DB 回填鏈完整(DB 實查 unlocked_at 非空、unlock_nonce=fe6e3cbc40d1…) | | V4 迴歸 | 乾淨容器 healthy 無誤鎖;integrity+license 指紋測試 131 passed | | V5 socketio 死透 | PID 1/eventlet/APScheduler 條件下 _termination_signals()[]、走 os._exit(1)、容器 Exited(1)、ping 000 |

決策:

  • 能不改 code 就不改:先實測「掛載是否足夠」再決定,結果足夠。改 machine_fingerprint.py 的空檔 fallback 屬另一議題(建議做成「打包模式落到第三級時警示」而非拒啟——拒啟會讓沒有 machine-id 的極簡宿主完全裝不起來),不在本卡範圍。
  • 過期 token 用 LC 私鑰直簽而非 CLI:CLI --valid-hours 有 1–336 下限(本身是正確的防呆),改用 sign_payload 直接造「真簽章但 expires_at 在過去」的 token——這樣驗到的是核銷的效期關,不是「簽發端擋住了」,兩者是不同的東西。
  • V5 用 harness 而非真 image:socketio 模式不載 REST blueprint(main.py 設計),8002 上沒有埋點端點;抽查 4h 週期且參數寫死(D8 刻意不開放 env)。故重現承載條件(容器 PID 1+monkey_patch(all=False, socket=True)socketio.runBackgroundScheduler)但執行 mount 進來的真源碼PYTHONPATH=/src/opt/guidant_ai),非複製品。承接第 9 棒同一手法。

教訓:

  1. 「容器內有這個檔」不等於「這個檔有內容」——/etc/machine-id 在 Debian base image 是 0 bytes 佔位檔os.path.exists() 為真、open().read().strip() 為空。三級 fallback 的 if value: 寫得完全正確,卻因為第一級「存在但空」而靜默降級到最不穩的第三級。檢查「來源可用性」時,存在性與有效性要分開驗。
  2. 設計註解寫對了假設,不代表部署實現了它——machine_fingerprint.py docstring 白紙黑字寫「正式 Host 版部署預期跑在有 /etc/machine-id 的 Linux 容器宿主上」,這句話是對的(宿主確實有),但沒有人把宿主的檔帶進容器。這與第 8 棒教訓 1(docstring 假設沒被測過)同源:假設寫得越清楚越容易讓人以為它已成立。
  3. 掛「檔」的陷阱-v /etc/machine-id:/etc/machine-id:ro 在宿主缺檔時 docker 會建成目錄,容器讀檔失敗→靜默退回 MAC→症狀與完全沒掛一模一樣。凡是掛單一檔案的 bind mount,文件都要附「宿主須先存在」的前置檢查。
  4. 綁機類欄位查回歸要看 is_current——tenant_licenses 是 replace 制,歷史列會累積一堆舊指紋,只看「有幾列綁了指紋」會嚇死自己(16 列!),實際受影響的只有現行列(0 列)。
  5. 驗負面案要驗到「該關」而不是「任何一關」——過期案若用 CLI 的 --valid-hours -1,擋下來的是簽發端參數防呆,核銷的效期關根本沒被執行到。負面案要確保失敗發生在你想驗的那一關。

推翻了什麼: 無推翻(卡上根因與修法選項 A 皆經實測成立,是少見的「卡寫對了」)。銷案兩項:第 9 棒留的「真 image 端到端複驗」(V3-① 順帶完成)、STATE 未實證清單第 1 項的 ③A-4 unlock 真循環+socketio 承載。

留給下一棒:

  1. 正式/STG/POC 套用 machine-id 掛載(需 docker compose up -d recreate 兩容器)——等決策者放行,本棒只改 repo 檔
  2. STATE 未實證清單餘項:scheduler×真 binary×4h 真窗、≥24h 長跑(A-5 完整版)、K8s liveness 下殼會 CrashLoop(FR-065 議題)
  3. socketio 真 image 完整竄改→死透鏈(低風險,比對邏輯與 api 共用同一份 runtime_check,但未實跑)
  4. CM-1239/CM-1240 等 user 收 Done;version-bump+母案 CM-1188 收尾(BUG-1/BUG-2 皆已修,出貨阻斷解除
§11

第 11 棒 — 2026-08-16 傍晚~夜 — 第三任首腦(收尾棒)

commits: BE 29fead0(FE CM-1240 details 開關,FE repo)/19a086af(BUG-2 fix)/7cc7989e(BUG-1 fix)+docs 若干,皆已 push 三 repo 與 origin 同步。

做了什麼:

  • 接手驗收:FE 鎖定頁真瀏覽器截圖複驗(user 親見);CM-1240 開卡→FE 指紋/時間預設隱藏(?details=1 才顯示,離線 unlock 鏈不斷)親驗收 Done
  • 派 CM-1229 實機驗收棒 → A/B/E+A-1/A-3/A-5 縮短版全過、B license 三環境三 kid 迴歸過,抓到 BUG-1 P0(hook 觸發不死透)
  • CM-1229 卡名消歧(「E2E」→「實機驗收測試(人工實測非自動化碼)」)+補白話段(user 反映看不懂術語)
  • 派 CM-1241 修 BUG-1 → runner 更正根因(非「master 收訊號不理」而是 killpg 打不到 PID 1)→ 首腦親驗實碼+自跑 integrity 157 passed
  • CM-1241 複驗輪本體親跑:真 image C-2 死透(log 見 master 收 SIGQUIT→Shutting down 不 respawn)+接力鏈(restart→gate 攔→殼 503);跑 A-4 unlock 時抓到 BUG-2 P0(指紋 restart 就變)
  • 開 CM-1242(卡內含完整執行指令,派工只給卡號)→ runner 修(compose 掛宿主 machine-id,code 不動)→ 首腦親驗:親算 sha256(宿主 machine-id) 對上容器指紋、親查 config.tenant_licenses 綁機無回歸、188 host 模式容器 restart 前後指紋不變
  • user 重啟 188 → 首腦驗正式部署:HEAD 66f6dea8、image 含兩 P0、掛 machine-id、gate 1304 檔、指紋穩定
  • 收尾:34 卡全 Done、SUMMARY、兩條 P0 memory、STATE/LOG 收尾標頭、母案回寫

決策:

  • 派工模式改「工作內容全寫進 Notion 卡、派工只給卡號」(user 明示不要重複貼長 prompt)
  • 卡名寫「做什麼」不寫代號、術語表前放白話段(user 反映看不懂)
  • 不進版(現標 1.14.0)——FR-064 後面還有 FR-065,里程碑確定才進版
  • CM-1242 把 A-4 unlock+socketio 死透併入(指紋修好才跑得完 unlock)

教訓:

  1. killpg 打不到容器 PID 1(POSIX 廣播特例排除 init)——已落 [[feedback_killpg_cannot_signal_container_pid1]]
  2. 容器 /etc/machine-id 空檔→指紋退回隨機 MAC 每次重啟就變——已落 [[feedback_container_machine_id_empty_fallback_mac_drifts]]
  3. 「實機驗收/end-to-end」與「自動化 E2E 測試碼」是兩回事,卡名用縮寫會誤導 PM——收尾把 CM-1229 改名並補白話
  4. 首腦親跑複驗(非全外包)在本棒兩度直接抓到 P0——C-2/A-4 是首腦本體跑的,BUG-2 就是這樣現形;驗收「親驗不信自報」延伸到「關鍵鏈自己走一遍」

推翻了什麼: BUG-1 原卡假設「master 收 SIGTERM 走 graceful 沒死」(實為根本沒收到);_terminate_process 原 docstring 的 killpg 宣稱(已改寫);「docker restart 不換 fingerprint」的 SOP 前提(僅宿主有穩定 machine-id 時成立,容器需掛載)。

留給下一棒: FR-064 已收口。續戰線=FR-065 installer(FE image/compose);出貨前待辦見 SUMMARY §10(進版/推 Harbor/正式 PUBLIC_KEYS 鑰/K8s liveness 排除鎖定態)。