---
title: "Agent 授權照：一台 Agent 一張照、機器碼開通、離線可用 — 需求討論稿 (FR-182)"
brand: "Guidant AI · **FR-182** Agent 授權照"
eyebrow: "FR-182 · 需求討論稿 v1 · 2026-11-07"
h1: "Agent 也要有照：比照主系統，先改 License Center"
subtitle: "一台 Agent 一張照、綁機器碼、有到期日；沒有功能分級；客戶封閉網路靠離線開通；第一期只做 License Center 端"
lede: "主系統從 FR-062 起每個租戶都要有一張簽過章的授權照才能用，但 **Agent 完全沒有授權檢查**——拿到註冊 token 就能裝、能跑、裝幾台都行。決策者 11-07 定調：Agent 要用**跟主系統一樣的模式**拿照——License Center 簽發、給序號、客戶抄機器碼來開通、拿到綁機的照上傳；Agent 不分功能等級，只分「有照／沒照」；客戶在封閉網路，所以**離線開通一定要成立**。本稿先把 License Center 端要長什麼樣講清楚，Agent 端與主系統端只定方向。"
chips: [
  {text: "決策者已裁：比照主系統模式", kind: ok},
  {text: "決策者已裁：Agent 不分功能，只驗有效", kind: ok},
  {text: "決策者已裁：離線開通必做", kind: ok},
  {text: "D1～D8 建議已口頭同意，待看雛形確認", kind: warn},
  {text: "第一期只動 license_center repo", kind: accent},
]
footer: "FR-182 需求討論稿 v1 · 首腦撰寫 · 2026-11-07 · 前作 FR-062（主系統授權照）、FR-066 D6（Agent 公鑰與驗章）、FR-166（Agent 主控台）"
---

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

**整案一句話**：License Center 多一種照叫「Agent 照」，一台 Agent 一張、綁那台機器碼、有到期日、沒有模組清單；員工在 LC 後台簽發拿序號，客戶在 Agent 主控台看到機器碼、把序號＋機器碼交回來，員工在 LC 後台離線開通、把照寄回，客戶上傳到 Agent；沒照的 Agent 不向主系統註冊、不跑掃描。

| 階段 | 做什麼 | 產出 | 完成怎麼判定（決策者檢查法） |
|---|---|---|---|
| **P1 License Center**（本稿範圍） | LC 新模組 `agent_licensing`：資料表、簽發／開通／換綁／補發／展延、後台五個頁、CLI、兩支機器 API、文件 | license_center repo 一版 | 開 LC 後台 → 「Agent 授權」→ 簽一張（數量 3）→ 看到 3 組序號 → 拿任一序號＋隨便一串 64 碼當機器碼走「離線開通」→ 下載到 `.license` 檔 → CLI `agent-verify` 驗過 |
| P2 Agent 端 | Agent 驗 Agent 照（既有驗章引擎加一種型別）、主控台「授權」頁（顯示機器碼、上傳照、看狀態）、沒照就不註冊不心跳不掃描 | evidence-agent 一版 | 123 的 Agent 刪掉照 → 主控台顯示「尚未開通」＋機器碼、主系統 Agent 列表變離線；上傳 P1 簽的照 → 主控台顯示「已授權，到期 2027-xx-xx」、主系統列表恢復上線 |
| P3 主系統端 | Agent 列表多一欄「授權到期」（Agent 用既有狀態事件回報）；線上開通代打 LC（Agent → 主系統 → LC） | BE＋FE 一版 | 主系統「遠端 Agent」頁每台看得到到期日；在 123 Agent 主控台輸入序號按「線上開通」，10 秒內變已授權 |

**依賴一句**：P1 獨立可做、現在開卡；P2 要 P1 簽得出照，**且要等 FR-181（Agent 長輪詢）與 FR-179（Agent 首次設定頁）的 Agent 卡合回**（接點見 §2.4）；P3 要 P2 的事件格式。**決策者功課**：§4 的 D1～D8 看一次雛形後正式拍板。

