FR-182 · 需求討論稿 v1 · 2026-11-07
一台 Agent 一張照、綁機器碼、有到期日;沒有功能分級;客戶封閉網路靠離線開通;第一期只做 License Center 端
主系統從 FR-062 起每個租戶都要有一張簽過章的授權照才能用,但 Agent 完全沒有授權檢查——拿到註冊 token 就能裝、能跑、裝幾台都行。決策者 11-07 定調:Agent 要用跟主系統一樣的模式拿照——License Center 簽發、給序號、客戶抄機器碼來開通、拿到綁機的照上傳;Agent 不分功能等級,只分「有照/沒照」;客戶在封閉網路,所以離線開通一定要成立。本稿先把 License Center 端要長什麼樣講清楚,Agent 端與主系統端只定方向。
整案一句話: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 看一次雛形後正式拍板。
| 主系統(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 端則是零——它只認得主系統的照。
最省事的做法是主系統照的 limits 加一個 max_agents: 5,主系統註冊第 6 台時拒絕。排除理由:
所以是獨立一張照,照上記得它屬於哪個客戶、掛在哪張主系統的照底下(可空),LC 總覽看得出「這個客戶買了主系統+3 台 Agent」,到期日各自算。
| 既有能力 | 位置 | 本案怎麼用 |
|---|---|---|
泛用簽章 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 全部是新路徑 |
| 既有能力 | 位置 | 本案怎麼用 |
|---|---|---|
機器碼 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 一節(狀態、到期日、照號) |
| 既有能力 | 位置 | 本案怎麼用 |
|---|---|---|
主系統向 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:加「授權到期」欄 |
決策者 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。
%%{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
跟主系統的照同一個信封(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_idplanmoduleslimits |
不要 | Agent 不分功能、不綁租戶 |
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 |
導覽列加一項「Agent 授權」,底下五個頁,全部照既有 Tabler 樣式:
| 頁 | 路徑 | 做什麼 |
|---|---|---|
| 清單 | /agent-licenses/ |
每台一列:客戶、照號、上層主系統照、型態、到期日、推算狀態、機器碼前 12 碼、開通狀態;搜尋客戶代碼/名稱/訂單;篩狀態;列尾下拉:詳情/下載/展延/補發/換綁 |
| 簽發 | /agent-licenses/issue |
客戶代碼、客戶名稱、訂單編號、上層主系統照(下拉,選填,列該客戶現行照)、型態、天數、數量、提醒天數、寬限天數、發照人 |
| 簽發結果 | /agent-licenses/batch/<batch_id> |
這批 N 張:每列照號+序號+下載;「全部下載(zip)」;提醒:序號 30 天內要開通 |
| 離線開通 | /agent-licenses/activate |
序號、客戶機器碼、發照人 → 簽出綁機的照 → 結果頁下載 |
| 詳情 | /agent-licenses/<id> |
明文欄位、事件歷史(簽發/開通/換綁…)、操作:下載(只有現行照)、展延、補發、換綁(換綁人工受理,同主系統政策) |
客戶總覽(主系統照那頁)不動,只在客戶時間軸頁加一行「Agent 照 N 台 → 看清單」連過去。
| 端點 | 呼叫端 | 認證 | 第一期 |
|---|---|---|---|
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 新情境表。
agent-issue(同表單欄位,--count N)、agent-activate(序號+機器碼)、agent-verify <檔>、agent-extend、agent-rebind。保底用,跟既有 issue/activate 一樣的形狀。
照存哪: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。
agent_status 事件多帶 license: {status, expires_at, license_id};主系統落到 agents 表兩欄(license_status、license_expires_at),遠端 Agent 列表多一欄。lc-issue)。決策者 11-07 已口頭同意下列建議;寫成條列是為了看雛形時再核一次,看完沒意見就當定案。
D1 Agent 照與主系統照的關係:獨立一張、記上層照號
獨立一張照,customer_code 必填、parent_license_id 選填。排除「主系統照加 max_agents」(§1.2)。
定案:獨立照。
D2 LC 資料怎麼存:新模組、新表
modules/agent_licensing/ 自己一張 agent_license_issuance,不改 license_issuance。序號邏輯與私鑰解鎖抽到共用層,不是從 licensing 模組 import。
定案:新模組新表。
D3 照的內容:payload_type: agent_license,沒有模組與方案,到期政策兩段式
§3.2 的欄位表。
定案:照 §3.2。
D4 機器碼:直接用 Agent 的 device_uuid
不另算一套。VM 模板複製會讓兩台機器碼相同、一張照兩台都能用——主系統註冊的撞號偵測已經擋第二台,LC 不處理。
定案:用 device_uuid。
D5 開通路徑:第一期離線;線上開通走 Agent → 主系統 → LC,第二期
LC 第一期順便把線上開通端點做出來(薄殼),主系統之後再接。Agent 永遠不直連 LC。
定案:離線先行,線上端點先做、主系統端第二期。
D6 沒照或過期的 Agent:不擋啟動,但不註冊、不心跳、不掃描
§3.7 的狀態表。寬限期全功能、過了寬限回到「尚未開通」。
定案:照 §3.7。
D7 批量簽發:表單加「數量」,一次產 N 張各自獨立的照與序號
不做「一張照 N 個座位」的共享池——每台一張才能各自換綁、各自到期。N 張共一個 batch_id,結果頁按批列、可打包下載。
定案:數量欄+批號。
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 的純函式小套件再說。
| 要做的事 | 判定 | 動哪裡 |
|---|---|---|
| 簽章、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(新端點) |
| 批 | 卡 | 做什麼 | 改哪些既有檔 | 新開檔 | 依賴 |
|---|---|---|---|---|---|
| 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 一張。
License dataclass、license_issuance 表、四支凍結 API 一字不動。update_ticket 是主系統照開通時附簽的;Agent 更新鏈(FR-172)目前不靠票,這期不碰。