Python API 參考

bootstrap()、open_model()、編譯強檔下自動生效的閘門,以及你的程式碼可能需要處理的例外。

你需要撰寫程式碼對接的執行期 API,其實只有兩個頂層函式——其餘的一切(啟用、授權、閘門本身)都發生在它們背後。沒有裝飾器需要匯入或使用:magiclock build 會自動把授權閘門檢查插入每一個編譯後的模組(見編譯強檔)。

magiclock.bootstrap()

python
magiclock.bootstrap(vault_dir=None, *, app_version="1.0.0") -> MagicLockRuntime

針對這台機器的啟用狀態,為目前的處理程序把授權閘門立起來。在 magiclock build 的產物裡,這個呼叫會在進入點模組自動插入;只有一般(未編譯)指令碼才需要你自己在第一次呼叫 open_model() 之前手動呼叫它。

  • vault_dir——覆寫本地啟用狀態的存放位置,通常不需要用到。
  • app_version——會與你授權設定的 max_app_version 做比對,若你採取版本鎖定的發布策略會用到。
  • 回傳執行期物件(很少需要直接使用——大多數程式碼只是為了它的副作用而呼叫 bootstrap())。
  • 若這台機器尚未啟用,或啟用狀態存在但對這台機器無效,會拋出例外(見下方「你可能需要處理的例外」一節)。

magiclock.open_model()

python
magiclock.open_model(path, *, vault_dir=None, passphrase=None, key=None) -> bytes

解密由 protect-model 產生的 .enc 信封,回傳明文位元組——只存在於記憶體中,永遠不會寫入磁碟。

  • path——.enc 檔案的路徑。
  • passphrase / key——用於密碼片語便攜(--passphrase)、金鑰便攜(--emit-key)或二因子(--bind-machine --passphrase)產物;免金鑰便攜(預設)或一般機器鎖定的產物則不需要傳入。
  • 機器鎖定(--bind-machine)的產物要求必須先呼叫過 bootstrap();便攜信封不需要就能解密。
python
import magiclock

magiclock.bootstrap()
weights = magiclock.open_model("model.onnx.enc")

編譯強檔的閘門

沒有函式或裝飾器需要呼叫——在機器鎖定(--bind-machine)或雲控(--web-gate)建置下,magiclock build 會走訪你的原始碼樹,在編譯前為每一個模組插入一次閘門檢查(核對 python_protect 這項能力);預設的便攜建置則不會編譯進任何閘門——它的保護是原生編譯本身:

python
# 你的原始碼,你自己完全不用改:
def export_report(data: str) -> bytes:
    ...

# `magiclock build` 實際編譯出來的內容,概念上等同於:
current_runtime().require_feature("python_protect")

def export_report(data: str) -> bytes:
    ...

這個檢查每個模組只會執行一次,在處理程序第一次 import 它的時候執行——不是每次呼叫都執行一次。因為它被編織進了每一個編譯後的模組,而不是集中在一處可刪除的共用裝飾器背後,攻擊者必須逐一模組去找、去 patch。非進入點模組的檢查失敗會照常向上拋出例外(它有可能被當作程式庫匯入);進入點模組檢查失敗則會印出一行乾淨的提示後結束,因為少了它其他任何東西都跑不起來。

magiclock.revalidate()

python
magiclock.revalidate() -> None

立即重跑保護檢查。閘門在匯入時每個模組只檢查一次——這對指令碼是合適的開銷,但對長駐行程(伺服器、worker、守護行程)而言,意味著它一直信任著啟動那一刻做出的判斷。按你合適的週期呼叫它,每小時一次是常見做法:

python
import magiclock

def periodic_check():
    magiclock.revalidate()   # 授權或判決不再成立時擲出例外

它會重跑完整的本機授權閘門;並且對本行程中載入的任何雲控(--web-gate)產物,強制向伺服器取一份新的簽章判決。後半句很關鍵:它決定了入口網站裡的一次停用,對一個長駐服務是立刻生效,還是要等到它下一個自然檢查點。如果你把雲控產物放進長駐行程,呼叫與否就是「幾秒內停」與「最終會停」的差別。

它擲出的錯誤型別與匯入時閘門相同;一切仍然成立時回傳 None

magiclock.revalidate_every()

python
magiclock.revalidate_every(seconds, *, on_error=None)

在背景守護執行緒上永久定時執行 revalidate()。在 bootstrap() 附近呼叫一次即可:

python
import magiclock

magiclock.bootstrap()
magiclock.revalidate_every(3600)     # 每小時複查一次

檢查失敗時,預設直接終止行程。 如果背景執行緒把失敗吞掉,你的服務就會帶著一份已經不成立的授權繼續對外提供,而守護執行緒的 traceback 沒有人會看。傳 on_error 可以接管——排空連線、翻轉就緒探針,然後自己退出:

python
magiclock.revalidate_every(3600, on_error=lambda exc: my_graceful_shutdown(exc))

回傳的執行緒帶 stop(),便於優雅停機。如果你的 on_error 自己擲出例外,行程仍會被終止——一個壞掉的處理器不能變成繼續執行的途徑。

你可能需要處理的例外

bootstrap()open_model() 會針對各種狀況拋出例外,從「尚未啟用」到「這個產物已經過期」都有。大多數整合方式是廣泛地攔截例外,再依類別名稱分支處理:

python
try:
    magiclock.bootstrap()
    data = magiclock.open_model("model.onnx.enc")
except Exception as exc:
    name = type(exc).__name__
    if name == "NotActivatedError":
        ...  # this machine hasn't been activated — run `magiclock activate`
    elif name == "TrialExpiredError":
        ...  # the artifact's expiry window has passed
    else:
        raise
例外觸發時機
NotActivatedError這台機器上找不到啟用狀態——執行 magiclock activate(或讓 CLI 在首次加密時自動啟用)。
VaultLockedError本地啟用狀態在這台機器上無法開啟——可能是機器不對,也可能是保管庫檔案被移動或遭竄改。這兩種情況在設計上刻意做成無法區分,見安全模型
TrialExpiredError--trial--expires-in--expires-at 產物已超過它的解密時限。
SubscriptionExpiredError你的方案已經過期。這只會阻擋新的加密——解密路徑永遠不會拋出這個例外。
SubscriptionInvalidError訂閱憑證在需要的地方缺失、格式錯誤,或簽章驗證失敗。
PortableFormatError便攜信封(預設,或 --passphrase/--emit-key)格式不正確,或版本不受支援。
PortableKeyError傳給 open_model()/run --key 的密碼片語或金鑰,與便攜產物不相符。
DebuggerDetectedError在解密邊界偵測到已附加的除錯器。
RuntimeNotInitializedErrorfrom magiclock_host.errors import RuntimeNotInitializedError在這個處理程序中,bootstrap() 尚未執行過,某個受閘控模組的檢查就先執行了。

相容性說明

.pya/.enc 信封內嵌了產生它們的 CPython 主版本號與次版本號。在 3.12 下加密的產物無法在 3.13 下載入——升級直譯器後請重新執行一次 protect/protect-model,而不要讓同一個產物跨 Python 版本使用。