---

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

### 1.1 現況：主系統有照，Agent 沒有

| | 主系統（FR-062 起） | Agent（現況） |
|---|---|---|
| 怎麼拿到授權 | LC 簽照 → 序號 → 開通綁機器碼 → 上傳照 | 主系統管理員產一組註冊 token，裝 Agent 時填進去 |
| 誰能裝 | 有照的租戶 | 拿到 token 的任何人、任何台數 |
| 到期 | 照上有到期日，狀態機：正常→提醒→寬限→唯讀→鎖定 | 沒有到期概念 |
| 驗章 | `jedi-license-runtime` 套件，公鑰編譯進去 | **已經有**：啟動時驗產物清單（`common/integrity/verify.py`），公鑰同一批、同樣編譯進 binary |
| 機器碼 | `sha256(machine-id)` | **已經有**：`device_uuid = sha256(主機板 product_uuid ∣ machine-id)`，註冊主系統時就是用它 |

結論：Agent 已經有驗章引擎、有機器碼、有編譯進去的公鑰，**缺的只是「照」這個東西和「沒照怎麼辦」的規則**。LC 端則是零——它只認得主系統的照。

### 1.2 為什麼不把 Agent 台數塞進主系統的照裡

最省事的做法是主系統照的 `limits` 加一個 `max_agents: 5`，主系統註冊第 6 台時拒絕。排除理由：

- **Agent 本機沒有東西可驗**——它只能信主系統說「你可以」。客戶封閉網路、主系統不通時，Agent 連自己有沒有授權都不知道，離線開通做不出來。
- **不是「一樣的模式」**——決策者要的是 Agent 自己拿機器碼來註冊，跟主系統一樣的體驗。
- 台數額度綁在主系統照上，客戶加買 Agent 要重簽主系統的照（換照＝重新開通一次），麻煩。

所以是**獨立一張照**，照上記得它屬於哪個客戶、掛在哪張主系統的照底下（可空），LC 總覽看得出「這個客戶買了主系統＋3 台 Agent」，到期日各自算。

---

## 2. 現況盤點：能複用什麼、缺什麼 {#inventory nav="現況"}

### 2.1 License Center 端

| 既有能力 | 位置 | 本案怎麼用 |
|---|---|---|
| 泛用簽章 `sign_payload(payload, key, kid)`，按 `payload_type` 分型 | `core/crypto/engine.py` | **reuse**：加常數 `PAYLOAD_TYPE_AGENT_LICENSE = "agent_license"`，Agent 照直接走它簽 |
| 驗章 `verify_payload(signed, expected_type)` | 同上 | **reuse**：CLI `agent-verify` 用 |
| `.license` armor 包裝 `armor_license`／`dearmor_license` | 同上 | **reuse**：下載檔格式與主系統照一致 |
| 序號產生 `new_activation_code()`、序號效期 `ACTIVATION_CODE_TTL`、逾期判斷 | `modules/licensing/service.py` | **extend**：這三個與「照」無關、純序號邏輯，**抽到 `core/activation_code.py`** 讓兩個模組共用（不從 licensing 模組 import——鐵律③跨模組只准叫對方 service，而這不是 licensing 的業務） |
| 開通失敗鎖定（來源 IP、15 分鐘 5 次、全站預算） | `modules/auth/service.py` `check_activation_lockout` | **reuse**：Agent 線上開通端點直接用同一套、同一張 `activation_attempts` 表 |
| 到期狀態推算 `compute_status(expires_at, policy)` | `core/status.py` | **extend**：Agent 照的到期政策只有兩段（提醒／寬限），加一支 `compute_agent_status(expires_at, notify_days, grace_days)` 放同檔，不塞 if |
| 私鑰解鎖（env passphrase 優先、表單備援） | `web/views/issuance.py` `_unlock_or_flash_redirect` | **extend**：搬到 `web/unlock_helper.py` 兩組 view 共用（原本就只被 issuance 用，搬出來不改行為） |
| 後台登入、CSRF、CSP、log 規範、`request_id` | `web/` | **reuse**：新頁照既有規矩掛 |
| Alembic | `migrations/` | **reuse**：新表一支 revision |
| 四支機器 API 合約凍結 | `docs/api-contract-baseline.md` | **不動**：Agent 的機器 API 全部是新路徑 |

