# FR-064 交接現況（STATE）

> ✅ **ARC CLOSED — 2026-08-16 收尾**：母案 CM-1188＋34 卡全 Done；兩 P0（BUG-1 死透／BUG-2 指紋）真 image 實證修復；188 已用 compose 起含兩 P0 的正式容器。收尾彙整見同資料夾 `2026-08-16-FR-064-SUMMARY.md`。下方為收尾當下現況快照，保留供回顧。

> **這份是 living 檔**：每棒就地 Edit，只描述「此刻」。歷史脈絡看同資料夾的 `FR-064-LOG.md`。

| 項目 | 值 |
|------|-----|
| 最後更新 | 2026-08-16 晚（第 10 棒 CM-1242：**BUG-2 修復＋A-4 unlock 真循環＋socketio 死透全綠**，兩個 P0 出貨阻斷皆已解除，待決策者放行部署） |
| branch | BE `feature/FR-064` / LC `feature/FR-064`（`~/Projects/Billows/Audit-Manager/license_center`） |
| 母案卡 | CM-1188 https://app.notion.com/p/FR-064-3bc346da4cd081b0afecdac6f11df640 |
| Notion 回寫規範 | `docs/claude/notion-issue-tracker.md`／CLI `scripts/notion_case.py` |
| 接手前必讀 | 本檔全文 + `design.md` §2 決策表 + LOG 最後 block |

## 🧭 Arc 定位（WHY）

三階段落地版戰線第二棒（FR-063 編譯 ✅ → **FR-064 防竄改【本案，母案 CM-1188】** → FR-065 Installer）。

- **需求**：客戶機上偵測產物被竄改 → 終結服務＋鎖定，唯一解鎖＝原廠一次性 unlock token。
- **定位（已對齊）**：client-side 驗證擋不住決心逆向者，目標「99% 不值得＋1% 留蓄意痕跡」，B2B 法律層是真防線；離線必須成立。
- **機制**：build 期逐檔 SHA-256 分層 manifest → LC Ed25519 簽章（復用 FR-062 體系，泛用 sign_payload/verify_payload，判別鍵 payload_type、缺鍵視為 license 保向後相容）→ 啟動 gate 驗章＋比對 core/resources → runtime 4h 抽查＋業務埋點 → tamper 雙落點（DB 全域單例表＋FS 標記）→ 拒啟鎖定。

## 角色定位

首腦＝分析決策／協調；實作全走 runner（開 case 給新 session 為主，機械小修才本體 subagent）；驗收一律親驗不信自報。

## 決策定案

D1–D10 全拍板 2026-08-15，D11 拍板 2026-08-16，完整表在 `design.md` §2（索引：分層驗證／簽驗共用層／main.py 錨點／唯讀化／雙落點／離線 unlock／邊界承認／4h+jitter 抽查／D9 回報 LC／D10 二次確認／**D11 lockdown 殼＋forensic 存證＋邊界明文**）。

## §1 當前狀態（2026-08-16 傍晚盤點）

### git

| repo | branch | 與 origin | commits（FR-064 範圍） |
|------|--------|----------|------------------------|
| BE | `feature/FR-064` | **ahead 3（未 push：`7cc7989e`／`11c9502e`／`19a086af`）** | 頂部見下表 |
| LC | `feature/FR-064` | 同步 | 頂部 5 支屬 FR-064：`651db9c`（T-5.1 unlock token 簽發）、`7c2974d`（T-6.3 tamper 事件接收 D9）、`076c06b`（CLAUDE.md migration 鐵則＋部署四步）、`57c6566`（T-2.1 manifest 簽章）、`6ce339e`（T-1.1 sign/verify_payload 層） |
| FE | `feature/FR-064` | — | 最新 `de23772` T-8.2 鎖定頁＋interceptor |

BE 最新（新→舊）：`19a086af` CM-1242 掛載宿主 `/etc/machine-id` 修指紋漂移（BUG-2，未 push）／`11c9502e`＋`7cc7989e` CM-1241 BUG-1 終止改依承載送訊號（未 push）／`0aaa0269` STATE 用語消歧／`4e64ca07` CM-1231 殼內容協商改只看 Accept（UA 嗅探讓 XHR 拿到 HTML 的修正）／`adf77882` CM-1236 scripts/README 修正／`2d56edb6` CM-1238 落地版 docker-compose 定式（api+socketio 雙容器＋pki volume）／`4c06ddc8` CM-1236 scripts/ 盤點整理／`f929abe7` CM-1237 build_all.sh 一鍵管線／`be3868f4` STATE/LOG docs／`aeb2bd44`＋`24834d73` T-8.3 forensic。更早鏈（T-8.1 殼 `520fc5e3`、T-5.2 `f8133e72`、gate/manifest/唯讀化等）見 LOG。

