
Monty用 Rust 为 AI 生成的代码打造的无容器沙箱 Python 解释器【免费下载链接】montyA minimal, secure Python interpreter written in Rust for use by AI项目地址: https://gitcode.com/GitHub_Trending/monty3/montyMonty 是一个用 Rust 编写的极简、安全的 Python 解释器专为由 AI语言模型生成的、未经人工审查的代码设计以库的形式嵌入宿主进程无需容器、虚拟机或任何外部沙箱服务即可执行 LLM 生成的 Python。本文基于本仓库 README 及其配套文档系统讲解 Monty 的安装方式、核心执行模型、外部函数与挂载机制、暂停-恢复快照以及由 VM 自身强制的资源限制帮助读者在 Python、JavaScript/TypeScript 与 Rust 三种生态中快速落地一套可审计、可恢复、受约束的 AI 代码执行环境。概述一个语言级沙箱而不是 OS 级沙箱Monty 的安全模型是**语言级language-level**的没有容器、没有 seccomp 过滤、没有虚拟机隔离完全由解释器自身实现——沙箱代码无法表达任何触碰宿主机的操作因为字节码 VM 里根本不存在这样的操作详见 docs/security.md。这一点与常见的容器沙箱有本质区别无环境权威不配置任何挂载mounts和宿主函数时沙箱内读不了文件、读不了环境变量、开不了 socket、也起不了进程。这不是被拦截而是能力本身不存在于 VM 中。解释器不做任何文件系统 I/O需要访问文件系统时解释器只是挂起并提交一个操作描述由宿主组件决定如何处理。所有文件系统代码都在独立的monty-fscrate 中部分构建产物甚至不会链接它。危险模块缺失socket、subprocess、multiprocessing、threading、ctypes均不可导入也不在随附的 typeshed 中因此类型检查阶段就能拒绝使用它们的代码。无 FFI、无 C 依赖沙箱内没有任何调用原生代码的途径。从解析到执行Monty 使用 Ruff 的解析器解析 Python并在自研字节码 VM 上执行——不依赖 CPython、无 FFI、无 C 依赖启动耗时是微秒级而非数百毫秒见 crates/monty/README.md。文档中将 Monty 描述为minimal, secure Python 3.14 interpreter见 docs/index.md。性能上README 给出的关键数字是创建一个沙箱并运行十条命令约 5 msDocker 约 900 ms沙箱服务约 1900 ms。docs/index.md 提供了更完整的对比表沙箱方案冷启动Agent 运行热合计Monty4.50 ms0.40 ms4.90 msFull MontyWebSocket3.50 ms3.90 ms7.40 msWASI / wasmtime16 ms180 ms200 msDocker195 ms700 ms900 ms沙箱服务Daytona1500 ms400 ms1900 msPyodide in Deno2700 ms35 ms2700 ms低延迟的根源在于沙箱就是一个子进程一条命令只是往返各一条消息且会话持久化使每条命令都是一次 feed无需重放此前所有命令。更多背景与对比见 docs/alternatives.md。安装一个包而不是一套基础设施README 提供了三种语言的安装命令uv add pydantic-monty # Python npm install pydantic/monty # JavaScript / TypeScript cargo add monty # Rust对应的快速入门分别见 docs/quickstart/python.md、docs/quickstart/javascript.md 与 docs/quickstart/rust.md。docs/index.md补充说明包体积约 4.5 MB无守护进程、无镜像、无 API keyworker 基线约 2 MB一台机器可以运行数百个实例。使用 Rust 且面向不可信代码时推荐从monty-pool入手而非裸montycratemontycrate 在宿主进程内运行解释器栈溢出、分配器失败等 abort 会连累宿主进程而monty-pool把解释器放进 worker 子进程池崩溃的 worker 会被替换宿主进程永远安全见 crates/monty/README.md。对于需要 OS 级隔离的部署README 提到商业版Full Monty以容器镜像方式运行同一套 worker详见 docs/server.md。第一个示例模型写代码、宿主执行工具README 的示例是一块巧克力能点亮灯泡多久——code字符串就是模型被问到该问题时写出的代码它调用宿主提供的工具、做算术、打印答案from pydantic_monty import Monty code kcal nutrition(chocolate bar)[kcal] hours kcal * 4184 / (bulb_watts * 3600) print(fa chocolate bar powers a {bulb_watts} W bulb for {hours:.1f} hours) with Monty() as pool: with pool.checkout() as session: session.feed_run( code, inputs{bulb_watts: 10}, external_lookup{nutrition: lambda food: {kcal: 230}}, ) # a chocolate bar powers a 10 W bulb for 26.7 hours关键点nutrition在宿主进程里执行沙箱只能看到它的返回值沙箱内部没有文件系统、环境变量和网络无法触达其他任何东西。同样的例子也提供 TypeScript 版本见 docs/index.md。从源码结构看Python 绑定的核心类位于 crates/monty-python/src/pool.rsMontyPyMonty是持有 worker 池的同步上下文管理器MontySessionPyMontySession对应一个独占 worker 的 REPL 会话feed_run负责执行一段代码并驱动全部外部回调。值得注意的实现细节worker 通过monty-pool以子进程方式运行解释器永远不在宿主进程内执行崩溃隔离不可省略模块注释明确说明 a monty process can never be made fully crash-proof against memory errorsAsyncMonty/AsyncMontySession提供相同的异步 API且允许external_lookup中的宿主函数是协程worker 以空环境启动Windows 仅保留SystemRoot宿主机密钥从一开始就不存在于 worker 内存中。沙箱触达宿主的三种机制一切沙箱可触达宿主的路径都收敛到三种机制详见 docs/security.md全部按 feed 逐次显式开启1. 宿主函数Host functions沙箱读取一个未定义的名称时执行会挂起由宿主的external_lookup解析该名称可调用条目成为沙箱可调用的函数宿主代码以宿主进程的完整权限运行然后恢复执行。非可调用值在名称被读取时转换后返回见 docs/host-functions.mdfrom pydantic_monty import Monty def get_price(sku: str) - float: return {A1: 3.5, B2: 12.0}[sku] with Monty() as pool: with pool.checkout() as session: result session.feed_run( get_price(B2) * 2, external_lookup{get_price: get_price} ) print(result) # 24.0inputs与external_lookup的差别在于时机前者在运行前急切绑定、每个条目都会被转换即使未使用可调用条目只是携带__name__的引用后者在名称被读取时惰性解析只转换代码实际触达的部分可调用条目会成为宿主函数。一个名字同时出现在两者时由急切的inputs优先供给。实践建议是代码围绕的少量常备值用inputs工具面用external_lookup——模型写代码调用你十个工具时只为真正调用的那几个付费。2. 宿主对象与类Host objects and classesClassInstance/ClassType包装器把宿主对象或类放到沙箱面前。每次方法调用、惰性属性读取、initTrue构造都会运行你的宿主代码。eager_attrs、lazy_attrs、allowed_methods是默认为空的名称白名单init是默认为False的布尔门all仍会跳过下划线前缀名称。没有任何东西被自动包装——方法返回另一个对象时除非convert_value钩子按你选择的策略包装它否则转换失败。安全模型文档的结论是Monty 保证沙箱触达不到你未交给它的任何东西但不保证你交给它的东西是安全的——接收路径、URL、SQL 片段或 shell 字符串的宿主函数本质上是你亲手写的文件系统、网络、数据库或 shell 原语参数必须像对待任何不可信输入一样校验。3. 挂载与os回调Mounts and the os callback宿主机目录通过MountDir挂载到沙箱内的虚拟路径只有在挂载内部open()与pathlib才有意义import tempfile from pathlib import Path from pydantic_monty import Monty, MountDir with tempfile.TemporaryDirectory() as tmp: Path(tmp, notes.txt).write_text(mounted from the host) with MountDir(host_pathtmp, virtual_path/data, moderead-only) as mount: with Monty() as pool: with pool.checkout() as session: print(session.feed_run(open(/data/notes.txt).read(), mountmount)) # mounted from the host挂载约束是结构性的而非检查性的每个挂载在挂载时打开一次cap_std::fs::Dir描述符所有操作都相对于它执行..、符号链接和操作中途被替换的目录都无法逃出挂载..与.在触碰文件系统前就被折叠绝对目标符号链接在读写挂载中被拒绝overlay 挂载整体拒绝符号链接路径组件中的空字节被拒绝返回给沙箱的路径如Path.resolve()的结果一律是虚拟路径宿主机路径永不泄漏。/tmp、/etc、/proc、/dev、~与宿主机工作目录除非你显式挂载否则均不可达。完整的挂载模式read-only / read-write / copy-on-write overlay与路径加固策略见 docs/filesystem.md。独立的os回调处理挂载覆盖不到的操作剩余的pathlib操作、os.getenv、os.environ、date.today()与datetime.now()。Python 侧AbstractOS是该回调的类型化形式OSAccess基于你提供的内存文件与environ映射实现它重写其中一个方法即可替换对应操作。暂停与恢复把整个解释器序列化成字节README 将暂停的解释器可序列化为字节、之后随时恢复列为三大核心能力之一。它之所以成立是因为沙箱不持有任何操作系统资源没有存活的文件描述符、socket 或线程挂起时一切有意义的状态都在解释器自己的堆里。会话 dump 与执行快照可快照的对象有两类详见 docs/snapshots.md类型何时生成如何恢复内容会话 dumpSession dump两次 feed 之间无代码在运行load_session全局变量、函数、类、时间预算快照Snapshotfeed 中途、挂起点load_snapshot上述全部外加暂停的调用栈两者都由dump()产生、都是不透明字节。用错 loader 会抛错且两个 loader 都只能用于尚未执行任何 feed 的全新会话。字节是 Monty 自有 dump 格式MONTY\0魔数后跟 dump 格式版本号读取不同版本的构建会拒绝加载——因此 dump 只在同一个 Monty 版本内有效同样的字节可以进程内、子进程内与 WebSocket 上加载。feed_start在每次挂起时交还控制权feed_start是feed_run的可挂起孪生不把一段代码驱动到完成而是在每次挂起时把控制权交还给宿主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 调用设置了object_id时是宿主对象上的方法调用resume(result)、resume_not_handled()、resume_auto()NameLookupSnapshot读取了未定义名称设置了object_id时是宿主对象的惰性属性resume(value...)resume()抛NameErrorresume_auto()FutureSnapshot所有沙箱任务都阻塞在宿主 future 上resume({call_id: result})MontyComplete无需恢复——代码段已结束读取.outputFunctionSnapshot.resume接受四种应答形状{return_value: value}、{exception: ValueError(...)}、{exc_type: ValueError, message: ...}当你没有原始异常对象时例如恢复一个他处创建的快照以及{future: ...}返回一个沙箱可await的挂起 future稍后在产生的FutureSnapshot处结算。每个快照只能恢复一次。恢复时需要注意dump 自带其配置script_name、资源限制、类型检查状态宿主实例存储不随 dump 迁移恢复后宿主对象以MontyClassProxy等只读代理形式出现其方法调用抛RuntimeError累计时间预算随 dump 迁移只有 suspension 限制迁移且计数归零挂载不迁移宿主路径从不进入 dump恢复后的FutureSnapshot无法resume_auto()其挂起协程活在前一个进程里。典型用法包括长时间运行的 agent 在工具调用处挂起、持久化、工具应答后恢复敏感调用处的人审批门从一个快照分叉出多个分支以及部署排空时把会话 dump 到别处继续。Rust 侧MontyRun 与 RunProgress在 Rust 中核心入口是MontyRun解析并编译一次run用输入值执行并返回最终表达式值见 crates/monty/src/run.rsstart返回RunProgress见 crates/monty/src/run_progress.rs在宿主函数调用处暂停执行use monty::{MontyRun, RunProgress}; use monty_types::{CompileOptions, ResourceTracker, MontyObject, PrintWriter, ResourceLimits}; let code data get_data(3)\ndata * 2; let runner MontyRun::new(code.to_owned(), main.py, vec![get_data.to_owned()], CompileOptions::default()).unwrap(); // pass the external function in as an input let get_data MontyObject::Function { name: get_data.to_owned(), docstring: None }; let progress runner.start(vec![get_data], ResourceTracker::default(), PrintWriter::Stdout).unwrap(); // execution pauses at the get_data(3) call let RunProgress::FunctionCall(call) progress else { panic!(expected a function call) }; assert_eq!(call.function_name, get_data); assert_eq!(call.args, vec![MontyObject::Int(3)]); // the host computes the result and resumes let progress call.resume(MontyObject::Int(21), PrintWriter::Stdout).unwrap(); let RunProgress::Complete(result) progress else { panic!(expected completion) }; assert_eq!(result, MontyObject::Int(42));REPL 会话则是自包含的解释器快照用dump()序列化、存入文件或数据库之后在另一个进程甚至另一台机器上用Dump::load()恢复并继续喂代码见 crates/monty/src/dump_format.rs 与 crates/monty/src/repl.rs 中的MontyRepl。MontyRun与RunProgress虽无自有 dump 格式但都实现了serde::Serialize/Deserialize宿主可以用自己的序列化格式缓存已解析代码或暂停中的执行。异步宿主函数同样支持FunctionCall::resume_pending让沙箱代码await一个挂起 future全部任务阻塞时执行让出RunProgress::ResolveFutures由宿主供值。资源限制由 VM 自身强制的硬约束不可信代码迟早会试图无限分配或无限循环。Monty 对内存、执行时间与递归深度施加硬限制按会话配置。五个配置项详见 docs/resource-limits.md键含义max_memory最大堆内存字节max_duration_secs最大累计执行时间秒max_recursion_depth最大函数调用栈深度默认 1000gc_interval每 N 次分配运行一次垃圾回收max_suspensions每个会话最大宿主往返次数外部调用、os回调、名称查找、future 解析默认 1000Python 侧每个键都可选省略max_memory/max_duration_secs或设为None即禁用该限制max_recursion_depth与max_suspensions不可禁用省略或传None保持 1000 默认gc_interval省略使用内置的每 100,000 次分配一次的调度且回收不可关闭。JavaScript 中对应字段是maxMemory、maxDurationSecs、maxRecursionDepth、gcInterval、maxSuspensions作为limits传给pool.checkout()Rust 中它们是monty_types::ResourceLimits的字段时长名为Duration类型的max_duration。几个与直觉不同的关键语义内存max_memory统计 worker 向全局分配器请求的字节从 worker 进程最精简状态起算会话分配的一切都计入包括保留的编译代码与解释器内部结构它不是进程 RSS 上限分配器开销、碎片化以及线程栈、二进制映射镜像、直接mmap都不计入。结果大小可从输入预测的操作在分配前预检阈值 100 KB 以上包括整数乘法/除法/divmod、左移、整数幂、序列重复x * n、str.replace/bytes.replace、re.sub、填充方法、deque 旋转与切片、将迭代器物化为容器、带动态宽度/精度的 f-string 或str.format()——所以x * 10**12会立即失败而不是耗尽机器内存。此外指数超过u32::MAX的base ** exp抛OverflowError0、1、-1 除外int(s, base)在基数非 2 的幂时拒绝超过 4,300 位的字符串与 CPython 的sys.int_info.default_max_str_digits一致。时间max_duration_secs统计累计执行时间而非墙钟时间。时钟只在解释器执行字节码时走动挂起等待宿主一个耗时一分钟的宿主函数不计费时暂停跨feed_run调用在会话生命周期内累加会被序列化进快照因此恢复的会话接续预算而不是从零开始沙箱代码没有任何办法观测预算或剩余时间。由于沙箱内检查只在解释器检查点运行无法捕捉卡死解释器本身的代码两个宿主侧兜底覆盖它池上的request_timeout每次宿主函数或挂载调用后的恢复都会开启新截止因此频繁挂起的程序可活过任何单个超时与时长兜底设置了max_duration_secs的会话worker 每轮协议都上报执行时间宿主机在预算耗尽后的宽限期杀 worker宽限期默认 1 秒。递归Python 层调用深度默认 1000 帧第 1001 层嵌套调用抛RecursionError。与内存/时间限制不同RecursionError可在沙箱内被捕获与 CPython 一致沙箱代码无法调高上限——生产构建中sys.setrecursionlimit不可用。每个await边界计为一帧所以await链不会放大深度。map()、filter()、sorted(key...)、min/max(key...)、递归__repr__/__str__等回调在原生 Rust 栈上重入而非堆分配的帧栈以较低固定深度独立设限在原生栈溢出可能 abort 进程之前抛RecursionError。挂起max_suspensions统计外部调用、宿主对象方法与构造、惰性属性查找、os回调、名称查找与 future 解析事件。由于max_duration_secs在挂起期间暂停代码段本可无限重试被拒绝的调用该限制正是为此兜底。第一个超预算的挂起以不可捕获的RuntimeError: suspension limit 3 exceeded中止 feed会话保持一致、可 dump后续 feed 运行到再次挂起为止恢复 dump 保留限制但计数归零恢复时 checkout 设置的max_suspensions会封顶 dump 中的值。限制触发后的处置至关重要内存或时间限制是终局性的——沙箱代码无法捕获且一旦触发堆状态与引用计数不再有任何保证堆中可能残留引用计数错误的孤儿对象必须丢弃会话而不是继续在其中运行代码池不会替你丢弃。被捕获的RecursionError是例外它不使任何东西失效执行可以继续。崩溃隔离与仓库结构Python 包与原生pydantic/monty绑定从不在你的进程内运行解释器每个会话都运行在montyworker 子进程中。worker 死亡时池观察到死亡、丢弃 worker、生成替代品调用抛MontyCrashedErrorRust 侧为PoolError::Crashed会话丢失但你的进程安然无恙。父进程把来自 worker 的每一帧都当作不可信输入线路解码验证一切、强制深度与大小预算、对畸形数据绝不 panic违反协议的 worker 被丢弃。WebAssembly 构建没有子进程可用浏览器中在Worker中离线程运行Node 下无全局Worker退化为进程内执行——语言级沙箱仍然成立但崩溃隔离不再成立详见 docs/security.md。从 crates/monty/README.md 可见完整的 crate 生态核心仓库布局如下crates/monty —— 核心解释器 cratePython 解析器、字节码 VM 与沙箱MontyRun、RunProgress、MontyRepl、Dump/Session均在此crates/monty-types —— 共享边界数据类型值、异常、OS 调用、资源限制宿主无需链接解释器即可使用crates/monty-fs —— 宿主侧文件系统挂载把虚拟沙箱路径映射到真实宿主目录crates/monty-runtime ——monty可执行文件REPL、文件运行器与子进程 worker 模式crates/monty-pool —— 崩溃隔离的montyworker 子进程弹性池crates/monty-proto —— 池父进程与 worker 之间通信的 protobuf 线路协议crates/monty-type-checking 与 crates/monty-typeshed —— 沙箱代码类型检查及其裁剪版 stdlib stubscrates/monty-macros ——monty参数解析背后的 proc 宏crates/monty-python ——pydantic_montyPython 绑定Monty/AsyncMonty/AsyncMontyWebsocket及会话类见 crates/monty-python/src/pool.rscrates/monty-js ——pydantic/montyTypeScript 绑定。仓库还维护着大规模的行为测试语料crates/monty/test_cases 下按模块命名了数百个.py用例如arith__div_zero_int.py、dict__methods.py、refcount__cycle_self_reference.py、ext_call__name_lookup_undefined.py与 crates/monty/tests 中的 Rust 集成测试配合验证 VM 语义、错误路径与引用计数行为是理解各功能边界的第一手材料。生态与适用场景Monty 运行 Pydantic AI 的 Code Mode社区绑定还有 Go 的gomonty与 Dart/Flutter 的dart_monty它隶属于 Pydantic StackPydantic AI、Pydantic Logfire、Logfire AI Gateway。LLM 写一段调用你的工具的短程序往往比一连串独立工具调用更快、更便宜、更可靠而 Monty 正是承载这些生成代码的安全执行地背景讨论见 docs/index.md。如果你的场景是AI 生成代码需要执行但不想为每次执行付出容器级延迟与成本、需要把执行中途状态持久化到字节以便跨进程/跨机器恢复、需要 VM 自身强制的内存/时间/递归硬限制那么 Monty 提供的是一个开箱即用的包级方案——uv add pydantic-monty、npm install pydantic/monty或cargo add monty即可起步。需要注意的边界是解释器实现的是经过裁剪的 Python 子集已知与 CPython 的差异清单见 docs/limitations/index.md宿主的回调宿主函数、os回调运行在宿主进程内其安全性完全取决于你所写的代码。【免费下载链接】montyA minimal, secure Python interpreter written in Rust for use by AI项目地址: https://gitcode.com/GitHub_Trending/monty3/monty创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考