### 2.2 Agent 端（P2 用，本稿只定方向）

| 既有能力 | 位置 | 本案怎麼用 |
|---|---|---|
| 機器碼 `device_uuid` | `guidant_agent/platform/fingerprint.py` | **reuse**：主控台顯示給客戶抄；LC 存的 `machine_fingerprint` 就是它 |
| 驗章 `verify_payload(signed, expected_type)` ＋ 編譯進去的 `PUBLIC_KEYS` | `common/integrity/` | **reuse**：加 `expected_type="agent_license"` 就能驗 Agent 照；**不裝 `jedi-license-runtime`**（它帶 PG 表、租戶解析、Flask API 一整套，Agent 是單租戶 SQLite，用不上） |
| 主控台（登入、頁面、本機設定） | `guidant_agent/console/` | **extend**：加「授權」分頁 |
| 註冊／心跳背景元件 | `guidant_agent/sync/app/enroll.py` | **extend**：啟動時先問授權狀態，沒照就不起這個元件 |
| Agent 自報狀態事件 `agent_status` | `guidant_agent/sync/app/agent_status.py` | **extend**：多帶 `license` 一節（狀態、到期日、照號） |

### 2.3 主系統端（P3 用）

| 既有能力 | 位置 | 本案怎麼用 |
|---|---|---|
| 主系統向 LC 請照（API token、`lc-issue`） | `jedi-license-runtime` + LC `/api/internal/*` | **extend**：root 後台加「向 LC 請 Agent 照」（第二期） |
| 主系統代打 LC 線上開通 | `activate_license_online()` | **reuse 做法**：Agent 線上開通由主系統 BE 代打 LC `/api/activation/agent/activate`，Agent 不直連 LC（FR-181 Agent 只出站、只連主系統） |
| Agent 列表 | 遠端 Agent 管理頁 | **extend**：加「授權到期」欄 |

### 2.4 與 FR-181（Agent 只出站）、FR-179（安裝精簡）的接點

決策者 11-07 提醒 FR-181 是 Agent 通訊方式大改，要對一次。對完的結論：**方向不衝突，P1 零影響；P2 有三個接點要排在那兩案的 Agent 卡之後**。

| 接點 | FR-181／FR-179 在做什麼 | FR-182 P2 要做什麼 | 怎麼不撞 |
|---|---|---|---|
| **「沒照不收指令」的位置** | FR-181 把指令通道從 8443 直推改成 Agent 自己長輪詢（新元件 `command_pull`，跟心跳、設定拉取一樣是 sync 模組的背景元件） | 沒照時不註冊、不心跳、不收指令、不掃描 | **閘門放在「sync 模組所有背景元件起不起」這一層**（`enroll_for_auth` 之前一道判斷），不是逐條通道各加 if。FR-181 多一條長輪詢、以後再多一條什麼，都自動被閘住。閘門不碰 `command_pull.py`、不碰 `registry.py` |
| **登記型檔 `platform/registry.py`** | FR-181 F-2 是它的 owner（登記 `command_pull` 元件） | 新模組 `license` 要登記進 `registry.MODULES`（申報 `status_fields` 讓狀態事件帶授權欄、申報主控台 blueprint） | P2 排在 F-2 合回之後；P2 的卡自己當 `registry.py` 下一個 owner |
| **主控台未註冊可登入** | FR-179 A1：未註冊時放行本機管理員登入與首次設定 API；A2：首次設定頁（填主系統網址、token、註冊） | 未開通時主控台可登入、看機器碼、上傳照 | 同一道「未註冊放行」的門，FR-179 A1 開了 P2 直接用；**順序是「授權 → 首次設定（註冊）」**：A2 的首次設定頁前面多一步「上傳授權照」，沒照時後面的步驟灰掉。P2 排在 A1／A2 合回之後，首次設定頁的改動在 P2 卡 |
| **線上開通的認證** | FR-181：Agent 只能出站、所有呼叫帶通行證（註冊後才有） | 線上開通是 Agent 發起、同步等主系統代打 LC——出站、同步，形狀合 FR-181 | 但**沒照的 Agent 還沒註冊、沒有通行證**，所以主系統的線上開通端點要跟 `/agents/register` 同一道門：**用註冊 token 認證**，不是通行證。P3 開卡時寫進去 |
| **狀態回報** | FR-171 起 Agent 自報狀態走 `agent_status` 事件，各模組用 `registry` 申報 `status_fields` | 事件多帶 `license` 一節 | 走申報掛點，不改 `agent_status.py` |