### Notion 卡（2026-08-16 傍晚盤點實況）

| 卡 | 狀態 |
|----|------|
| CM-1188（母案） | Not started（母案收尾時等令收 Done） |
| CM-1215~1223/1226/1227/1230/1224/1225/1228/1232/1234 | 修正待驗證（1224/1225/1228/1232/1234 已過首腦親驗 2026-08-16 晚） |
| CM-1231/1233 | 修正待驗證（殼＋forensic；殼另補內容協商修正 `4e64ca07`） |
| CM-1236/1237/1238 | 修正待驗證（scripts 盤點／build_all.sh 管線／compose 定式） |
| CM-1229 | **修正待驗證**（第 8 棒實機驗收測試＝人工實測非自動化測試碼；A-2 FAIL 如實在卡，卡不收 Done） |
| CM-1239 | 修正待驗證（解鎖 SOP 雙份 `91a3bcd8`：原廠側 unlock-sop.md＋客戶側手冊，首腦已親驗，等 user 收 Done） |
| CM-1241 | 修正待驗證（BUG-1 修復，第 9 棒；真 image 端到端已由第 10 棒 V3-① 順帶證實） |
| CM-1242 | 修正待驗證（BUG-2 修復＋A-4／socketio 複驗，第 10 棒；**兩個 P0 皆解除**） |

### DEV DB

`integrity_tamper_events` 表存在確認 ✅（`psql -h 192.168.50.188 -p 25432 -d guidant_ai_dev`，`to_regclass` 回表名）。

### 188 實況（2026-08-16 傍晚唯讀查證）

- `/opt/guidant_ai`：branch `feature/FR-064`、HEAD `4e64ca07`（與本地同步）
- 正式容器：`guidant-api`＋`guidant-socketio` 皆 `guidant-ai-be:1.14.0`、Up (healthy)；`guidant-api` log 有 `[INTEGRITY] 產物完整性驗證通過（core+resources 共 1304 檔，version=1.14.0）` ✅ gate 真容器實跑通過
- `/srv/guidant/pki/`：空（tamper 標記已清）✅
- image：本機 `guidant-ai-be:1.14.0`＋`:latest` 皆 2026-08-16 15:38 新打；Harbor tag 仍是 08-15 舊版（新 image 未推）
- `/opt/license_center`：HEAD `651db9c`（與本地同步）；`systemctl is-active license-center` → active
- FE 部署 bundle：`/var/www/html/audit-manager/assets/index-B6KcAQdr.js` 含 `INTEGRITY_503001`（×2）✅ 鎖定碼已在線上 bundle
- 附註：/opt/license_center 對 jedi 帳號有 git dubious ownership 警告，查詢需帶 `-c safe.directory=/opt/license_center`

## 子需求進度表

| 子需求 | 卡 | 狀態 |
|--------|----|------|
| .1 簽驗共用層 | CM-1215/1216 | ✅ 驗收過（LC 6ce339e＋BE 97bca82e） |
| .2 build manifest | CM-1217/1218 | ✅ 驗收過＋三次返工（參數改名 6f37019e、.env fallback 4fe7638a，皆已驗） |
| .3 gate＋落點 | CM-1219/1220/1221 | ✅ 驗收過（eba7f5a0/d4ef0285/607b4b3a） |
| .4 抽查＋埋點 | CM-1222/1223 | ✅ 驗收過（b4a102b7） |
| .6 唯讀化 | CM-1226 ✅/CM-1227 ✅/CM-1228 修正待驗證 | T-6.1/6.2 過（7079ae24/3f338ffc）；T-6.3（D9 回報 LC）已做（LC 7c2974d／BE d181fdc6），三情境實測皆過，待 user 複測 |
| .5 unlock 全鏈 | CM-1224/1225 | **兩張皆已做（本棒），修正待驗證**——LC 651db9c（後台頁＋CLI＋migration 49629d9e5d99 已套 DEV）／BE f8133e72（核銷四關＋回執回填 DB）；六情境活體實測皆過（含以真簽章分別驗指紋關與效期關），待 user 複測 |
| .7 實機驗收 | CM-1229＋修復棒 CM-1241/1242 | **修正待驗證**——第 8 棒：A-1／A-1b／A-3／A-5 縮短版＋B license 迴歸＋E orchestrator PASS，A-2 FAIL 抓出 BUG-1；第 9 棒修 BUG-1（C-1 銷案）；**第 10 棒：A-2／C-2 死透、A-4 unlock 真循環（含 4 條負面案）、socketio 死透、BUG-2 指紋穩定 全數 PASS**。餘 24h 長跑（A-5 完整版）與 4h 真窗 |
| .8 D11 lockdown 殼＋forensic | CM-1231/1232/1233/1234 | **四張全數已做，皆修正待驗證**——T-8.4 文件／T-8.2 FE（de23772，FE repo）／**T-8.1 lockdown 殼（520fc5e3）＋T-8.3 forensic 擴充（24834d73＋aeb2bd44），本棒合做**。BE↔FE 整鏈端到端（真殼回 503 → 真瀏覽器 → 跳鎖定頁）尚未跑 |
| 場外 | CM-1230 | .env 稽核回補✅（bfbe6cc9/4b2234ac/f681a583/dc5c1090/9508c7f8） |

