pydantic-monty 实战指南用 Monty 沙箱在 Python 宿主进程中安全执行不可信代码【免费下载链接】montyA minimal, secure Python interpreter written in Rust for use by AI项目地址: https://gitcode.com/GitHub_Trending/monty3/montypydantic-monty 是 Monty一个用 Rust 编写、专为 AI 场景设计的最小化安全 Python 解释器的 Python 绑定包。它以子进程池架构隔离执行所有沙箱代码即使沙箱内发生段错误、栈溢出或分配器崩溃宿主进程也毫发无损崩溃的 worker 会被透明替换并抛出MontyCrashedError。读完本文你将掌握如何安装并配置 pydantic-monty使用Monty/AsyncMonty运行同步与异步沙箱代码通过ClassInstance/ClassType安全地向沙箱暴露宿主对象并利用快照snapshot机制暂停、序列化、跨进程恢复执行。架构基础为什么执行必须发生在子进程池中pydantic-monty是 Monty 的 Python 官方绑定其核心理念写在其文档第一段执行永远发生在一组montyworker 子进程中。原因很直接——Monty 进程无法对内存类错误stack overflows、allocator aborts做到完全崩溃免疫尤其是面对对抗性输入时。因此崩溃隔离被内建为架构特性某个 worker 崩溃后池会透明地替换它你的进程永远处于安全侧。相关的行为细节宿主侧挂载、带缓冲的 print 回调、会话 dump记录在仓库的 limitations/pool-architecture.md 中。从 packages/pydantic-monty/pyproject.toml 可以看到pydantic-monty本身是一个metapackage元包它自身不携带任何代码只负责把构成一个可用沙箱的两个发行版精确固定为同一版本当前均为0.0.22一起安装pydantic-monty-client—— 你实际import的pydantic_monty模块worker 池、会话、值转换源码位于 crates/monty-pythonpydantic-monty-runtime—— 池要拉起的montyworker 二进制以与uv、ruff相同的方式随 wheel 分发maturin 的 bin 绑定模式。pyproject.toml中[project.scripts]声明的pydantic-monty pydantic_monty._cli:main是唯一由元包提供的可执行入口。值得注意的实现细节是这个 console script刻意不叫monty否则会与pydantic-monty-runtime安装到 scripts 目录里的真实二进制同名冲突、静默覆盖它导致后续的二进制查找解析到 shim 自身见 crates/monty-python/python/pydantic_monty/_cli.py 的模块文档。安装与二进制定位标准安装方式二选一uv add pydantic-monty # or pip install pydantic-monty元包会同时拉入 client 与 runtime 两个发行版。需要安装 OpenTelemetry 可观测性支持时使用pip install pydantic-monty[opentelemetry]只安装 client 的场景当 worker 二进制来自其他渠道——基础镜像base image、系统包、或本仓库直接cargo build出来的产物——可以只安装pydantic-monty-client然后通过三种方式之一让pydantic_monty找到它环境变量MONTY_BINMonty(binary_path...)构造参数将二进制放在PATH上pydantic-monty-client的独立安装说明见 crates/monty-python/README.md它同时支持单独用于连接远程monty-serverWebSocket 传输。二进制解析顺序find_monty_binary 实现了完整的查找链按优先级排列显式传入的binary_path参数MONTY_BIN环境变量当前环境的 scripts 目录pydantic-monty-runtimewheel 默认安装位置会同时检查sysconfig.get_path(scripts)与--user方案目录PATH上名为montyWindows 下为monty.exe的可执行文件开发模式兜底当以可编辑安装运行在本仓库内时回退到repo/target/{debug,release}/monty中最近构建的那个cargo build与cargo build --release都能被正确感知。全部失败则抛出FileNotFoundError提示安装pydantic-monty或在仓库内执行make dev-py、传入binary_path...或设置MONTY_BIN。CLI不安装也能用安装了更好用不安装任何东西直接用 uvx 运行uvx pydantic-monty --helpuvx pydantic-monty不带参数时进入 REPL带文件参数时执行该文件uvx pydantic-monty file。或者把monty装到本地使用uv tool install pydantic-monty-runtime # 然后运行 REPL: monty # 或运行文件: monty file # 或查看帮助: monty --help在已安装pydantic-monty的环境中python -m pydantic_monty会运行同一个二进制。其实现__main__.py只是转发参数POSIX 上直接os.execv替换进程信号与退出码归二进制自身所有Windows 上没有 exec 语义则subprocess.Popen等待子进程并在等待循环中吞掉KeyboardInterrupt避免 Ctrl-C 提前退出而把二进制孤儿化在终端上。基本执行池、会话与 feed_run最简用法from pydantic_monty import Monty with Monty() as pool: with pool.checkout() as session: print(session.feed_run(1 2)) # 3这里有三层概念Monty()是一个worker 池context managerwith进入时才真正拉起子进程pool.checkout()把池中一个专用 worker 租给一个 REPL 会话session.feed_run(code)在会话内执行一段代码并返回末表达式的值。会话状态全局变量、函数定义跨feed_run调用持续存在from pydantic_monty import Monty with Monty() as pool: with pool.checkout() as session: session.feed_run(x 40) print(session.feed_run(x 2)) # 42Monty 池的构造参数依据 crates/monty-python/python/pydantic_monty/_monty.pyi 中的签名Monty()支持以下关键字参数参数默认值含义binary_pathNonemontyCLI 二进制路径省略时按上文解析顺序查找min_processes1预热并常驻的 worker 数max_processesCPU 核数存活 worker 上限超出时 checkout 排队等待 worker 归还checkout_timeoutNone永远等待checkout()等待空闲 worker 的秒数超时抛TimeoutErrorrequest_timeoutNone宿主侧每轮turn截止时间worker 超时会被杀死调用抛timed_outTrue的MontyCrashedErrormax_checkouts_per_workerNoneworker 服务完这么多会话后被回收checkout 的会话级参数checkout()返回的MontySession同样是 context managerwith块退出时 worker 归还池中。其参数包括script_name默认main.py出现在 traceback 与错误消息里的文件名limits在 worker 内部强制执行的资源限制见下文专节其中max_suspensions由池侧自行强制type_check/type_check_stubs/type_check_format/type_check_color类型检查开关与诊断渲染选项见下文专节assert_message_annotations默认开启让失败的assert输出 pytest 风格的内省消息如AssertionError: assert 2 5这是相对 CPython 空消息AssertionError的有意分歧设为False恢复 CPython 行为设为int 1可自定义每个操作数 repr 的截断长度默认 120 字节print_flush_interval默认0.005秒worker 最多缓存多久的print()输出再统一发送使大量 print 只产生一次回调而非每次一次设为0恢复行缓冲每完成一行就投递。输出在任何宿主调用前和运行结束前总是被冲刷因此该参数只影响实时性不影响最终送达内容与顺序。异步执行AsyncMontyAsyncMonty是Monty的 asyncio 对应物worker I/O 在事件循环之外运行且external_lookup中的外部函数可以是协程。其池构造参数与Monty完全一致见 crates/monty-python/python/pydantic_monty/_monty.pyi 中AsyncMonty.__new__。import asyncio from pydantic_monty import AsyncMonty async def fetch(url: str) - str: await asyncio.sleep(0.01) return fcontents of {url} async def main(): async with AsyncMonty() as pool: async with pool.checkout() as session: result await session.feed_run( await fetch(https://example.com), external_lookup{fetch: fetch}, ) print(result) # contents of https://example.com asyncio.run(main())协程外部函数会被并发等待并通过AsyncFutureSnapshot结算。输入变量与外部查找feed_run提供两种向沙箱注入宿主值的机制inputs急切绑定在代码运行前就把每个条目转换并绑定为全局变量——无论代码是否引用都会绑定一次external_lookup惰性按需解析可调用条目成为沙箱可调用的宿主函数任何其他值在名字被读取时转换并返回缺失的名字抛NameError。同名时由急切的inputs绑定优先。from pydantic_monty import Monty with Monty() as pool: with pool.checkout() as session: result session.feed_run( double(x) y, inputs{x: 5, y: 1}, external_lookup{double: lambda x: x * 2}, ) print(result) # 11feed_run还接受print_callback默认输出到宿主进程的 stdout/stderr也可传CollectStreams/CollectString收集器二者默认 10 MiB 上限、传max_bytesNone可禁用、mount宿主目录挂载见下、os未被挂载覆盖的 OS 调用兜底处理器与skip_type_check。需要强调的是feed_run会阻塞调用线程期间释放 GIL异步外部函数在此不受支持——请用AsyncMonty。宿主对象与类ClassInstance / ClassType沙箱中的代码可能需要操作宿主进程中的对象。pydantic-monty用两个包装器以白名单策略控制暴露面实现位于 crates/monty-python/python/pydantic_monty/class_instance.py。from dataclasses import dataclass from pydantic_monty import ClassInstance, ClassType, Monty dataclass class Person: name: str age: int def greeting(self) - str: return fhi {self.name} person Person(nameSamuel, age4) with Monty() as pool: with pool.checkout() as session: wrapper ClassInstance(person, eager_attrsall, allowed_methods{greeting}) code assert user.greeting() hi Samuel\nuser result session.feed_run(code, inputs{user: wrapper}) print(result is person) # True wrapper ClassType(Person, initTrue, instance_eager_attrsall) print(session.feed_run(Person(Ada, 36).name, inputs{Person: wrapper})) # Ada策略语义ClassInstance包装一个实例是纯宿主侧策略它决定哪些属性急切跨边界eager_attrs、哪些可以惰性按需获取lazy_attrs、哪些方法允许沙箱调用allowed_methods。三者取值均可为None什么都不暴露all暴露包装器判定为“公开”的一切——实例侧为 dataclass 字段或__dict__条目与__slots__中不以_开头的名字类侧为类__dict__中公开的非常量项方法/描述符被_is_class_machinery排除因此all只发送普通类常量一组名字的可迭代对象精确暴露这些名字。注意all之外任何裸字符串都会抛TypeError——防止把ab误当作字符集合而意外暴露每个子串。方法白名单下all只放行定义在类上的函数嵌套类或存储为属性的可调用对象不算实例的__call__永远被拒绝只有ClassType接受它作为构造语义。类的构造能力ClassType是类级包装器initTrue允许沙箱代码实例化该类。构造以一次__call__方法调用到达宿主侧在宿主机执行后结果按instance_*策略instance_eager_attrs、instance_lazy_attrs、instance_allowed_methods包装成ClassInstance送回沙箱。ClassType.method_allowed只放行 classmethod/staticmethod——实例方法经类对象调用会拿任意沙箱值当self故一律拒绝。类还带有稳定的会话内身份ClassInstance默认携带ClassType(type(value))作为其class_type因此沙箱内的type(x)与作为值传入的ClassType是同一个类型对象。convert_value 钩子与返回值包装方法返回值不会自动包装派生对象要按你选择的策略继续暴露需要覆写convert_value每个包装器会被会话保留到关闭。例如在ClassInstance.convert_value中返回ClassInstance(value, eager_attrsall)即可为返回的派生实例套上新的策略。ClassType覆写了convert_value并默认由其实例委托因此一个在构造路径上做了脱敏/包装的ClassType子类其构造出的实例与随其发送的实例都会走同一套钩子。沙箱内部定义的实例到达宿主侧时是只读的MontyClassProxy占位保留实例的id再次传回沙箱可还原为同一存活对象。完整的设计说明见 docs/host-objects.md。快照暂停与恢复执行feed_start是feed_run的可挂起对应物它不把一段代码一路驱动到底而是在每次外部调用、OS 调用、名字查找或 future 结算处把控制权以snapshot快照形式交还宿主。你用snapshot.resume(...)应答它返回下一个快照或MontyComplete。from pydantic_monty import FunctionSnapshot, Monty, MontyComplete with Monty() as pool: with pool.checkout() as session: snapshot session.feed_start(greet(name) !, inputs{name: Ada}) assert isinstance(snapshot, FunctionSnapshot) print(snapshot.function_name, snapshot.args) # greet (Ada,) result snapshot.resume({return_value: hello Ada}) assert isinstance(result, MontyComplete) print(result.output) # hello Ada!快照类型FunctionSnapshot等待一次外部函数或 OS 调用的结果。is_os_function为True时function_name是OsFunction名可resume_not_handled()交给 Monty 的默认未处理行为object_id指向被路由的宿主对象经ClassInstance/ClassType发送的接收者不包含在args中。NameLookupSnapshot等待一个未定义名字的值或object_id被设置时宿主对象上的惰性属性查找。resume()省略value时沙箱内抛NameError属性查找则抛AttributeError。FutureSnapshot所有沙箱任务都阻塞在外部 future 上协程外部函数的同步会话场景。用pending_call_idsresume({call_id: result})按 id 结算resume_auto()在同步会话中恒抛RuntimeError。MontyComplete完成态output属性给出最终值。自动驱动resume_auto不想手工应答每个挂起点时给feed_start传入external_lookup和/或os然后用snapshot.resume_auto()驱动——它会从这些表中自动解析每次外部调用与名字查找这正是feed_run所做的解析只不过一次一步让你能沿途检查甚至dump()每个快照from pydantic_monty import Monty, MontyComplete with Monty() as pool: with pool.checkout() as session: snapshot session.feed_start( greet(name) !, inputs{name: Ada}, external_lookup{greet: lambda n: fhello {n}}, ) while not isinstance(snapshot, MontyComplete): snapshot snapshot.resume_auto() print(snapshot.output) # hello Ada!要点feed_start的初始驱动阶段不消费external_lookup——外部调用与名字查找仍以快照形式浮出该表只是被捕获供后续resume_auto()使用。异步场景中AsyncMonty上的external_lookup可含协程函数resume_auto变为可等待snapshot await snapshot.resume_auto()协程外部被并发等待并经AsyncFutureSnapshot结算但经load_snapshot恢复的FutureSnapshot的挂起协程已随旧进程消失其resume_auto()会抛错需手工resume({call_id: ...})结算。序列化dump 与恢复snapshot.dump()把暂停中的 worker 序列化为字节新会话的load_snapshot恢复它并返回待恢复的快照。这使得执行可以被检查点化并在之后继续——甚至可以跨进程from pydantic_monty import FunctionSnapshot, Monty, MontyComplete with Monty() as pool: with pool.checkout() as session: snapshot session.feed_start( fetch(url), inputs{url: https://example.com} ) blob snapshot.dump() # 稍后——恢复到新会话并继续 with pool.checkout() as session: snapshot session.load_snapshot(blob) assert isinstance(snapshot, FunctionSnapshot) result snapshot.resume({return_value: page contents}) assert isinstance(result, MontyComplete) print(result.output) # page contents注意事项来自load_snapshot的文档如果被暂停的 feed 使用了文件系统mount恢复时必须向load_snapshot(blob, mount...)重新提供相同的挂载——dump 不存储宿主路径且 dump 前的 overlay 写入不会保留恢复后的 overlay 从空开始。session.dump()两次 feed 之间序列化的是空闲会话用session.load_session(blob)恢复返回None之后可继续喂代码。load_session与load_snapshot都只对全新会话尚未有任何 feed有效用错类型会抛错。dump 恢复自己的script_name/limits/类型检查状态checkout()的对应配置不生效类实例存储是宿主状态、从不入 dump因此恢复后的ClassInstance值会退化为MontyClassProxy占位对其的方法调用会失败。AsyncMonty会话暴露同样的feed_start/load_session/load_snapshotresume(...)可等待。资源限制限制在worker 内部强制执行而池的request_timeout是宿主侧兜底——直接杀死卡死的 worker。已安装的遥测会同步调用受信任的 Python SDK 回调此类回调运行期间强制执行被推迟。checkout(limits{...})接受 ResourceLimits一个 TypedDict完整字段如下字段类型含义max_duration_secsfloat \| None最大执行时间秒max_memoryint \| None最大堆内存字节gc_intervalint \| None每 N 次分配运行一次垃圾回收max_recursion_depthint \| None最大函数调用栈深度默认 1000不可禁用max_suspensionsint \| None每个 checkout 内外部调用/os回调/名字查找/future 结算的最大次数默认 1000不可禁用超出后 feed 被不可捕获的RuntimeError中止会话仍可用恢复 dump 会重置计数省略某个键或显式设为None即禁用该限制上述两个例外不可禁用。max_duration_secs 的精确语义max_duration_secs限制的是累积执行时间——时钟只在解释器执行时走动暂停等待宿主时不计时且跨 feed 累积。worker 在每次协议轮次上报执行时间设了限制的会话在剩余预算耗尽后还会被额外杀死宽限为duration_limit_grace1 秒当前不可从 Python 配置——这覆盖了沙箱内限制无法捕获的挂起其检查只在解释器检查点运行。from pydantic_monty import Monty, MontyRuntimeError with Monty(request_timeout10) as pool: with pool.checkout(limits{max_duration_secs: 0.1}) as session: try: session.feed_run(while True:\n pass) except MontyRuntimeError as exc: print(exc.display(formattype-msg).split(:)[0]) # TimeoutError挂载MountDir宿主目录通过MountDir挂入沙箱构造参数全部为关键字参数避免 docker-v与 nginxalias在“宿主在前还是虚拟在前”上的分歧host_path真实宿主目录构造时即打开并校验沙箱代码永远看不到该路径、也到不了其外部。目录内符号链接只跟随相对目标绝对目标即使指回同一挂载也会在沙箱内抛PermissionError。virtual_path沙箱内的绝对 POSIX 风格路径前缀如/data与宿主操作系统无关。moderead-only写入抛PermissionError/read-write穿透写入宿主目录并持久化警告不可信代码写的文件属于不可信输入不要执行它们——若该目录在sys.path上沙箱代码可写入json.py之类的模块名并借后续 import 执行包括 pydantic_monty 自身发起的 import/overlay默认读穿透到宿主写按 feed 隔离在内存中、feed 结束时丢弃。write_bytes_limit单 feed 内累计写入字节上限超出在沙箱内抛OSErrorNone表示不限。memory_usage_limit默认 100 MB每个挂载的内存预算由保留的 overlay 数据与临时文件系统结果共享超出抛MemoryError。目录从构造起保持打开直到close()幂等Windows 上不关闭会阻止宿主重命名/删除该目录可跨 feed 复用、也可作为 context manager 使用把已关闭的 mount 传给 feed 会抛ValueError。使用示例from pathlib import Path from pydantic_monty import Monty, MountDir with Monty() as pool, MountDir(host_pathPath(host-dir), virtual_path/data) as mount: with pool.checkout() as session: contents session.feed_run(open(/data/notes.txt).read(), mountmount)类型检查ty 集成Monty 内置了 ty 类型检查器每个 feed 的代码片段可在 worker 内、运行之前被类型检查成功执行的片段会累积进检查上下文用于后续片段。这在 AI 场景中尤其有价值——在把代码送进沙箱跑之前先拦截类型错误。from pydantic_monty import Monty, MontyTypingError with Monty() as pool: with pool.checkout(type_checkTrue) as session: try: session.feed_run(x: int not an int) except MontyTypingError as exc: print(invalid-assignment in exc.display()) # Truetype_check_format选择诊断的渲染格式取值即 ty 的full默认源码片段加脱字符、concise、azure、json、jsonlines、rdjson、pylint、gitlab、githubtype_check_color为full/concise追加 ANSI 颜色。二者都是checkout()参数而非display()参数原因见 crates/monty-python/python/pydantic_monty/init.py 的类型别名文档类型检查器在 worker 内部运行其结构化诊断永不离开 workerty 的结构化诊断需对照检查器数据库解析 span跨线传输的只有已渲染的文本。from pydantic_monty import Monty, MontyTypingError with Monty() as pool: with pool.checkout(type_checkTrue, type_check_formatconcise) as session: try: session.feed_run(x: int not an int) except MontyTypingError as exc: print(exc.display()) main.py:1:10: error[invalid-assignment] Object of type Literal[not an int] is not assignable to int type_check_stubs可为类型检查提供额外的 stub 声明feed_run(..., skip_type_checkTrue)可在会话开启类型检查时跳过单次 feed。崩溃与故障隔离Monty 代码执行中的一切失败都抛MontyError的子类。异常层级定义于 crates/monty-python/python/pydantic_monty/_monty.pyi包括MontySyntaxError语法错误或无法解析MontyRuntimeError执行期失败携带traceback()Frame列表含文件名、1 起始行列号、函数名、源码行与display(format...)traceback/type-msg/msg三种格式MontyTypingError类型检查拒绝诊断按 checkout 时选择的格式预渲染好MontyConversionError宿主值无法跨边界转换如external_lookup/inputs中出现不支持的类型的值feed 被拒绝MontyCrashedErrorworker 进程消失——段错误、分配器 abort、外部杀死、request_timeout看门狗或它在退出前宣告的致命错误宿主进程毫发无损池会替换 worker。timed_out属性指示是否为看门狗所致exit_status给出 OS 报告的退出码信号死亡时为None远程 worker 恒为NoneMontyDisconnectError仅 WebSocket 传输远程 worker 连接在会话中途关闭——可能是沙箱死了也可能是服务端按策略空闲/会话/轮次超时、过载断开会话客户端无法区分应在新会话重试MontyShutdown仅 WebSocket 传输远程服务器正在关停该请求没有运行可在新会话安全重跑且其dump携带关停前捕获的会话状态可用load_session/load_snapshot跨服务器重启续上会话注意若被中断的请求正在应答某个挂起宿主已执行过该回调恢复后会重新宣布并再次执行此类回调应保持幂等。from pydantic_monty import Monty, MontyError hostile_code ... with Monty() as pool: with pool.checkout() as session: try: session.feed_run(hostile_code) # 即使是段错误也被隔离 except MontyError: ... # worker 已死池已经替换了它可观测性OpenTelemetry 集成先安装可选支持再在创建任何池之前用标准的 Python OpenTelemetry 组件调用instrument_telemetryfrom opentelemetry import _logs, metrics, trace from pydantic_monty import instrument_telemetry instrument_telemetry( tracertrace.get_tracer(pydantic-monty), metermetrics.get_meter(pydantic-monty), logger_logs.get_logger(pydantic-monty), )每个组件都是可选的安装是进程级且只能发生一次各信号可独立启用Tracer把每次 checkout 记录为 session span嵌套 feed span 与挂起suspensionspanLogger在这些 span 下记录异常与print输出WebSocketAsyncMontyWebsocket的 checkout 还会在升级请求上发送当前上下文为 W3Ctraceparent/tracestate头支持的服务端可加入同一 traceconnect_headers回调每个会话进入时、等待池容量之前调用一次同步运行于 checkout 任务上可见其 contextvars不可阻塞可注入额外的str→str头如基础设施令牌重复名后者胜出Meter记录实时、立即可用与宿主阻塞的 worker 数checkout 等待时长按原因的 worker 死亡数运行时长以及每次 feed 的沙箱执行时间。两个安全设计值得强调见 crates/monty-python/python/pydantic_monty/init.py 与 README 的 Observability 一节指标覆盖每次 checkout但不记录任何沙箱提供的值其属性是封闭集合脚本选择的任何内容被调用函数名、异常类、路径都不会成为维度——防止沙箱数据污染监控维度trace 与 log 会记录代码、输入、外部调用、异常与打印输出会话 dump 与恢复只按大小记录。在instrument_telemetry被调用之前插桩处于禁用状态启用后的大值会在遥测属性大小限制处截断。供应商无关是刻意设计提供的 OpenTelemetry providers 拥有 ID、采样、metric views 与聚合、resources、readers、exporters、flush 与 shutdown 的自主权因此 Logfire 等 OpenTelemetry 发行版可走同一条插桩路径如logfire.instrument_monty()提供绑定到其配置实例的组件。延伸阅读docs/host-objects.md宿主对象/类跨边界机制的完整设计文档docs/cli.mdmonty二进制命令行参考docs/resource-limits.md 与 limitations/resource_limits.md资源限制的详细行为limitations/pool-architecture.md子进程执行的架构细节宿主侧挂载、缓冲 print 回调、会话 dumpcrates/monty-python/tests本仓库的 Python 绑定测试test_limits.py、test_type_check.py、test_telemetry.py、test_feed_start.py等可当作活文档阅读crates/monty-python/python/pydantic_monty/_monty.pyi全部公开 API 的完整类型签名与逐参数说明【免费下载链接】montyA minimal, secure Python interpreter written in Rust for use by AI项目地址: https://gitcode.com/GitHub_Trending/monty3/monty创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考