**FR-181 退役 8443 六個指令 blueprint**對本案無關——本案在 8443 上不加任何東西；主控台「授權」頁在 8444。

**時序**：P1（LC）現在開；P2 等 FR-181 批 2（F-2）與批 5（A-3）、FR-179 批 1（A1）與批 2（A2）合回後再拆卡；P3 等 P2。

---

## 3. 設計 {#design nav="設計"}

### 3.1 端到端：一台 Agent 怎麼拿到照

```{.mermaid cap="圖 1 — 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'}}}%%
sequenceDiagram
    participant E as 我們的員工
    participant L as License Center
    participant C as 客戶
    participant A as Agent 主控台
    participant M as 主系統

    Note over E,L: 簽約後
    E->>L: 「Agent 授權」→ 簽發：客戶、訂單、數量 3、天數 365
    L-->>E: 3 張照（未綁機）＋ 3 組序號
    E->>C: 交付序號（email 等既有管道）

    rect rgb(238, 242, 243)
    Note over C,A: 路徑 B — 離線開通（第一期）
    C->>A: 裝好 Agent，登入主控台 → 「授權」頁顯示「尚未開通」＋機器碼
    C->>E: 序號 ＋ 機器碼
    E->>L: 「Agent 離線開通」：輸入序號＋機器碼
    L-->>E: 綁定該機器碼的照（.license）
    E->>C: 回寄照檔
    C->>A: 上傳照
    A->>A: 驗章＋核對機器碼＋到期日 → 「已授權」
    A->>M: 開始註冊／心跳／回報授權狀態
    end

    rect rgb(226, 240, 241)
    Note over C,L: 路徑 A — 線上開通（第二期，Agent 不直連 LC）
    C->>A: 「授權」頁輸入序號 → 線上開通
    A->>M: 序號＋機器碼（走既有出站通道）
    M->>L: 代打 /api/activation/agent/activate
    L-->>M: 綁機的照
    M-->>A: 照
    A->>A: 驗章 → 「已授權」
    end
```

### 3.2 Agent 照的內容

跟主系統的照同一個信封（Ed25519、`format_version` 2、armor 後叫 `.license`），差在 payload：

| 欄位 | 值 | 跟主系統照比 |
|---|---|---|
| `payload_type` | `"agent_license"` | **新**：主系統照沒這個鍵（缺鍵＝主系統照，向後相容的支點）；Agent 驗章時 `expected_type` 指定它，主系統的照拿來上傳會被拒 |
| `license_id` | `alic-` 前綴＋12 碼 | 前綴不同，一眼分得出 |
| `customer_code`／`order_no`／`issued_to` | 同主系統 | 同 |
| `parent_license_id` | 主系統照的 `license_id`，**可空** | **新**：掛在哪張主系統照底下；只是對照用，Agent 不驗它 |
| `type` | `formal`／`trial` | 少 `extension`（展延照直接換新照，不另立型態） |
| `issuer`／`issued_at`／`starts_at`／`expires_at` | 同主系統 | 同 |
| `expiry_policy` | `{"notify_days_before": 30, "grace_days": 14}` | **簡化**：主系統是四段式（提醒／寬限／唯讀／鎖定），Agent 沒有「唯讀」可言，只留提醒與寬限 |
| `machine_fingerprint` | Agent 的 `device_uuid`（64 碼 hex），開通前空 | 同（算法不同但 LC 不管算法） |
| `product_major` | 1 | 同（FR-167 更新站用，先帶著） |
| `kid` | 簽章鑰指紋 | 同 |
| ~~`tenant_id`~~ ~~`plan`~~ ~~`modules`~~ ~~`limits`~~ | 不要 | Agent 不分功能、不綁租戶 |