## 簽章鏈實證（2026-08-16 上午，本任親驗）

188 真 dist 產 manifest（core 1291/resources 13/thirdparty 5492）→ LC 簽章（kid 04b1e65f＝STG 鑰）→ BE verify_payload 驗章 PASS → 三層各抽一檔 sha256 與實檔全中。**首次端到端全通**。

## 🔴 尚未實證清單（接手必知，勿當已完成；第 8 棒更新）

**第 7 棒已銷**：~~gate 真 dist~~／~~正式 image 唯讀化~~／~~真容器 gate~~／~~殼真容器~~。

**第 8 棒已銷**：~~殼修正（4e64ca07）後整鏈~~（真 image BE 側四組協商全對：瀏覽器 UA＋JSON Accept 回 `INTEGRITY_503001` JSON；FE 側 user 已於第 7 棒實測）／~~真實已出貨 license 匯入迴歸~~（三環境三把 kid 真照全過＋兩條負控＋D2 缺鍵向後相容確認）／~~T-7.1 五條中的 A-1／A-3／A-5 縮短版~~／~~殼在 orchestrator 下的行為~~（unhealthy 但不進重啟迴圈）。

**第 9 棒已銷**：~~C-1 scheduler 觸發×兩承載~~（scheduler 路徑不同病，修前修後皆 exit=1 死透）。

**第 10 棒已銷（CM-1242）**：~~C-2 真產物×gunicorn hook 觸發後服務死透~~（真 image 1.14.0 實測 `Handling signal: quit`→`Shutting down: Master`→容器 Exited、ping 000，BUG-1 修正端到端成立）／~~A-2 運行中改檔觸發~~（同上，hook 正常判定並終止）／~~A-4 unlock 真容器完整循環~~（鎖定→LC 簽 token→放檔→restart 解鎖成功＋標記清＋nonce 入帳＋DB `unlocked_at` 回填，**四條**負面案他機／過期／錯事件編號／重放全擋）／~~socketio 承載死透~~（PID 1＋eventlet＋APScheduler 條件下 `_termination_signals()` 回 `[]`、`os._exit(1)`、容器 Exited(1)）／~~BUG-2 指紋漂移~~（掛載後跨 restart／換容器皆不變，附不掛載的負控復現）。

仍未實證：
1. **正式／STG／POC 尚未套用 machine-id 掛載**——第 10 棒只改 repo 內 compose 檔與拋棄式容器；正式部署需 `docker compose up -d` recreate 兩容器才生效，**等決策者放行**
2. **socketio 真 image 的完整竄改→死透鏈**——第 10 棒 V5 驗的是承載條件下真的 `_terminate_process()`；「真 binary 被改→真抽查 job 比對出不符→終止」在 socketio 模式未實跑（8002 不載 REST 故無埋點端點、抽查 4h 且參數寫死不可調）。比對邏輯與 api 共用同一份 `runtime_check`，風險低但非已驗
3. **scheduler×真 binary×4h 真窗**——縮短間隔驗過不等於真窗驗過，由時間自然銷或後續長跑驗
4. 「≥24h 連續運行跨多輪抽查零誤報」（A-5 完整版）
5. **宿主無 `/etc/machine-id` 時 docker 建成目錄的退化行為**——屬 docker 已知語意（掛載來源不存在時依目標推斷建立），deployment-env.md 已寫前置檢查，但未在缺檔宿主上實測；同理「machine-id 重生成會讓既有綁機憑證失效」亦為推論（不宜在 188 重生成驗證）
6. **K8s liveness 下殼會 CrashLoop**——第 8 棒觀察推得：Docker restart policy 只看 process 退出故安全，但 K8s liveness probe 失敗會 kill+重啟 pod → 鎖定態變 CrashLoop。**FR-065 installer 若涵蓋 K8s 部署，需把鎖定態排除在 liveness 之外**（目前 compose 落地版不受影響）

