Agent 比照主系統拿照:License Center 簽發、給序號、客戶抄 Agent 機器碼來開通、拿到綁機的照上傳。Agent 不分功能,只分有照/沒照;客戶封閉網路靠離線開通。第一期只做 License Center 端。
主系統每個租戶要有一張簽過章的授權照才能用(FR-062),Agent 完全沒有:拿到註冊 token 就能裝、能跑、裝幾台都行。決策者 2026-11-07 定調 Agent 要用跟主系統一樣的模式拿照,做完後的狀態:
| 現況 | 目標 | |
|---|---|---|
| License Center | 只認主系統的照 | 多一種「Agent 照」:新模組、新表、後台五頁、三支機器 API、五支 CLI |
| 一台 Agent | 無授權概念 | 一張照、綁那台的機器碼、有到期日、沒有模組清單 |
| 客戶怎麼拿 | — | 員工在 LC 簽發拿序號 → 客戶在 Agent 主控台看機器碼 → 序號+機器碼交回 → 員工在 LC 離線開通、寄回照 → 客戶上傳 |
| 沒照的 Agent | — | 主控台可登入、看機器碼、上傳照;不註冊、不心跳、不收指令、不掃描 |
| 主系統 | — | Agent 列表多一欄授權到期(P3) |
分三期:P1 License Center(本版開卡)、P2 Agent 端、P3 主系統端。P2/P3 等 FR-181 與 FR-179 的 Agent 卡合回再拆(§6)。
| 階段 | 做什麼 | 產出 | 完成怎麼判定(決策者檢查法) |
|---|---|---|---|
| P1 License Center | 新模組 agent_licensing:表、簽發/開通/換綁/補發/展延、後台五頁、三支機器 API、五支 CLI、文件 |
license_center 一版 | 開 LC 後台 → 「Agent 授權」→ 簽一張(數量 3)→ 3 組序號 → 拿一組序號+一串 64 碼機器碼走「離線開通」→ 下載 .license → CLI agent-verify 過。對照雛形 prototype/lc-agent-license.html 七個場景 |
| P2 Agent 端 | 驗 Agent 照、主控台「授權」頁、沒照不起 sync 模組 | evidence-agent 一版 | 123 的 Agent 刪掉照 → 主控台「尚未開通」+機器碼、主系統列表離線;上傳 P1 的照 → 「已授權,到期 …」、主系統恢復上線。對照 prototype/agent-console-license.html 七個場景 |
| P3 主系統端 | Agent 列表授權欄;線上開通代打 LC | BE+FE 一版 | 主系統「遠端 Agent」頁每台看得到到期日;Agent 主控台輸入序號按線上開通 10 秒內變已授權 |
limits.max_agents——Agent 本機沒東西可驗、離線開通做不出來、加買要重簽主系統的照。照上 customer_code 必填、parent_license_id 選填,LC 看得出「這客戶買了主系統+N 台 Agent」。agent_licensing、新表 agent_license_issuance,不動 license_issuance。 既有表 tenant_id/plan_code/modules 必填,塞進去要全改可空、總覽與狀態推算到處加「如果是 Agent」分支。序號邏輯與私鑰解鎖抽到共用層(core/activation_code.py、web/unlock_helper.py),不從 licensing 模組 import(鐵律③)。payload_type: "agent_license",沒有模組與方案,到期政策兩段式。 欄位見 §4.2。payload_type 缺鍵=主系統照,是向後相容的支點;Agent 驗章指定型別,拿主系統的照來上傳會被拒。device_uuid(sha256(product_uuid|machine-id)),不另算。VM 模板複製兩台同碼的風險由主系統註冊撞號偵測擋,LC 不處理。batch_id。 不做一張照 N 座位的共享池。common/integrity/verify.py,不裝 jedi-license-runtime。 後者帶 PG 表、租戶解析、Flask API,Agent 是單租戶 SQLite 用不上。到期狀態機很小,兩邊各一份。| 元件 | 現況 | 本案動作 |
|---|---|---|
LC core/crypto/engine.py sign_payload/verify_payload/armor |
按 payload_type 分型,已簽過 manifest/unlock/update_ticket |
reuse,加常數 PAYLOAD_TYPE_AGENT_LICENSE |
LC modules/licensing/service.py 序號三支 |
與照無關的純序號邏輯 | extend:抽到 core/activation_code.py |
LC web/views/issuance.py 解鎖兩支 |
只被 issuance 用 | extend:搬到 web/unlock_helper.py |
LC modules/auth/service.py 開通鎖定 |
來源 IP、15 分鐘 5 次、全站預算,表 activation_attempts |
reuse,Agent 線上開通共用同表(同 IP 連錯主系統與 Agent 序號合計,寫進合約文件) |
LC core/status.py compute_status |
四段式 | extend:加 compute_agent_status 兩段式 |
LC web/views/internal_api.py token 驗證 |
492 行、超標 | extend:抽到 web/internal_auth.py 共用,不往裡加 |
| LC 四支凍結 API | 合約守衛 | 不動;Agent 三支全部新路徑 |
Agent platform/fingerprint.py device_uuid |
註冊主系統用 | reuse(P2) |
Agent common/integrity/verify.py+PUBLIC_KEYS |
驗產物清單 | reuse,加 expected_type(P2) |
Agent sync/__init__.py setup_agent_auth → enroll_for_auth |
起註冊/心跳/各背景元件 | extend:前面加授權閘(P2) |
Agent platform/registry.py MODULES |
模組申報 status_fields/blueprint |
extend:登記新模組 license(P2,排 FR-181 F-2 之後) |
主系統 activate_license_online() |
主系統代打 LC 開通 | reuse 做法(P3) |
%%{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 主系統
E->>L: 「Agent 授權」→ 簽發:客戶、訂單、數量 3、天數 365
L-->>E: 3 張照(未綁機)+ 3 組序號
E->>C: 交付序號
rect rgb(238, 242, 243)
Note over C,A: 離線開通(P1 LC 端+P2 Agent 端)
C->>A: 登入主控台 → 「授權」頁顯示「尚未開通」+機器碼
C->>E: 序號 + 機器碼
E->>L: 「Agent 離線開通」:序號+機器碼
L-->>E: 綁機的照(.license)
E->>C: 回寄照檔
C->>A: 上傳照
A->>A: 驗章+核對機器碼+到期 → 已授權 → 起 sync 模組
A->>M: 註冊/心跳/狀態事件帶授權
end
rect rgb(226, 240, 241)
Note over C,L: 線上開通(P3,認證用註冊 token)
C->>A: 「授權」頁輸入序號
A->>M: 序號+機器碼+註冊 token(出站)
M->>L: 代打 /api/activation/agent/activate
L-->>M: 綁機的照
M-->>A: 照
A->>A: 驗章 → 已授權
end
| 欄位 | 值 |
|---|---|
payload_type |
"agent_license"(主系統照無此鍵) |
license_id |
alic- + 12 碼 |
customer_code/order_no/issued_to |
同主系統 |
parent_license_id |
主系統照號,可空;只對照用,Agent 不驗 |
type |
formal/trial |
issuer/issued_at/starts_at/expires_at |
同主系統(ISO 8601) |
expiry_policy |
{"notify_days_before": 30, "grace_days": 14} |
machine_fingerprint |
Agent device_uuid,開通前 null |
product_major |
1 |
kid |
簽章鑰指紋 |
信封與主系統照相同(format_version 2、Ed25519、armor 後 .license)。core/schema.py 加 AgentLicense dataclass,與 License 並列不繼承。
agent_license_issuance(append-only 事件帳)| 欄位 | 型別 | 說明 |
|---|---|---|
id |
serial PK | |
agent_license_id |
varchar(64) unique | alic-… |
batch_id |
varchar(32) | 同一次簽發 N 張共用;索引 |
customer_code |
varchar(64) | 索引 |
customer_name |
varchar(256) | |
order_no |
varchar(64) | |
parent_license_id |
varchar(64) null | 字串、不設 FK(跨模組不碰對方的表) |
license_type |
varchar(16) | formal/trial |
product_major |
int default 1 | |
starts_at/expires_at |
timestamptz | |
expiry_policy |
jsonb | 兩個數字 |
activation_code |
varchar(64) null | token_urlsafe(16) |
activation_code_expires_at/activation_code_revoked_at |
timestamptz null | 30 天效期;補發作廢舊序號 |
machine_fingerprint |
varchar(128) null | |
activated_at |
timestamptz null | |
rebind_history |
jsonb default [] |
|
issuer |
varchar(128) | |
event_type |
varchar(16) | new/activate/rebind/reissue/extend |
license_payload |
jsonb | 完整簽章信封 |
created_at |
timestamptz default now() |
Alembic revision 一支;migrations/env.py 加 import。
modules/agent_licensing/service.py(六支,依賴由入口傳入、不 commit)| 函式 | 做什麼 |
|---|---|
issue_agent_licenses(repo, private_key, *, customer_code, order_no, issued_to, parent_license_id, license_type, days, count, notify_days_before, grace_days, issuer) |
產 batch_id,迴圈 N 次各簽一張未綁機照+序號,回 list[AgentIssueResult] |
activate_agent_license(repo, private_key, *, activation_code, machine_fingerprint, issuer) |
查序號(未作廢)→ 已用 409 → 逾期 → 照過期 → 補指紋重簽新照、event_type=activate、mark_activated |
rebind_agent_license(repo, private_key, *, agent_license_id, new_machine_fingerprint, issuer) |
同主系統 rebind_license |
reissue_agent_license(repo, *, agent_license_id) |
重出同一張;未開通的補發產新序號、舊序號作廢 |
extend_agent_license(repo, private_key, *, agent_license_id, days, issuer) |
起算基準 max(原到期, now),同主系統 |
verify_agent_license(signed, public_keys) -> AgentLicense |
verify_payload(expected_type=agent_license) 後組 dataclass |
例外類別比照 licensing(AgentLicenseNotFoundError、ActivationCodeExpiredError 共用 core/activation_code.py 的)。
web/views/agent_licenses.py,雛形 prototype/lc-agent-license.html)| 頁 | 路徑 | 雛形場景 |
|---|---|---|
| 清單 | GET /agent-licenses/ |
1:搜尋客戶代碼/名稱/訂單,篩狀態/開通/型態,只列現行照;列尾下拉詳情/下載/展延/補發/換綁;待開通列顯示序號剩幾天逾期 |
| 簽發 | GET/POST /agent-licenses/issue |
2:客戶代碼、名稱、訂單、上層主系統照下拉(選填,呼叫 licensing service 查該客戶現行照)、型態、天數、數量、提醒天數、寬限天數、發照人 |
| 簽發結果 | GET /agent-licenses/batch/<batch_id> |
3:N 列照號+序號+下載未綁機照;zip 全部下載;提示序號 30 天內開通 |
| 離線開通 | GET/POST /agent-licenses/activate、GET …/activate/<id>/result |
4、5:序號+機器碼+發照人 → 結果頁明文+下載 |
| 詳情 | GET /agent-licenses/<id>、…/download、…/extend、…/reissue、…/rebind |
6:明文、事件歷史、操作下拉;非現行照標「已被取代」不給下載 |
| 客戶時間軸 | 既有頁加一張卡 | 7:「Agent 照 N 台(已開通 x/待開通 y)→ 看清單」;事件混進時間軸前綴「Agent」 |
導覽列加「Agent 授權」。log:簽發/開通/換綁/補發/展延/下載六種 INFO 帶 agent_license_id、customer_code、actor;序號只記 activation_code_digest。
| 端點 | 認證 | Request | 成功 |
|---|---|---|---|
POST /api/activation/agent/activate |
無 token;序號+機器碼+IP 鎖定(共用 activation_attempts) |
activation_code、machine_fingerprint(strip 後必填) |
{"license": {…}, "license_id": "alic-…"} |
POST /api/internal/agent/issue |
X-API-Token |
customer_code、order_no、issued_to 必填;parent_license_id、license_type、days、count(預設 1、上限 50)、notify_days_before、grace_days 選填 |
{"batch_id", "licenses": [{"license_id", "activation_code", "license"}]} |
GET /api/internal/agent/licenses?customer_code= |
X-API-Token |
{"licenses": [{license_id, parent_license_id, type, expires_at, status, machine_fingerprint_prefix, activated}]} |
失敗 {"error": "…"};驗證順序與錯誤字串比照既有 /api/activation/activate。schema 進 web/api_schemas.py、@bp.doc 進 OpenAPI、tests/test_api_contract.py 加情境表、docs/api-contract-baseline.md 加三節。
agent-issue --count N、agent-activate <序號> --fingerprint <64 hex>、agent-verify <檔>、agent-extend、agent-rebind,放 cli/agent_commands.py,cli/main.py 一行掛。
AGENT_DATA_DIR/license/agent.license。guidant_agent/license/(domain:狀態機;app:載入/驗證/上傳;api:主控台「授權」分頁三支端點——看狀態+機器碼、上傳照、(P3)線上開通),登記進 registry.MODULES,申報 status_fields(license: {status, expires_at, license_id})。setup_agent_auth 內 enroll_for_auth 之前,狀態不是正常/提醒/寬限就不起 sync 的任何背景元件;上傳成功後同行程起。/agents/register 同一道門:註冊 token 認證(沒照的 Agent 還沒有通行證)。agents 表加 license_status、license_expires_at,從 agent_status 事件落;遠端 Agent 列表加一欄(雛形場景 7)。POST /api/1.0/agents/license/activate(註冊 token 認證)代打 LC /api/activation/agent/activate,照原樣回 Agent,主系統不落照。lc-issue(第二期候選)。feature/FR-172)拆卡表由 handoff/cards.py 產,兩個斷言(依賴在更早的批、同批不改同一檔)已過。
| 批 | 卡 | 做什麼 | reuse/extend/new | 改哪些既有檔 | 依賴 |
|---|---|---|---|---|---|
| 1 | L1 | 模組地基:表、六支 service、序號與解鎖抽共用、AgentLicense、compute_agent_status、CLI 五支、空 blueprint 先掛(登記型 app.py/csrf.py owner) |
extend 四處、new 模組 | core/crypto/engine.py、core/schema.py、core/status.py、modules/licensing/service.py、web/views/issuance.py、web/views/activation.py、cli/main.py、web/app.py、web/csrf.py、migrations/env.py |
— |
| 2 | L2 | 後台五頁+客戶時間軸一張卡 | new 五頁、extend 時間軸 | web/views/agent_licenses.py(填空殼)、templates/base.html、web/views/customer_timeline.py+模板 |
L1 |
| 2 | L3 | 機器 API 三支、token 驗證抽 internal_auth.py、合約測試 |
new 三支、extend 抽共用 | web/views/agent_activation_api.py/internal_agent_api.py(填空殼)、web/views/internal_api.py、web/api_schemas.py、tests/test_api_contract.py、tests/test_csrf.py、docs/api-contract-baseline.md |
L1 |
| 3 | L4 | 文件站四頁、CLAUDE.md、DEV 端到端證據 | extend | docs/handbook/*.md、CLAUDE.md |
L2、L3 |
P2 第一批(Stage 4,已開卡 CM-3621~3623):拆卡表 handoff/cards_p2.py,檔頭有與 FR-181/FR-179 的撞檔核對——P2-1 Agent 驗照+閘門+模組登記(registry.py owner,F-2 已合回)、P2-2 主控台授權 API、P2-3 主控台授權頁 FE,三張同批零撞檔。P2 後補:未開通時主控台放行、首次設定頁插「上傳照」一步——等 FR-179 A1/A2 合回。P3(已開卡 CM-3624~3630,拆卡表 handoff/cards_p3.py):批 5 P3-1 契約兩鍵+remote_agents 兩欄+套件 migration 020(只改檔不發版)、P3-2 主系統落庫+線上開通代打 LC(註冊 token get_enabled_by_hash 只驗不消耗、主系統不驗章不落照、開發期 path dependency 不 commit);批 6 P3-3 Agent 送狀態+線上開通、P3-3b 主控台畫面;批 7 P3-4a 兩套件發版、P3-4b 主系統列表授權欄+error-code;批 8 P3-4 釘版+SPEC+手冊。FR-181 已於 11-07 全合回,批 7/8 前置解除;P3-4a 發 0.5.2/1.17.2(181 已發 0.5.1/1.17.1)。
alic-….license → agent-verify 印 payload_type=agent_license、machine_fingerprint 相符、狀態 valid。.license 餵 agent-verify → 拒(型別不符)。curl POST /api/activation/agent/activate 序號② → 200 帶信封;再打 → 409;錯序號連打 6 次 → 429。curl POST /api/internal/agent/issue count=2 → 2 張;無 token → 401 先於 body 錯。pytest 全綠(含既有合約測試零變動);alembic check 無漂移;mkdocs build --strict 零警告。Agent 功能分級;Agent 直連 LC;一張照多台;主系統照格式與四支凍結 API 變動;Agent 唯讀/鎖定四段式;時鐘倒轉防護(P2 第二期候選);自助換綁;Agent 照當更新站身分票。