### 3.3 LC 資料表：`agent_license_issuance`

新模組 `modules/agent_licensing/`，自己一張表，**不塞進 `license_issuance`**（那張表 `tenant_id`／`plan_code`／`modules` 必填，塞進去要全改可空、總覽頁與狀態推算到處加「如果是 Agent」分支）。欄位比照 `license_issuance` 的 append-only 事件帳：

| 欄位 | 說明 |
|---|---|
| `agent_license_id` | `alic-…`，unique |
| `batch_id` | 同一次簽發 N 張共一個批號，結果頁按批列、打包下載 |
| `customer_code`／`customer_name`／`order_no` | 商務識別，同主系統 |
| `parent_license_id` | 可空；存字串、不設 DB 外鍵（跨模組不碰對方的表；簽發表單用 licensing 模組的 service 查清單給下拉） |
| `license_type`／`product_major` | 同主系統 |
| `starts_at`／`expires_at`／`expiry_policy`（jsonb） | 同主系統，policy 只有兩個數字 |
| `activation_code`／`activation_code_expires_at`／`activation_code_revoked_at` | 序號三欄，語意同主系統（`token_urlsafe(16)` 約 22 碼、30 天效期、補發作廢舊序號） |
| `machine_fingerprint`／`activated_at`／`rebind_history` | 開通狀態，同主系統 |
| `issuer`／`event_type`／`license_payload`／`created_at` | 稽核；`event_type` ∈ new／activate／rebind／reissue／extend |

### 3.4 LC 後台頁（雛形：[prototype/lc-agent-license.html](prototype/lc-agent-license.html)）

導覽列加一項「Agent 授權」，底下五個頁，全部照既有 Tabler 樣式：

| 頁 | 路徑 | 做什麼 |
|---|---|---|
| 清單 | `/agent-licenses/` | 每台一列：客戶、照號、上層主系統照、型態、到期日、推算狀態、機器碼前 12 碼、開通狀態；搜尋客戶代碼／名稱／訂單；篩狀態；列尾下拉：詳情／下載／展延／補發／換綁 |
| 簽發 | `/agent-licenses/issue` | 客戶代碼、客戶名稱、訂單編號、上層主系統照（下拉，選填，列該客戶現行照）、型態、天數、**數量**、提醒天數、寬限天數、發照人 |
| 簽發結果 | `/agent-licenses/batch/<batch_id>` | 這批 N 張：每列照號＋序號＋下載；「全部下載（zip）」；提醒：序號 30 天內要開通 |
| 離線開通 | `/agent-licenses/activate` | 序號、客戶機器碼、發照人 → 簽出綁機的照 → 結果頁下載 |
| 詳情 | `/agent-licenses/<id>` | 明文欄位、事件歷史（簽發／開通／換綁…）、操作：下載（只有現行照）、展延、補發、換綁（換綁人工受理，同主系統政策） |

客戶總覽（主系統照那頁）**不動**，只在客戶時間軸頁加一行「Agent 照 N 台 → 看清單」連過去。

### 3.5 LC 機器 API（新路徑，不碰凍結的四支）

