# FR-062 License 控管機制 — STATE（living，只描述此刻）

> 最後更新：2026-08-10（.8/.9/.10 三輪追加 ＋ STG/POC 雙環境部署完成，全案收尾中）
> 本檔為 STATE/LOG 雙檔制的 living 檔；脈絡與決策軌跡見同資料夾 `fr062-LOG.md`

## 🧭 原始需求（WHY）

產品（Guidant AI）準備上線，將以「雲端 SaaS」與「落地 Host」雙模式銷售。需要 License 控管：root tenant（系統維護特權租戶，永不開放客戶）以外的所有租戶，功能受 license 管控。要能簽發 license 給客戶開通、支援有效期與訂閱到期、按模組販售。**簽發端（私鑰）與產品端（只驗不簽）物理分離是整個信任模型的根基。**

## 角色定位

本 arc 首腦 session = **分析決策 / 協調者**：只做需求釐清、拍板、拆工派工、驗收抽查；一切實作與文件產出外包（subagent 或開 case 給新 session）。接手本檔者若也是首腦，同樣適用。

## 當前狀態（2026-08-10）

**階段**：**開發全部完成（含 .8/.9/.10 三輪追加）＋ STG/POC 雙環境部署完成**，全案收尾中。剩餘＝收尾文件批次 ＋ STG 兩租戶正式發照（決策者手動）＋ 雲端公網補驗。

| 項目 | 狀態 |
|---|---|
| .1 簽發端地基 | 完成（LC repo：引擎+CLI+DB；DEV kid `2ce3bb59a6f5a2ce`） |
| .2 產品端驗照 | 完成（tenant_licenses+root 後台+FE 兩頁；「上傳授權照」鈕已在 T-5.1 移除） |
| .3 狀態機+橫幅 | 完成（每日排程 cron+conditional UPDATE 冪等+FE 橫幅；root 特判） |
| .4 執法進場 | 完成（authz 第六軸+唯讀 gate+動態過濾；兩環境開關 `LICENSE_ENFORCEMENT_ENABLED`/`LICENSE_READONLY_GATE_ENABLED` 預設 true；POST 讀白名單逐支列舉制） |
| .5 開通 | 完成（T-5.1 離線+T-5.2 線上皆落地；四案驗收：happy path/錯誤序號/已用序號/LC 斷網 fallback） |
| .6 簽發 Web 後台 | 完成（Tabler UI+下載+詳情頁+列操作 Menu；passphrase 已 .env 化表單欄位移除） |
| .7 通知+migration | 完成（T-7.1 notify 信+T-7.3 grace/readonly 信=格式 v2.1+T-7.2 DEV 發照演練；SOP 在 `existing-tenant-license-migration-sop.md`，script 護欄只認 DEV——**STG/POC 實際未用此腳本，改走 .10 的「向 LC 請照」**） |
| **.8 角色矩陣 license 維度** | 完成（T-8.1~T-8.5：`ui-routes` 附 `is_licensed`／FE 未授權反灰空框（推翻初版「已授予但未生效」打勾）／`POST·PUT /roles` 寫入端守門只擋新增未授權點／provisioning 第三道扣除／選單結構調整 `is_platform` 正解＋`group-license` 群組＋關閉舊儀表板入口） |
| **.9 手動停權獨立欄位** | 完成（CM-1172~1174：停權升為獨立欄位、cron 不再覆寫人為決策、API 回傳旗標、FE 文案＋狀態快取失效；migration 回填**刻意不寫 `suspend_reason`**——該欄直接對客戶顯示） |
| **.10 產品後台向 LC 請照** | 完成（CM-1175~1182：LC API token 白名單＋管理頁＋三支內部簽發 API／BE `lc-issue`·`lc-extend`·`lc/plans` 走既有落地漏斗第五入口／FE 請照 dialog（新簽·展延·Trial 選項）／LC 總覽三態化＋同客戶分組＋排序＋搜尋篩選／design.md §4.7 圖 4 修正） |
| 照檔格式（最終形） | **v2 信封（zlib+base64 payload 打包，非加密）＋ v3 外皮（.license 副檔名 PEM 風格 armor）＋ v2.1 三段 send_email 旗標**；兩 repo 引擎零依賴各自實作、驗章對 unwrap 原文、v1/裸 JSON 向下相容 |
| 簽發鑰（三環境獨立） | DEV `2ce3bb59a6f5a2ce`／STG `04b1e65f100c6c8f`／POC `f3b562a6d9755527`，三把公鑰**共存**於 BE `common/license/public_keys.py`；私鑰各留該機不外流。STG 首把 `ef99f508c74e43fd` 因 passphrase 對不上作廢重產 |
| Notion 卡 | 開到 **CM-1183**；FR-062 全案子卡與母案 CM-1112 **今日已全數收 Done**（查詢「修正待驗證」為零）。唯一非 Done＝**CM-1143 遺留模組整鏈退役（Not started，全案後另議）** |
| 三 repo | **全部已 push，HEAD == origin/main**：BE `8c82e8e6`／FE `d00ba1b`／LC `d001b76`。BE 另有 `release/v1.14.0` branch（落後 main 2 commit，**v1.14.0 release note 尚未撰寫、pyproject 仍 1.13.0**） |
| DEV DB 現況 | `config.tenant_licenses` 24 列／events 27 列；現行照三租戶＝102 Billows Tech／131 歐洲航空／158 JEDI。**root（tenant 1）已無任何照**（先前的 T-7.1 殘留照已清） |
| STG DB 現況 | 現行照 2 張：102 Billows Tech（2027-08-10 到期）／131 AirAsia（2026-11-08，另有一張已被展延取代的 2026-10-09）。LC 側 `license_issuance` 3 筆（issuer=Staging×2、Raymond×1） |
| POC DB 現況 | 現行照 1 張：102 Billows Tech（host 模式，2027-08-10）。LC 側 1 筆（issuer=GuidantAI-POC）——首發那張 saas 記載錯誤的照已依決策者指示連同事件清空重發 |
| 服務 | 本機 BE :8000／FE dev :5180／LC :5062；STG 188 與 POC 189 皆 BE :8000＋LC :5062＋nginx :80（座標見下方環境座標節） |

