本卡屬 FR-182(母卡 CM-3609 https://app.notion.com/p/FR-182-Agent-Agent-P1-License-Center-4-3-410346da4cd0816ea5e2c4757ebc4fbb),P2 Agent 端,卡 P2-2,Stage 4(P2 第一批,接在 P1 的批 1~3 之後;P1 LC 端 CM-3610~3613 已 Done 並上 189)。只改 evidence-agent。依賴:無(同批 P2-1/P2-2/P2-3 平行;P2-2 的 gate 介面以 P2-1 為準,合回後對一次)。
P2-1 讓 Agent 認得照、沒照就不起 sync;這張卡給主控台兩支 API,畫面(P2-3)靠它們:①看授權狀態+本機機器碼(任何登入者都能看——客戶要抄機器碼給原廠);②上傳照(只有「設定」權限能傳)。上傳走 P2-1 的 install:先驗新檔、通過才覆蓋,驗不過原檔不動、回明確原因(型別不對/簽章不對/綁的是別台/已過期/檔壞了),畫面照原因顯示一句人話。順便在總覽 API 多帶授權狀態與到期日兩個值,總覽頁頂端要顯示「N 天後到期」提醒。全部照 keys_routes.py 的形狀(薄殼、require_session、body 上限、稽核)。
new:guidant_agent/console/api/license_routes.py(license_bp,url_prefix="/api/license")——GET "" @require_session(None)(任何登入者)回 {status, reason, license_id, customer_code, issued_to, type, starts_at, expires_at, days_to_expiry, grace_days_left, machine_fingerprint(本機 device_uuid 完整 64 碼), bound_fingerprint_prefix, expiry_policy};POST /upload @require_session("configure"),body_limit.read_limited(64 KB)、multipart 或 raw body 都收 → license.app.install.install(bytes) → 成功回新狀態 200、失敗 400 帶 reason(wrong_type/bad_signature/mismatch/expired/malformed);兩支都 page_viewed/audit(事件碼用既有 Code 裡最接近的,不加新事件碼——event_code.py 是登記型檔,P3 統一加)。extend:console/api/__init__.py 檔尾加一行 from ...license_routes import license_bp 與 SUB_BLUEPRINTS 加一項(登記型,本卡 owner);console/api/pages.py _SPA_PAGES 加 "license";console/app/overview.py 回應加 license: {status, expires_at} 兩鍵(總覽頁頂端要顯示提醒)。禁:不碰 console/api/__init__.py 的 create_session/require_session(FR-179 A1 的範圍)。
repo:~/Projects/Billows/Audit-Manager/evidence-agent
改既有:guidant_agent/console/api/__init__.py(檔尾 import 一行+SUB_BLUEPRINTS 一項)
改既有:guidant_agent/console/api/pages.py(_SPA_PAGES 加 license)
改既有:guidant_agent/console/app/overview.py(加 license 兩鍵)
新開:guidant_agent/console/api/license_routes.py
新開:test/test_license_routes.py
guidant_agent/console/api/keys_routes.py(範本:blueprint、require_session、page_viewed、稽核);console/api/__init__.py 檔尾登記段與 require_session 的 scope 語意;console/app/body_limit.py(read_limited/json_body);console/api/pages.py _SPA_PAGES;console/app/overview.py;.claude/rules/security/uploads-and-output.md、endpoints.md。P2-1 的 license/app/gate.py 介面(同批平行——先照本卡寫的介面名寫,P2-1 合回後對一次,對不上以 P2-1 為準改本卡)。console/api/license_routes.py:license_bp = Blueprint("console_license", __name__, url_prefix="/api/license");GET "" @require_session(None):gate = current_app.extensions["agent_license"]; s = gate.refresh() → 回 {status, reason, license_id, customer_code, issued_to, type, starts_at, expires_at, days_to_expiry, grace_days_left, machine_fingerprint: device_uuid, bound_fingerprint_prefix: (s.bound_fingerprint or "")[:12] or None, expiry_policy}(逐欄明列,不 vars() 整包吐)。POST /upload @require_session("configure"):body_limit.read_limited(64 * 1024);request.files.get("file") 有就讀它、否則 request.get_data();gate.install(data) → 200 同 GET 形狀;LicenseRejected → 400 {"error_code": "AGENT_LICENSE_REJECTED", "reason": state.reason, "bound_fingerprint_prefix": …}(錯誤碼加在 common/code/agent_console_error_code.py 既有 enum,不是 event_code.py);稽核 audit(logger, "AGENT_CONSOLE_LICENSE_UPLOAD", Code.AGENT_CONSOLE_SETTINGS_CHANGED…)——事件碼挑既有最接近的(grep -n "CONSOLE" guidant_agent/platform/event_code.py 選),message 不帶照內容。console/api/__init__.py 檔尾:from guidant_agent.console.api.license_routes import license_bp # noqa: E402 一行、SUB_BLUEPRINTS tuple 加 license_bp。其他一行不動(create_session/require_session 是 FR-179 A1 的)。console/api/pages.py:_SPA_PAGES 加 "license"。console/app/overview.py:回應 dict 加 "license": {"status": s.status, "expires_at": s.expires_at, "days_to_expiry": s.days_to_expiry, "grace_days_left": s.grace_days_left}(gate 沒掛時回 None,別炸)。test/test_license_routes.py:用既有主控台測試的 app fixture 與假 session(看 test/test_fr166_guards.py 或 keys 的測試怎麼拿 cookie);gate 用 fake(allows_sync/install 可控);情境見驗收。AGENT_DATA_DIR=./data python main_app.py,本機管理員登入拿 cookie(scripts/install/lib/local_admin.sh 建帳號的方式或既有測試帳號),curl -k --http1.1 -b cookie https://127.0.0.1:8444/console/api/license;curl -F [email protected] …/upload。pytest:未登入 401;files scope 的票 GET 200、POST 403;上傳主系統照 400 wrong_type;上傳壞簽章 400 bad_signature;上傳別台的照 400 mismatch 且回 bound_fingerprint_prefix;上傳對的照 200 且 GET 變 valid;65 KB body 413;_SPA_PAGES 含 license(GET /console/license 回 SPA 殼 200);守衛三支綠。本機 curl -k --http1.1 走一遍(本機管理員登入拿 cookie)
~/Projects/Billows/Audit-Manager/compliance-manager-be/docs/features/FR-182-2611-agent-license/design.md(D6 不擋啟動但不起 sync、D8 用既有 integrity 驗章、§4.2 照 payload、§4.8 Agent 端);討論稿 ~/Projects/Billows/Audit-Manager/compliance-manager-be/docs/features/FR-182-2611-agent-license/discussion.md §2.2 Agent 端盤點、§2.4 與 FR-181/FR-179 接點、§3.7 狀態表~/Projects/Billows/Audit-Manager/compliance-manager-be/docs/features/FR-182-2611-agent-license/prototype/agent-console-license.html(七場景;P2-3 對照刻,P2-1/P2-2 看資料要長什麼樣)