| 端點 | 呼叫端 | 認證 | 第一期 |
|---|---|---|---|
| `POST /api/activation/agent/activate` | 主系統 BE（代 Agent 打） | 無 token，序號＋機器碼＋既有 IP 鎖定 | 做（薄殼，跟現有 `/activate` 同形） |
| `POST /api/internal/agent/issue` | 主系統 root 後台 | `X-API-Token` 白名單 | 做（薄殼；主系統端的按鈕是第二期） |
| `GET /api/internal/agent/licenses?customer_code=` | 主系統 root 後台 | 同上 | 做（給主系統查某客戶 Agent 照清單） |

回應形狀比照既有：成功 `{"license": {…信封…}, "license_id": "alic-…"}`，失敗 `{"error": "…"}`；驗證順序比照既有「格式 → 鎖定 → 查無 → 已使用 → 序號逾期 → 授權過期 → passphrase」。進 OpenAPI、進 `tests/test_api_contract.py` 新情境表。

### 3.6 CLI

`agent-issue`（同表單欄位，`--count N`）、`agent-activate`（序號＋機器碼）、`agent-verify <檔>`、`agent-extend`、`agent-rebind`。保底用，跟既有 `issue`／`activate` 一樣的形狀。

### 3.7 Agent 端的規則（P2，雛形：[prototype/agent-console-license.html](prototype/agent-console-license.html)）

**照存哪**：`AGENT_DATA_DIR/license/agent.license`（資料目錄，升級不動）。

**狀態**（Agent 自己算，不問主系統）：

| 狀態 | 條件 | Agent 做什麼 | 主控台顯示 |
|---|---|---|---|
| 尚未開通 | 沒有照、照驗不過、機器碼對不上 | **不註冊、不心跳、不跑掃描、不收指令**；主控台可登入 | 「尚未開通」＋機器碼（可複製）＋上傳照 |
| 正常 | 驗過、未到提醒日 | 全功能 | 「已授權」＋到期日 |
| 到期提醒 | 到期前 `notify_days_before` 天內 | 全功能 | 黃色提示「N 天後到期」 |
| 寬限 | 過期、未超過 `grace_days` | 全功能 | 紅色提示「已過期，寬限剩 N 天」 |
| 已過期 | 超過寬限 | 回到「尚未開通」的行為 | 「授權已過期」＋機器碼＋上傳照 |

**不擋啟動**：啟動閘門（防竄改）維持原樣；授權檢查在閘門之後、起 sync 模組背景元件（註冊、心跳、設定拉取、FR-181 的長輪詢）之前——一道門關掉整個 sync 模組，不逐條通道加 if（§2.4）。擋啟動的話客戶連主控台都進不去、看不到機器碼，開通流程走不下去。上傳照成功後**不用重啟**：閘門在同一行程內放行、起 sync 元件（照 FR-179 A1 「未註冊 → 首次設定頁註冊」的同一條路）。

**機器碼對不上**（照是別台的）：視同沒照，主控台明講「這張照綁的是另一台機器（前 12 碼 …），請洽原廠換綁」。

**時間倒轉**：主系統照有時鐘水位（`clock_watermark`）防把系統時間撥回去。Agent 第一期不做，列在 §7。

### 3.8 主系統端（P3）

- Agent 的 `agent_status` 事件多帶 `license: {status, expires_at, license_id}`；主系統落到 `agents` 表兩欄（`license_status`、`license_expires_at`），遠端 Agent 列表多一欄。
- 第二期：Agent 主控台「線上開通」→ 走出站通道到主系統 → 主系統 BE 代打 LC → 照回傳 Agent。主系統只是中轉，**仍由 Agent 自己驗章**。
- 第二期：root 後台「向 LC 請 Agent 照」（比照 `lc-issue`）。

---

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

決策者 11-07 已口頭同意下列建議；寫成條列是為了看雛形時再核一次，看完沒意見就當定案。

::: {.callout .decided}
**D1 Agent 照與主系統照的關係：獨立一張、記上層照號**

獨立一張照，`customer_code` 必填、`parent_license_id` 選填。排除「主系統照加 `max_agents`」（§1.2）。

[**定案**：獨立照。]{.rec}
:::

::: {.callout .decided}
**D2 LC 資料怎麼存：新模組、新表**