## ✅ 兩個 P0 出貨阻斷皆已修（等 user 複驗）

### BUG-1（CM-1241，第 9 棒修，第 10 棒端到端證實）
**症狀**：真產物×gunicorn 下 hook 觸發 tamper 後服務沒死透——落點都寫了、FATAL 也印了，但 master 存活並 respawn，服務照常回 200。
**真因（第 9 棒推翻原假設）**：不是「master 收到 SIGTERM 走 graceful 沒死」，而是**master 根本沒收到**——容器內 pgid=1，`killpg(1,sig)`＝`kill(-1,sig)` 是 POSIX 廣播特例、Linux 明文排除 PID 1；且 `kill(1,SIGKILL)` 對無 handler 的 PID 1 也被核心忽略。
**修法**：`_terminate_process()` 改**依承載分流**——gunicorn worker 指名對 master 送 SIGQUIT（有 handler 收得到、quick shutdown 不 respawn），單 process 承載／容器 PID 1 只 `os._exit(1)`，bare metal 僅 `pgid==pid` 時 killpg。master pid 由 `main.py` post_fork 登記。
**第 10 棒真 image 端到端證實**：`Handling signal: quit` → `Shutting down: Master` → 容器 Exited、ping 000 ✅

### BUG-2（CM-1242，第 10 棒修）
**症狀**：容器機器指紋每次重啟就變 → 解鎖憑證重啟即失效（永遠解不了鎖）、license 綁機漂移。
**根因**：Debian base image 的 `/etc/machine-id` 是 **0 bytes 空檔**（容器不跑 systemd 永遠填不上）＋`/var/lib/dbus/machine-id` 不存在 → 指紋退到第三級 `uuid.getnode()`（容器 MAC，每次 restart 就換）。
**修法（選項 A，code 零改動）**：compose 掛 `- /etc/machine-id:/etc/machine-id:ro`，指紋穩定錨定宿主機。實測跨 restart／換容器皆不變，附「不掛載」負控復現 bug。
**⚠️ 掛的是檔不是目錄**——宿主缺檔時 docker 會建成目錄、指紋靜默退回 MAC，症狀與沒掛一樣；deployment-env.md 已補前置檢查。
**綁機回歸**：DEV 16 列綁舊指紋但**全為歷史照**（`is_current=false`），現行照 0 列綁機；STG/POC 皆 null → 無回歸。

相關卡：
- CM-1241 https://app.notion.com/p/FR-064-BUG-1-P0-hook-tamper-gunicorn-master-respawn-C-1-scheduler-3be346da4cd08132869ddb39342bf307
- CM-1242 https://app.notion.com/p/FR-064-BUG-2-P0-etc-machine-id-MAC-3be346da4cd081bd9495dc084c71845c
- 發現棒 CM-1229 https://app.notion.com/p/FR-064-T-7-1-License-3bd346da4cd08129b06afef7612b71d4

## 待辦地圖

1. ~~CM-1241 修 BUG-1~~ ✅（第 9 棒）／~~CM-1242 修 BUG-2＋複驗輪~~ ✅（第 10 棒，A-4 unlock 真循環＋socketio 死透一併銷案）
2. **🔴 user push 三支未 push commit**（`7cc7989e`／`11c9502e`／`19a086af`）→ 188 pull → **rebuild image**（含 BUG-1 修正的 1.14.0 已在 188 本機，但 BUG-2 是 compose 組態不需 rebuild）
3. **正式／STG／POC 套用 machine-id 掛載**（`docker compose up -d` recreate 兩容器）——**等決策者放行**，非開發期動作
4. ~~CM-1239 解鎖 SOP 文件~~（已完成 `91a3bcd8`，修正待驗證）；CM-1240 FE details 開關已收 Done（`29fead0` 待 push/部署）
5. version-bump（現標 1.14.0，含 FR-064 出貨前走 version-bump skill）＋母案 CM-1188 收尾（等 user 令收 Done）——**兩個 P0 已解除，此步的阻斷條件已消失**
5. 場外：CM-1204 待排程；probe「問卷上傳落點」存量問題待開卡（T-6.2 發現）；AGENT_CERT user 另 session；test/ 下 9 支 AI Dashboard 舊測試 collection error（與 scripts/archive 同批，待 user 裁）

> ⚠️ 本節是建議清單，不是執行授權。接手方讀完盤點完即停，等 user 發令。

### 竄改測試 SOP（接手複驗用，第 7 棒實戰校正版）