## Notion 卡對照表

母案 CM-1112。子需求：.1=CM-1113 / .2=CM-1116 / .3=CM-1120 / .4=CM-1123 / .5=CM-1128 / .6=CM-1131 / .7=CM-1134。
初版子任務：T-1.1=1114、T-1.2=1115、T-2.1=1117、T-2.2=1118、T-2.3=1119、T-3.1=1121、T-3.2=1122、T-4.1=1124、T-4.2=1125、T-4.3=1126、T-4.4=1127、T-5.1=1129、T-5.2=1130、T-6.1=1132、T-6.2=1133、T-7.1=1135、T-7.2=1136。
驗收回饋期追加：T-1.3=1137（v2 打包）、T-1.4=1138（passphrase 環境變數化+金鑰重產）、T-6.3=1139（下載+明文顯示）、T-1.5=1140（.license armor）、T-0.1=1141（design.md 回寫批次）、T-2.4=1142（root 狀態頁顯示）、退役案=1143（cruise/resource/report，全案後）、T-4.5=1144（反灰鋪面）、T-6.4=1145（詳情頁+Menu）、T-7.3=1146（grace/readonly send_email）、總驗收清單=1147、T-0.2 三文件同步=1148。

**FR-062.8 角色矩陣 license 對齊**：T-8.1=1167（`ui-routes` 附 `is_licensed`）、T-8.2=1168（FE 反灰）、T-8.3=1169（`roles` 寫入端守門）、T-8.4=1170（provisioning 扣除）、T-8.5=1171（選單結構調整）。
**FR-062.9 手動停權**：母案=1172、T-9.1 BE=1173、T-9.2 FE=1174；回歸 bug（登出後誤打 license status）=1183。
**FR-062.10 向 LC 請照**：母案=1175、T-1 LC=1176、T-2 BE=1177、T-3 FE=1178、T-4 docs=1179；驗收當下追加＝1180（開通狀態三態化）、1181（同客戶分組＋排序）、1182（搜尋＋下拉篩選）。

**未開卡即修的兩筆**（決策者裁示併收尾、不補開卡）：BE `8c82e8e6`（請照未指定 `deployment_mode` 時帶本站台設定）、FE `d00ba1b`（請照 dialog 補照型態選項，admin 可發 Trial）。

## 剩餘工作隊列（2026-08-10 收尾時點）

開發與部署已無在途工作。剩下六項：

```
等 user  (a) 三 repo push ── 本次已 push 完（HEAD==origin/main），僅未來新 commit 適用；
             另 v1.14.0 若要正式發版，release note ＋ pyproject bump 尚未做（走 version-bump skill）
決策者   (b) STG 兩租戶正式發照 ── BE/LC 已就緒，發照動作由決策者在 root 後台手動執行
             （現有 2 張為部署驗證照，是否沿用或重發由決策者定）
待部署   (c) POC FE 部署 ── 已於 2026-08-10 20:42 完成（/var/www/html/audit-manager，
             已驗證含 type_trial / lc_extend 字串）；此項可視為完成，若有後續改動需重 build
等雲端   (d) 雲端 LC 公網補驗 ── TLS／網域／防火牆，清單在 CM-1130 卡內；目前 LC 皆為
             內網 :5062，BIND_HOST 已環境變數化
上正式前 (e) 正式環境簽發鑰 ── 產出後必須先進 common/license/public_keys.py 再 build 出貨
             image，順序顛倒＝產品收不到正式照（會報「未知的 kid」）
全案後   (f) CM-1143 遺留模組整鏈退役（cruise-project/resource/report＋jedi-resource-store）
```