`modules/agent_licensing/` 自己一張 `agent_license_issuance`，不改 `license_issuance`。序號邏輯與私鑰解鎖抽到共用層，不是從 licensing 模組 import。

[**定案**：新模組新表。]{.rec}
:::

::: {.callout .decided}
**D3 照的內容：`payload_type: agent_license`，沒有模組與方案，到期政策兩段式**

§3.2 的欄位表。

[**定案**：照 §3.2。]{.rec}
:::

::: {.callout .decided}
**D4 機器碼：直接用 Agent 的 `device_uuid`**

不另算一套。VM 模板複製會讓兩台機器碼相同、一張照兩台都能用——主系統註冊的撞號偵測已經擋第二台，LC 不處理。

[**定案**：用 `device_uuid`。]{.rec}
:::

::: {.callout .decided}
**D5 開通路徑：第一期離線；線上開通走 Agent → 主系統 → LC，第二期**

LC 第一期順便把線上開通端點做出來（薄殼），主系統之後再接。Agent 永遠不直連 LC。

[**定案**：離線先行，線上端點先做、主系統端第二期。]{.rec}
:::

::: {.callout .decided}
**D6 沒照或過期的 Agent：不擋啟動，但不註冊、不心跳、不掃描**

§3.7 的狀態表。寬限期全功能、過了寬限回到「尚未開通」。

[**定案**：照 §3.7。]{.rec}
:::

::: {.callout .decided}
**D7 批量簽發：表單加「數量」，一次產 N 張各自獨立的照與序號**

不做「一張照 N 個座位」的共享池——每台一張才能各自換綁、各自到期。N 張共一個 `batch_id`，結果頁按批列、可打包下載。

[**定案**：數量欄＋批號。]{.rec}
:::

::: {.callout .pending}
**D8 Agent 端驗章用哪個引擎（P2 的事，先問方向）**

兩條路：①用 Agent 既有的 `common/integrity/verify.py`（公鑰已編譯進去、`verify_payload` 已能按型別驗），加一個常數與一支「核對機器碼＋算狀態」的小模組；②裝 `jedi-license-runtime`（主系統在用的那支）。②帶 PG 表、租戶解析、Flask API 一整套，Agent 是單租戶 SQLite 用不上，裝了還要解相依（它釘 jedi-common 1.3.2 以上、Flask 外掛一堆）。

[**建議**：①。狀態機邏輯（提醒／寬限）很小，兩邊各一份可接受；若之後要共用，抽成不帶 DB 的純函式小套件再說。]{.rec}
:::

---

## 5. 接入點：reuse／extend／new {#reuse nav="接入點"}

| 要做的事 | 判定 | 動哪裡 |
|---|---|---|
| 簽章、armor、驗章 | reuse | `core/crypto/engine.py` 加常數 `PAYLOAD_TYPE_AGENT_LICENSE` |
| 序號產生／效期／逾期判斷 | **extend（抽共用）** | 從 `modules/licensing/service.py` 抽到 `core/activation_code.py`，licensing 改 import 它（行為不變，既有測試要綠） |
| 私鑰解鎖（env 優先、表單備援） | **extend（抽共用）** | `web/views/issuance.py` 的 `_resolve_passphrase`／`_unlock_or_flash_redirect` 搬到 `web/unlock_helper.py` |
| 開通失敗鎖定 | reuse | `modules/auth/service.py` 既有三支 |
| 到期狀態推算 | extend | `core/status.py` 加 `compute_agent_status()`，不改 `compute_status` |
| Agent 照 dataclass | new | `core/schema.py` 加 `AgentLicense`（與 `License` 並列，不繼承——欄位差太多） |
| 資料表／repo／service | new | `modules/agent_licensing/{models,repository,service}.py`，形狀照 licensing |
| 後台五頁 | new | `web/views/agent_licenses.py` ＋ 五支模板；導覽列 `base.html` 加一項 |
| 機器 API 三支 | new | `web/views/agent_activation_api.py`、`web/views/internal_api.py` **不動**（已 492 行，新端點另開 `internal_agent_api.py`）；`web/api_schemas.py` 加 schema；`csrf.exempt_machine_apis()` 登記 |
| CLI 五支 | new | `cli/agent_commands.py`，`cli/main.py` 一行掛 |
| 守衛測試 | extend | `tests/test_architecture_boundaries.py` 自動涵蓋新模組；`tests/test_api_contract.py` 加 Agent 情境表；`tests/test_csrf.py` 加「請求真的走進 view」 |
| 文件站 | extend | `docs/handbook/data-model.md`（新表）、`license-format.md`（Agent 照格式）、`operations.md`（查案食譜）、`api-contract-baseline.md`（新端點） |

