本卡屬 FR-182(母卡 FR-182 母案(同清單上一張)),卡 L3,批 2。只改 license_center repo。依賴:L1。
後台頁是給員工用的,這張卡做給機器打的三支 API:①線上開通 POST /api/activation/agent/activate——以後主系統會代 Agent 來打(Agent 永遠不直連 LC),帶序號+機器碼,LC 簽出綁機的照回去;沒有 token,靠序號本身+既有的來源 IP 失敗鎖定(跟主系統的線上開通共用同一張鎖定表,同一個 IP 不管猜主系統序號還是 Agent 序號合計算)。②內部簽發 POST /api/internal/agent/issue——主系統 root 後台以後可以帶 API token 直接請 LC 簽 Agent 照(跟既有 /api/internal/issue 同一道 token 白名單)。③內部查詢 GET /api/internal/agent/licenses?customer_code=——主系統查某客戶有幾張 Agent 照、各自狀態。既有四支機器 API 的合約是凍結的(主系統已對接,守衛測試擋著),本卡全部新路徑、一字不碰既有四支;token 驗證那段從 internal_api.py(已 492 行超標)抽到共用檔,兩邊用同一支。回應形狀、錯誤字串、驗證順序比照既有,寫進合約基線文件與合約測試。
new:填 L1 建的空殼——web/views/agent_activation_api.py POST /api/activation/agent/activate(無 token;body activation_code+machine_fingerprint strip 後必填;鎖定走 modules/auth/service.check_activation_lockout/record_activation_failure 同一張 activation_attempts 表、同 _lockout_key;驗證順序比照既有:格式 → 鎖定 → 查無 → 已使用 → 序號逾期 → 授權過期 → passphrase;成功 {"license": {…}, "license_id": "alic-…"},失敗 {"error": "…"} 字串比照既有六句);web/views/internal_agent_api.py POST /api/internal/agent/issue(X-API-Token,body 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=(回現行照清單含狀態、到期、機器碼前 12 碼、開通狀態);token 驗證與 _authenticate/_require_valid_token 從 internal_api.py 抽到 web/internal_auth.py 兩邊共用(internal_api 已 492 行,不往裡加)。extend:web/api_schemas.py 加三個 schema(只驗格式)、@bp.doc 進 OpenAPI;tests/test_api_contract.py 加 Agent 三支的情境表(成功/401/400 缺欄/404 查無/409 已用/400 序號逾期/429 鎖定/503);tests/test_csrf.py 加「請求真的走進 view」斷言;docs/api-contract-baseline.md 加三節
repo:~/Projects/Billows/Audit-Manager/license_center
改既有:src/license_center/web/views/agent_activation_api.py(填空殼)
改既有:src/license_center/web/views/internal_agent_api.py(填空殼)
改既有:src/license_center/web/views/internal_api.py(抽 token 驗證到 internal_auth.py,改 import)
改既有:src/license_center/web/api_schemas.py
改既有:tests/test_api_contract.py
改既有:tests/test_csrf.py
改既有:docs/api-contract-baseline.md
新開:src/license_center/web/internal_auth.py
新開:tests/test_agent_activation_api.py
web/views/activation.py 整支(鎖定、驗證順序、錯誤字串、_on_validation_error)、web/views/internal_api.py 的 _authenticate/_require_valid_token/_unlock_or_503/_validation_error/issue、web/api_schemas.py、tests/test_api_contract.py(情境表怎麼寫)、docs/api-contract-baseline.md、LC CLAUDE.md「API schema 屬入口層」「四支端點合約不可變」。web/internal_auth.py(新):從 internal_api.py 搬 _authenticate/_require_valid_token(改 public 名)與 token 驗證用到的 repo 取法;internal_api.py 改 import;_unlock_or_503、_validation_error 若兩邊都要用也一起搬(放 web/api_helpers.py)。既有 tests/test_api_contract.py 四支情境零變動綠。web/api_schemas.py:AgentActivateRequestSchema(activation_code、machine_fingerprint 字串必填,strip)、AgentIssueRequestSchema(customer_code/order_no/issued_to 必填;parent_license_id/license_type/days/count/notify_days_before/grace_days 選填;只驗型別,count 1~50 在 service)、AgentLicensesQuerySchema(customer_code 必填);回應 schema 三個。web/views/agent_activation_api.py(填空殼):POST /activate——before_request 無;流程照 activation.activate:schema → _lockout_key(remote_addr) → check_activation_lockout → get_by_activation_code 查無 404「找不到此開通序號」→ 已開通 409「此開通序號已被使用」→ 序號逾期 400「此開通序號已逾期,請洽原廠重新申請」→ 照過期 400「此開通序號對應的授權已過期」→ passphrase 503 兩句 → activate_agent_license → 200 {"license": signed, "license_id": id}。四種失敗都 record_activation_failure。_on_validation_error 回 400「activation_code 與 machine_fingerprint 皆為必填」。log 照 activation.py(序號摘要、指紋 fingerprint_digest)。web/views/internal_agent_api.py(填空殼):before_request 走 internal_auth.require_valid_token(401 先於 body);POST /issue → issue_agent_licenses → 200 {"batch_id", "licenses": [{"license_id", "activation_code", "license"}]};count 超過 50 → 400「count 須為 1~50」;GET /licenses?customer_code= → list_current 每筆 {license_id, parent_license_id, type, starts_at, expires_at, status(compute_agent_status), machine_fingerprint_prefix(前 12 碼或 null), activated: bool}。issuer 寫 token:<token_name>(照既有)。web/csrf.py:L1 已把兩個 bp 加進 exempt_machine_apis,本卡只在 tests/test_csrf.py 加「POST /api/activation/agent/activate 不帶 csrf 走進 view(回 400 參數錯而非 CSRF 400)」與 POST /api/internal/agent/issue 同款斷言(照既有兩條的寫法——要驗「真的進 view」不是「回應沒 CSRF 字樣」)。tests/test_api_contract.py:加三節情境表(成功/401 兩種/400 缺欄/404/409/400 逾期/400 照過期/429 連打 6 次/503),斷言 status+body 逐 byte。tests/test_agent_activation_api.py:鎖定共用——同 IP 先打主系統序號錯 3 次、再打 Agent 序號錯 2 次、第 6 次兩邊都 429(這是刻意的,寫進合約文件)。docs/api-contract-baseline.md:加三節(Request 表、成功、失敗表、驗證順序),檔頭「四支」改「七支」並註明 Agent 三支自 2026-11 起凍結。@bp.doc 寫用途/呼叫端/失敗情境,/api/docs 看得到。curl(DEV token 從 LC 後台「API Token 管理」建一把 dev 用,明文不進回寫)。pytest tests/test_api_contract.py 既有四支情境零變動、新三支全綠;tests/test_csrf.py 新斷言:POST 不帶 csrf token 仍走進 view(回 400 是參數錯不是 CSRF);手打:curl -XPOST :5062/api/activation/agent/activate -d '{"activation_code":"<L1 簽的>","machine_fingerprint":"<64 hex>"}' 回 200 帶信封、agent-verify 過;同序號再打 409;錯序號連打 6 次第 6 次 429,且主系統那支 /api/activation/activate 同 IP 也被鎖(共用表,預期行為,寫進合約文件);curl -H 'X-API-Token: <dev token>' -XPOST /api/internal/agent/issue -d '{…,"count":2}' 回 2 張;無 token 401 先於 body 錯;/api/docs 看得到三支