- **絕不對正式 compose 容器測**（read_only 改不了檔；且標記會寫進正式 /srv/guidant/pki，殘留會讓正式容器一直拒啟——本棒實際發生過，復原＝`rm /srv/guidant/pki/.integrity-tamper`＋compose 重起）
- 正確做法：拋棄式可寫容器＋`-v /tmp/pki-test:/opt/guidant/pki`；要看 FE 就讓它佔 8000（先 `docker rm -f guidant-api`，測完 compose 起回來）
- 改檔對象必須是 manifest 清單內的檔（resources 層模板檔最方便）；清單外新增檔案不觸發（設計如此，log/上傳/DB 都不在驗證範圍）
- unlock 眉角：fingerprint 抄**該容器**印的；解鎖前先把檔改回（否則解鎖後立刻再鎖，屬設計）；重啟用 restart 不可 rm（rm 換 fingerprint、token 作廢）

**第 8 棒補充（真產物實測校正）**：
- **要驗 DB 落點必須 `-e DB_NAME=guidant_ai_dev` 覆寫**——拋棄式容器沿用 `/srv/guidant/guidant.env` 會指向 STG（該庫無 `integrity_tamper_events` 表）。啟動 gate 本就不寫 DB，所以只驗 boot 拒啟時不必覆寫；驗 runtime／hook 落點時必須。
- **改運行中的 binary 要用 unlink+rename**：`printf >> /app/guidant-ai` 會被 `Text file busy` 擋下，正確作法 `cp → 改副本 → mv 原檔改名 → mv 副本頂上`。用前者會誤以為 binary 天然免疫。
- **Nuitka 產物的埋點實際只驗 binary 一檔**：`_HOTPATH_PRIORITY_PREFIXES` 那三個 `common/*` 前綴在真產物中不存在為獨立檔（全編進 binary），manifest grep 零命中 → 選檔退化成只剩 binary 本體。要觸發 hook 偵測，改的必須是 binary。
- 觸發 hook 的端點：`POST /api/1.0/license/upload`（需 **root tenant（`/1/`）admin** token，一般租戶 admin 會被 `GRC_403060` 擋在埋點前）或 SSP 報告匯出。

**第 10 棒補充（CM-1242 指紋與 unlock 實戰）**：
- **拋棄式容器必須加 `-v /etc/machine-id:/etc/machine-id:ro`**，否則指紋每次 restart 就變、unlock token 必然對不上（BUG-2 本體）。加了之後 188 上的指紋恆為 `ff793254d00c364107dda07846e5e90e4d4db79fbdbd2a4259fdf24e4a3da080`。
- **查指紋三個出口**（同一個值）：① 服務正常時 `GET /api/1.0/license/activation/machine-code`（需登入＋`DEPLOYMENT_MODE=host`，故拋棄式容器要 `-e DEPLOYMENT_MODE=host`）② 鎖定時看拒啟 log 的 `machine_fingerprint:` 行，或 lockdown 殼 503 JSON ③ 標記檔 `.integrity-tamper` 的 `machine_fingerprint` 欄位。
- **root tenant admin 帳號＝`admin`／密碼同 DEV 慣用**（`blsadmin` 是 tenant 102，打埋點端點會被 `GRC_403060` 擋掉）。
- **驗「過期」負面案不能用 CLI `--valid-hours`**（有 1–336 下限防呆，擋在簽發端就中止了，核銷的效期關根本沒執行到）——要用 LC 私鑰 `sign_payload` 直接造「真簽章但 `expires_at` 在過去」的 token（`load_private_key(Path("/opt/license_center/keys/private.pem"), os.environ["LICENSE_CENTER_KEY_PASSPHRASE"].encode())`）。
- **驗「重放」要先手工重建標記檔**（解鎖成功後標記已清），把同一個 `event_uid` 寫回 `.integrity-tamper` 再放已用過的 token。
- **socketio 承載無法用埋點觸發**：8002 不載 REST blueprint（`main.py` 設計），且抽查 4h 週期參數寫死不可調 → 要驗終止行為得自建 harness 重現承載條件（PID 1＋`monkey_patch(all=False, socket=True)`＋`socketio.run`＋`BackgroundScheduler`）但 mount 真源碼執行（`PYTHONPATH=/src`）。

## 環境紀律

- 開發只動 DEV；STG/POC 寫入等 user 明示
- 188 程式碼一律 git pull（**先 checkout feature/FR-064**——上次事故主因）
- STG 188 兩服務跑舊產物勿動
- LC 部署四步（pull → 依賴 → alembic `PYTHONPATH=src` → restart，見 memory `feedback_lc_deploy_four_steps`）
- 189 POC 的 LC 落後未對齊