**未套的 migration（決策者裁示這次不動，非遺漏）**：`2026-07-20-ssp-shared-metadata-DEV-cleanup`（DEV 專用清理）、`2026-08-01-fr058-22-openscap-content-path-advanced`、`2026-08-03-fr060-1-detection-profile-split`——三支 DEV 有、STG/POC 無。

## 已定裁示（不重開）

- D1–D15 全定案（見 design.md §2），含五項晚於 discussion v2 的最終修正：過渡照刪除（type 僅 formal/trial/extension）、簽發端自建（不採 Keygen）、Plan=簽發端 DB 資料、升級換發立即生效、數字定案（子租戶上限 5；expiry_policy 出廠預設 notify30/grace14/readonly 終態/lockout 關）
- 照檔格式（實作期三次演進，全部落地+design.md §4.1 同步）：**v2 信封**＝payload zlib+base64 打包，**非加密**（防翻閱嚇阻，文件不可宣稱加密）；**v3 外皮**＝`.license` 副檔名+PEM 風格 armor（BEGIN/END GUIDANT LICENSE）；**v2.1**＝grace/readonly 段補 `send_email`（from_dict 缺鍵容忍=硬需求）。簽章永遠對 unwrap 原文；v1/裸 JSON 向下相容（未出貨拒收無收益）
- .6 Web 後台 UI＝Tabler（MIT，pin 版入 static/vendor/，不依賴 CDN）；顯示用明文、交付用信封；下載只限現行照（get_if_current 守門）
- 簽發 passphrase＝`.env` `LICENSE_CENTER_KEY_PASSPHRASE` 自動解鎖，表單欄位僅未設時顯示；正式鑰仍須密碼管理器+兩處站外備份（金鑰遺失=無法簽續約，上版前要再提醒 user 安排）
- 照內 tenant_id＝簽發端客戶識別代稱，產品端不強比對；實質綁定＝SaaS root 指派動作/Host 機器指紋
- 通知收件人＝三者聯集（is_super_admin ∪ is_admin 角色成員 ∪ tenant.update capability 持有者，走 v_user_capabilities）；空租戶 fallback 寄 root 管理員+log 警告（DEV 實據：is_admin/is_super_admin 覆蓋殘缺，131 只有 capability 收得到）
- `TENANT_ADMIN_EXCLUDED_RESOURCE_TYPES` **不退役**（實作者抗命經覆核接受：provisioning-time 預設授予 vs request-time 商務授權兩維度，license 取代不了；詳見 CM-1126 裁決段）；cruise/resource/report 三類=全案後整鏈退役（CM-1143）、dashboard 留、workflow 頁面不動嚴禁誤傷 flow engine
- T-5.2 裁示（2026-08-09）：**不等雲端，local 模擬**（HTTP 流程 localhost 即等價）；公網/TLS/網域留部署後補驗——CM-1130 卡有完整更新段
- LC 架構裁示：**不導 DDD**（2,268 行分層架構+Service Layer 已足；領域核心純函式+依賴單向是真正要守的）；演進觸發條件=第二產品線接入/訂單結算領域進場/多人維護——屆時再升級。LC repo 補 CLAUDE.md（分層守則）可併收尾批次
- DB 命名新標準：`license_center_{dev|stg}`，**prod 不帶後綴**；DB user 暫沿用 `cmmgr`（上 PRD 前換）
- 到期終態=永久唯讀可下載匯出，不鎖登入（lockout 保留預設關）；能通知就通知

### 追加裁示（2026-08-10）

