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":
        ...  # 这台机器还没激活——执行 `magiclock activate`
    elif name == "TrialExpiredError":
        ...  # 产物的有效期已经过了
    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 版本使用。