
monty v0.0.20
一个使用 Rust 编写的、最小化且安全的 Python 解释器,供 AI 使用。
Monty
一个用 Rust 编写的、供 AI 使用的最小化、安全的 Python 解释器。
实验性 - 该项目仍处于开发阶段,尚未做好正式发布的准备。
一个用 Rust 编写的、供 AI 使用的最小化、安全的 Python 解释器。
Monty 避免了为运行 LLM 生成的代码而使用基于完整容器的沙箱所带来的成本、延迟、复杂性以及各种麻烦。
相反,它让你安全地运行由嵌入在智能体中的 LLM 编写的 Python 代码,启动时间以个位数微秒计算,而不是数百毫秒。
Monty 可以做什么:
- 运行一个合理的 Python 子集——足以让你的智能体表达它想要做的事情
- 完全阻止对主机环境的访问:文件系统、环境变量和网络访问都通过开发者可以控制的外部函数调用来实现
- 调用主机上的函数——仅限于你授予访问权限的函数
- 运行类型检查——Monty 支持完整的现代 Python 类型提示,并在单个二进制文件中附带 ty 用于运行类型检查
- 在外部函数调用时被快照为字节,这意味着你可以将解释器状态存储在文件或数据库中,稍后恢复
- 启动极快(从代码到执行结果少于 1μs),并且运行时性能与 CPython 相似(通常快 5 倍到慢 5 倍之间)
- 可从 Rust、Python 或 Javascript 调用——因为 Monty 不依赖 cpython,你可以在任何能运行 Rust 的地方使用它
- 控制资源使用——Monty 可以跟踪内存使用、堆栈深度和执行时间,并在超出预设限制时取消执行
- 收集 stdout 和 stderr 并将其返回给调用者
- 通过主机端的异步或同步代码在主机上运行异步或同步代码
- 使用标准库的一小部分:
sys、os、typing、asyncio、re、datetime、json、dataclasses(即将支持)
Monty 不能做什么:
- 使用标准库的其余部分
- 使用第三方库(如 Pydantic),支持外部 Python 库不是目标
- 定义类(支持应很快到来)
- 使用 match 语句(同样,支持应很快到来)
简而言之,Monty 功能极其有限,专为一个用例而设计:
运行由智能体编写的代码。
如果你想了解为什么要这么做,可以参考:
- Codemode 来自 Cloudflare
- 编程式工具调用 来自 Anthropic
- 使用 MCP 执行代码 来自 Anthropic
- Smol Agents 来自 Hugging Face
简单来说,上述所有方案的核心思想是:如果让 LLM 编写 Python(或 Javascript)代码,而不是依赖传统的工具调用,它们可以更快、更便宜、更可靠地工作。Monty 让这一切成为可能,而不必面对沙箱的复杂性,也不必承担直接在主机上运行代码的风险。
注意: Monty 将(很快)被用于在 Pydantic AI 中实现 codemode。
使用
可以从 Python、JavaScript/TypeScript 或 Rust 调用 Monty。
Python
要安装:```bash uv add pydantic-monty
(或者,`pip install pydantic-monty` 是给老古董们用的)
`pydantic-monty` 是一个元包,将 `pydantic-monty-client`(`pydantic_monty` 模块)与 `pydantic-monty-runtime`(`monty` 工作程序二进制文件)配对。如果二进制文件已经来自其他来源,则可以单独安装 `pydantic-monty-client`。
用法:```python
from typing import Any
import pydantic_monty
code = """
async def agent(prompt: str, messages: Messages):
while True:
print(f'messages so far: {messages}')
output = await call_llm(prompt, messages)
if isinstance(output, str):
return output
messages.extend(output)
await agent(prompt, [])
"""
type_definitions = """
from typing import Any
Messages = list[dict[str, Any]]
async def call_llm(prompt: str, messages: Messages) -> str | Messages:
raise NotImplementedError()
prompt: str = ''
"""
Messages = list[dict[str, Any]]
async def call_llm(prompt: str, messages: Messages) -> str | Messages:
if len(messages) < 2:
return [{'role': 'system', 'content': 'example response'}]
else:
return f'example output, message count {len(messages)}'
async def main():
async with pydantic_monty.AsyncMonty() as pool:
async with pool.checkout(
script_name='agent.py',
type_check=True,
type_check_stubs=type_definitions,
) as session:
output = await session.feed_run(
code,
inputs={'prompt': 'testing'},
external_lookup={'call_llm': call_llm},
)
print(output)
#> example output, message count 2
if __name__ == '__main__':
import asyncio
asyncio.run(main())
执行发生在 monty 工作子进程池中,因此即使由对抗性代码
触发的内存错误(栈溢出、分配器中止)也永远不会使你的
进程崩溃——工作子进程会终止,抛出 MontyCrashedError,
并被替换。此外还有一个完全同步的 API:```python
import pydantic_monty
with pydantic_monty.Monty() as pool: with pool.checkout() as session: # session state persists between feed_run calls session.feed_run('x = 21') print(session.feed_run('x * 2')) #> 42
### JavaScript / TypeScript
安装:```bash
npm install @pydantic/monty
The JS package is a native (napi) binding over the same Rust worker pool the
Python package uses — the binding and the monty worker binary ship via
platform-specific npm packages:```ts
import { Monty } from '@pydantic/monty'
await using pool = await Monty.create() await using session = await pool.checkout()
// session state persists between feedRun calls await session.feedRun('x = 21') console.log(await session.feedRun('x * 2')) // 42
// external functions may be async const result = await session.feedRun('await fetch_data()', { externalLookup: { fetch_data: async () => 'data' }, })
对于浏览器(或任何无法使用子进程的环境),同一个包
在 `@pydantic/monty/wasm` 子路径下提供了进程内 WebAssembly 构建
(没有崩溃隔离:那里的沙箱崩溃即是主机崩溃)。
### Rust
对于在 Rust 中运行不受信任的代码,我们推荐使用
[`monty-pool`](https://crates.io/crates/monty-pool) crate,而不是下面提供的进程内 API。
`monty-pool` 仅在 `monty` 工作子进程中运行代码,这提供了额外保护:
由对抗性代码(栈溢出、分配器中止)触发的崩溃只会杀死工作进程 —
池会检测到该进程死亡并替换它 — 并且父端看门狗可以杀死
超过硬超时时间的工作进程。它与上述 Python 和 JavaScript 包所基于的
引擎相同。有关用法,请参阅 [monty-pool README](https://github.com/pydantic/monty/tree/main/crates/monty-pool)
以获取用法。
`monty` crate 本身提供了进程内解释器:```rust
use monty::MontyRun;
use monty_types::{CompileOptions, ResourceTracker, MontyObject, PrintWriter, ResourceLimits};
let code = r#"
def fib(n):
if n <= 1:
return n
return fib(n - 1) + fib(n - 2)
fib(x)
"#;
let runner = MontyRun::new(code.to_owned(), "fib.py", vec!["x".to_owned()], CompileOptions::default()).unwrap();
let result = runner.run(vec![MontyObject::Int(10)], ResourceTracker::default(), PrintWriter::Stdout).unwrap();
assert_eq!(result, MontyObject::Int(55));
序列化
REPL 会话可以使用 dump() 序列化,并通过 Dump::load() 恢复。转储数据携带会话元数据(脚本名称、类型检查存根)以及解释器状态,并附有加载构建所检查的版本:```rust
use monty::{Dump, MontyRepl, Session, SessionRef, dump};
use monty_types::{CompileOptions, MontyObject, PrintWriter, ResourceTracker};
// Snapshot a session between snippets let mut repl = MontyRepl::new("main.py", ResourceTracker::default(), CompileOptions::default()); repl.feed_run("x = 41", vec![], PrintWriter::Stdout).unwrap(); let bytes = dump("main.py", None, SessionRef::Idle(&repl)).unwrap();
// Later, restore and carry on feeding let Session::Idle(mut restored) = Dump::load(&bytes).unwrap().state else { panic!("dumped an idle session") }; let result = restored.feed_run("x + 1", vec![], PrintWriter::Stdout).unwrap(); assert_eq!(result, MontyObject::Int(42));
`MontyRun` 和 `RunProgress` 本身没有转储格式,但两者都实现了 `serde::Serialize`/`Deserialize`,因此宿主可以使用它已有的任何格式来序列化解析后的代码或暂停中的运行。
## Worker 中的内存限制
会话的 `max_memory` 由 worker 的分配器(allocator)衡量。解释器在超过软限制后会报告一个优雅的 `MemoryError`;如果某次分配在检查点之间跳跃过大,更高的硬限制会终止并替换该 worker。