- **三環境各自獨立簽發鑰**（決策者要求 POC/STG 不得與 DEV 同鑰）：每環境自行 keygen，**私鑰永不離開該機**，公鑰以新 kid 加進 BE `PUBLIC_KEYS` **多把共存**（不取代舊鑰，否則舊環境的照全部驗不過）。連帶紀律：每次 keygen ⇒ 一個 BE commit ⇒ 該環境 BE 重新部署，順序錯即「未知的 kid」
- **不做「非 production 只能簽 trial」的強制規則**：曾考慮此防呆，最終不採——主站管理員即決策者本人、LC 環境未來也會與產品環境分開部署；token 白名單的目的是**管理便利與可追溯**（可撤銷、發照紀錄看得出來源環境），不是防範內部誤用
- **API token 白名單設計刻意保持最小**：只做「新增 / 列表 / 停用」，不做完整 CRUD 編輯。撤銷用停用不刪列（保留歷史發照的 issuer 追溯）；token 只存 hash 不存明文
- **產品端不因來源是 LC 就跳過驗章**：`lc-issue`／`lc-extend` 回照仍走完整 `_verify_signed_license` + `_store_verified_license`，BE 是唯一信任邊界；手動上傳路徑保留不動，作為 LC 不可達時的天然 fallback
- **請照時產品必須自報 `deployment_mode`**：未指定時帶本站台 config，不讓 LC 用預設值猜（POC 首發曾因此簽出 saas 記載錯誤的照）

## 環境座標

**本機開發**
- 主產品 BE：`~/Projects/Billows/Audit-Manager/compliance-manager-be`（branch **main**）；:8000（main_app.py，重啟必 kill -9）
- 簽發站：`~/Projects/Billows/Audit-Manager/license_center`（branch **main**）；web :5062（`PYTHONPATH=src poetry run python -m license_center.web.app`）；帳密/passphrase/DB 連線都在 LC `.env`；測試 `PYTHONPATH=src poetry run pytest`
- FE dev：:5180（`npm run dev`；多開殘留曾佔 5181-5183，砍舊 PID 再起）
- DB：`guidant_ai_dev` ＋ `license_center_dev`（188:25432，cmmgr）
- 文件站預覽：repo 根 `python3 -m http.server 8100`

**STG（192.168.50.188）**
- BE：`/opt/compliance-manager-be`（:8000）
- LC：`/opt/license_center`，**systemd 常駐** `license-center.service`（enabled+active，`ExecStart=/opt/license_center/.venv/bin/python -m license_center.web.app`，`BIND_HOST=0.0.0.0`，:5062）
- FE：nginx `/var/www/html/audit-manager`（:80）
- DB：`guidant_ai_stg` ＋ `license_center_stg`（188:25432）
- 簽發鑰 kid `04b1e65f100c6c8f`；LC API token `Staging`（environment=stg）

**POC（192.168.50.189）**
- BE：`/opt/compliance-manager-be`（:8000）
- LC：`/opt/license_center`（:5062，已起）
- FE：nginx `/var/www/html/audit-manager`（:80）
- DB：`guidant_ai_poc` ＋ `license_center_poc`（189:25432）
- 簽發鑰 kid `f3b562a6d9755527`；LC API token `GuidantAI-POC`（environment=production）
- ⚠️ POC 等同 production，**唯讀操作可自由做，任何寫入／部署一律等決策者當次放行**

**LC 部署手冊**：`~/Projects/Billows/Audit-Manager/license_center/docs/deployment-guide.html`（以 STG 實戰部署為藍本，含 systemd unit、環境變數、keygen 與公鑰接線步驟）

- Notion 操作一律 `scripts/notion_case.py`（get/status/append/query）；**開卡 script 無 create 子命令**——API 直打 POST /pages（database id `23c346da4cd0801d8b67dbd9e73cf246`，title 屬性「任務名稱」，本棒開了 10 張全用此法，pattern 見任一次對話或 import notion_case 模組復用其 helpers）

## 行為規範提醒（每棒都適用）

- 派工必引用實際 Notion 卡號；完成回寫子卡 `修正待驗證` + 白話補充（不等令）；母卡/子需求卡收尾等令
- 顯式 git add、禁 -am、不 push（等 user 明示）、不切 branch
- migration 只套 DEV；STG/POC 需決策者當次放行（T-7.2 script 已內建 DB 白名單護欄）。**2026-08-10 的 STG/POC 套用是決策者當次明示放行的部署動作，不構成常態授權**
- 對 STG/POC **唯讀操作（SELECT / pg_dump / 看容器 / 讀設定）一律可做不需請示；任何寫入（含 migration、重啟、部署）一律停下問決策者**
- root tenant 無條件豁免 license 執法；`/license/` 前綴整段豁免唯讀 gate（防死鎖）
- 首腦驗收紀律（本棒實踐有效，建議沿用）：每棒回報後 fetch 卡+看 commit+親跑測試+查 DB 實況，不信自報；user 手測過才收 Done；裁決發生時回填到產生疑問的那張卡（user 會催「有沒有回填」）
- user 風格備註：驗收很細（親測 UI、抓過 dropdown 裁切/時區/i18n 缺漏），常在驗收中丟出新需求——每條都開卡落痕再派，小修可併棒（如 T-2.4 併 .3、i18n 併 T-1.3）