---

## 6. 分棒草案（P1，license_center repo） {#cards nav="分棒"}

| 批 | 卡 | 做什麼 | 改哪些既有檔 | 新開檔 | 依賴 |
|---|---|---|---|---|---|
| 1 | L1 模組地基 | 序號與解鎖抽共用、`AgentLicense` dataclass、`payload_type` 常數、新模組三檔、Alembic、`compute_agent_status`、CLI 五支、單元測試；DEV 套 migration | `core/crypto/engine.py`、`core/schema.py`、`core/status.py`、`modules/licensing/service.py`（改 import）、`web/views/issuance.py`（改 import）、`cli/main.py`、`migrations/env.py` | `core/activation_code.py`、`web/unlock_helper.py`、`modules/agent_licensing/*`、`cli/agent_commands.py`、`migrations/versions/<rev>_agent_license_issuance.py` | — |
| 2 | L2 後台五頁 | 清單／簽發／結果／離線開通／詳情＋客戶時間軸一行；log 照規範記簽發與下載 | `web/app.py`（掛 blueprint）、`web/templates/base.html`、`web/views/customer_timeline.py`、`templates/customer_timeline.html` | `web/views/agent_licenses.py`、五支模板 | L1 |
| 2 | L3 機器 API 三支 | 線上開通、內部簽發、內部查詢；schema、csrf exempt、OpenAPI、合約測試 | `web/api_schemas.py`、`web/csrf.py`、`tests/test_api_contract.py`、`tests/test_csrf.py` | `web/views/agent_activation_api.py`、`web/views/internal_agent_api.py` | L1 |
| 3 | L4 文件與收口 | 文件站四頁、`CLAUDE.md` 介面表加三列、DEV 端到端走一次（簽 3 張→離線開通→CLI 驗） | `docs/handbook/*.md`、`docs/api-contract-baseline.md`、`CLAUDE.md` | — | L2、L3 |

L2 與 L3 都要在 `web/app.py` 掛 blueprint——**登記型共用檔**。做法：L1 當 owner，先把兩個空 blueprint 檔建好、在 `app.py` 掛上（空的也能起），L2／L3 只填自己的檔。這樣 L2、L3 同批。

P2（Agent）與 P3（主系統）等 P1 驗完、D8 定了再拆卡；粗估 Agent 三張（驗章與狀態機／主控台授權頁／狀態事件帶授權）、主系統兩張（列表欄位與事件落庫／線上開通代打）、FE 一張。

---

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

- **Agent 功能分級**：照上沒有模組清單，永遠只有有效／無效。
- **Agent 直連 LC**：線上開通一律經主系統中轉（FR-181 Agent 只出站）。
- **一張照多台共享**：每台一張。
- **主系統照格式變動**：`License` dataclass、`license_issuance` 表、四支凍結 API 一字不動。
- **Agent 的唯讀／鎖定四段式**：只有提醒與寬限。
- **時鐘倒轉防護**（主系統照有）：Agent 第一期不做，列入 P2 的「第二期」候選。
- **自助換綁**：同主系統政策，人工受理。
- **Agent 照當更新站身分票**：FR-167 的 `update_ticket` 是主系統照開通時附簽的；Agent 更新鏈（FR-172）目前不靠票，這期不碰。
