
monty v0.0.19
Минимальный, безопасный интерпретатор Python, написанный на Rust для использования ИИ.
Monty
Минимальный, безопасный интерпретатор Python, написанный на Rust для использования ИИ.
Экспериментально — проект всё ещё находится в разработке и пока не готов к использованию в продакшене.
Минимальный, безопасный интерпретатор Python, написанный на Rust для использования ИИ.
Monty позволяет избежать затрат, задержек, сложности и лишней возни, связанных с полноценной песочницей на основе контейнеров для выполнения кода, сгенерированного LLM.
Вместо этого он позволяет безопасно запускать Python-код, написанный LLM, встроенным в вашего агента, со временем запуска, измеряемым в однозначных микросекундах, а не в сотнях миллисекунд.
Что Monty может:
- Выполнять достаточное подмножество Python-кода — достаточно, чтобы ваш агент мог выразить то, что он хочет сделать
- Полностью блокировать доступ к хостовому окружению: файловая система, переменные окружения и сетевой доступ реализованы через внешние вызовы функций, которые разработчик может контролировать
- Вызывать функции на хосте — только те функции, к которым вы предоставили доступ
- Выполнять проверку типов — Monty поддерживает современные подсказки типов Python и поставляется с ty в одном бинарном файле для проверки типов
- Сохраняться в байты в точках внешних вызовов функций, что позволяет хранить состояние интерпретатора в файле или базе данных и возобновлять работу позже
- Запускаться чрезвычайно быстро (<1мкс от кода до результата выполнения), а производительность во время выполнения сопоставима с 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
- Programmatic Tool Calling от Anthropic
- Code Execution with MCP от Anthropic
- Smol Agents от Hugging Face
Если очень просто, идея всего вышеперечисленного в том, что LLM могут работать быстрее, дешевле и надёжнее, если их просить писать код на Python (или JavaScript) вместо традиционных вызовов инструментов. Monty делает это возможным без сложности песочницы и без риска запуска кода напрямую на хосте.
Примечание: Monty (в ближайшее время) будет использоваться для реализации codemode в Pydantic AI
Использование
Monty можно вызывать из Python, JavaScript/TypeScript или Rust.
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
Пакет JS — это нативный (napi) биндинг поверх того же пула воркеров Rust, что
использует пакет Python — биндинг и рабочий бинарник monty поставляются через
платформозависимые npm-пакеты:```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' }, })
Для браузеров (или везде, где невозможны подпроцессы) тот же пакет
предоставляет внутрипроцессную сборку WebAssembly по подпути `@pydantic/monty/wasm`
(без изоляции сбоев: сбой песочницы там означает сбой хоста).
### Rust
Для запуска недоверенного кода из Rust мы рекомендуем
крейт [`monty-pool`](https://crates.io/crates/monty-pool), а не внутрипроцессный API ниже.
`monty-pool` запускает код только в рабочих подпроцессах `monty`, что даёт дополнительные защиты:
сбой, вызванный вредоносным кодом (переполнение стека, аварийное завершение аллокатора), убивает только рабочий процесс —
пул обнаруживает гибель и заменяет рабочий процесс — а родительский watchdog может убивать рабочие процессы,
превысившие жёсткий тайм-аут. Это тот же движок, на котором построены пакеты Python и JavaScript выше.
См. [monty-pool README](https://github.com/pydantic/monty/tree/main/crates/monty-pool)
для использования.
Сам крейт `monty` предоставляет внутрипроцессный интерпретатор:```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`, поэтому хост может сериализовать разобранный код или приостановленный запуск в любом формате, который он уже использует.
## Ограничения памяти в воркерах
`max_memory` сеанса измеряется аллокатором воркера. Интерпретатор
сообщает о штатной `MemoryError` после пересечения мягкого предела; более высокий жёсткий
предел убивает и заменяет воркера, если одно выделение памяти перескакивает слишком далеко между контрольными точками.
См. [`limitations/resource_limits.md`](https://github.com/pydantic/monty/blob/HEAD/limitations/resource_limits.md) о том, как
превышение лимита проявляется для хоста, и `monty-alloc` — об аллокаторе, под управлением которого работают как
субпроцессы, так и WebAssembly-воркеры.
## Интеграция PydanticAI
Monty будет обеспечивать работу code-mode в
[Pydantic AI](https://github.com/pydantic/pydantic-ai). Вместо выполнения
последовательных вызовов инструментов LLM пишет Python-код, который вызывает ваши инструменты
как функции, и Monty безопасно выполняет его.```python test="skip"
import asyncio
import json
import logfire
from httpx import AsyncClient
from pydantic_ai import Agent, RunContext
from pydantic_ai.toolsets.code_mode import CodeModeToolset
from pydantic_ai.toolsets.function import FunctionToolset
from typing_extensions import TypedDict
logfire.configure()
logfire.instrument_pydantic_ai()
class LatLng(TypedDict):
lat: float
lng: float
weather_toolset: FunctionToolset[AsyncClient] = FunctionToolset()
@weather_toolset.tool
async def get_lat_lng(
ctx: RunContext[AsyncClient], location_description: str
) -> LatLng:
"""Get the latitude and longitude of a location."""
# NOTE: the response here will be random, and is not related to the location description.
r = await ctx.deps.get(
'https://demo-endpoints.pydantic.workers.dev/latlng',
params={'location': location_description},
)
r.raise_for_status()
return json.loads(r.content)
@weather_toolset.tool
async def get_temp(ctx: RunContext[AsyncClient], lat: float, lng: float) -> float:
"""Get the temp at a location."""
# NOTE: the responses here will be random, and are not related to the lat and lng.
r = await ctx.deps.get(
'https://demo-endpoints.pydantic.workers.dev/number',
params={'min': 10, 'max': 30},
)
r.raise_for_status()
return float(r.text)
@weather_toolset.tool
async def get_weather_description(
ctx: RunContext[AsyncClient], lat: float, lng: float
) -> str:
"""Get the weather description at a location."""
# NOTE: the responses here will be random, and are not related to the lat and lng.
r = await ctx.deps.get(
'https://demo-endpoints.pydantic.workers.dev/weather',
params={'lat': lat, 'lng': lng},
)
r.raise_for_status()
return r.text
agent = Agent(
'gateway/anthropic:claude-sonnet-4-5',
# toolsets=[weather_toolset],
toolsets=[CodeModeToolset(weather_toolset)],
deps_type=AsyncClient,
)
async def main():
async with AsyncClient() as client:
await agent.run('Compare the weather of London, Paris, and Tokyo.', deps=client)
if __name__ == '__main__':
asyncio.run(main())
Биндинги сообщества
- Go: gomonty — биндинги Go для интерпретатора Monty
- Dart/Flutter: dart_monty (github) (pub.dev) — биндинги Dart/Flutter для Monty
Альтернативы
Обычно, когда показываешь людям Monty, получаешь один из двух ответов:
- О боже, это решает столько проблем, я хочу это.
- Почему не X?
Где X — некая альтернативная технология. Как ни странно, эти ответы часто сочетаются, что говорит о том, что люди ещё не нашли альтернативу, которая им подходит, но не могут поверить, что созданию целой реализации Python с нуля действительно нет хорошей альтернативы.
Я постараюсь пройтись по самым очевидным альтернативам и объяснить, почему они не подходят для того, что мы хотели.
ПРИМЕЧАНИЕ: все эти технологии впечатляют и широко используются; этот комментарий об их ограничениях для нашего сценария использования не следует воспринимать как критику. Большинство этих решений не задумывались с целью предоставления песочницы для LLM, поэтому они не обязательно хороши в этом.
| Технология | Полнота языка | Безопасность | Задержка запуска | FOSS | Сложность настройки | Монтирование файлов | Снапшоты |
|---|---|---|---|---|---|---|---|
| Monty | частичная | строгая | 0.06ms | бесплатно / OSS | легко | легко | легко |
| Docker | полная | хорошая | 195ms | бесплатно / OSS | средне | легко | средне |
| Pyodide | полная | слабая | 2800ms | бесплатно / OSS | средне | легко | сложно |
| starlark-rust | очень ограниченная | хорошая | 1.7ms | бесплатно / OSS | легко | недоступно? | невозможно? |
| WASI / Wasmer | частичная, почти полная | строгая | 66ms | бесплатно * | средне | легко | средне |
| сервис песочницы | полная | строгая | 1033ms | платно | средне | сложно | средне |
| YOLO Python | полная | отсутствует | 0.1ms / 30ms | бесплатно / OSS | легко | легко / страшно | сложно |
См. ./scripts/startup_performance.py — скрипт, использованный для расчёта показателей производительности запуска.
Подробности по каждой строке ниже:
Monty
- Полнота языка: нет классов (пока), ограниченная стандартная библиотека, нет сторонних библиотек
- Безопасность: явно контролируемый доступ к файловой системе, сети и переменным окружения, строгие ограничения на время выполнения и использование памяти
- Задержка запуска: запускается за микросекунды
- Сложность настройки: просто
pip install pydantic-montyилиnpm install @pydantic/monty, загрузка ~4.5MB - Монтирование файлов: строго контролируется, см. #85
- Снапшоты: функциональность паузы и возобновления Monty с помощью
dump()иload()делает паузу, возобновление и форкинг выполнения тривиальными
Docker
- Полнота языка: полный CPython с любой библиотекой
- Безопасность: изоляция процессов и файловой системы, сетевые политики, но существуют побеги из контейнеров; ограничение памяти возможно
- Задержка запуска: накладные расходы на запуск контейнера (~195ms по замерам)
- Сложность настройки: требует демона Docker, образов контейнеров, оркестрации;
python:3.14-alpineвесит 50MB — docker нельзя установить через PyPI - Монтирование файлов: монтирование томов работает хорошо
- Снапшоты: возможно с решениями для долговечного выполнения, такими как Temporal, или созданием снимка образа и сохранением его как Docker-образа.
Pyodide
- Полнота языка: полный CPython, скомпилированный в WASM, доступны почти все библиотеки
- Безопасность: полагается на песочницу браузера/WASM — не предназначена для серверной изоляции; python-код может выполнять произвольный код в JS-рантайме, изоляцию обеспечивает только deno; ограничения памяти с deno обеспечить трудно или невозможно
- Задержка запуска: загрузка WASM-рантайма медленная (~2800ms холодный старт)
- Сложность настройки: нужно загрузить WASM-рантайм, обрабатывать асинхронную инициализацию; NPM-пакет pyodide ~12MB, deno ~50MB — Pyodide нельзя вызвать, используя только PyPI-пакеты
- Монтирование файлов: виртуальная файловая система через API браузера
- Снапшоты: предположительно возможно с решениями для долговечного выполнения, такими как Temporal, но сложно
starlark-rust
См. starlark-rust.
- Полнота языка: конфигурационный язык, а не Python — нет классов, исключений, async
- Безопасность: детерминированный и герметичный по замыслу
- Задержка запуска: работает встроенно в процесс, как Monty, отсюда впечатляющее время запуска
- Сложность настройки: можно использовать в Python через starlark-pyo3
- Монтирование файлов: файловых операций по замыслу нет, насколько я знаю?
- Снапшоты: невозможно, насколько я знаю?
WASI / Wasmer
Запуск Python в WebAssembly через Wasmer.
- Полнота языка: полный CPython; чистые Python-пакеты работают через монтирование, внешние пакеты с C-биндингами не работают
- Безопасность: в принципе, WebAssembly должен обеспечивать строгие гарантии песочницы.
- Задержка запуска: python-пакет wasmer не обновлялся 3 года, и я не смог найти документацию о вызове Python в wasmer из Python, поэтому я вызывал его через subprocess. Задержка запуска составила 66ms.
- Сложность настройки: загрузка wasmer — 100mb, пакет "python/python" — 50mb.
- FOSS: я отметил это как "бесплатно *", поскольку стоимость нулевая, но не всё, похоже, является открытым исходным кодом. По состоянию на 2026-02-10 пакет
python/pythonwasmer package не имеет readme, лицензии, ссылки на исходный код и указаний на то, как он собран; недавно загруженные версии показывают размер "0B", хотя загрузка составляет ~50MB — процесс сборки бинарника Python не является ясным и прозрачным. (Если я здесь ошибаюсь, пожалуйста, создайте issue, чтобы меня поправить) - Монтирование файлов: поддерживается
- Снапшоты: поддерживается через журналирование
Сервис песочницы
Сервисы вроде Daytona, E2B, Modal.
Здесь схожие проблемы: больше сложности в настройке, но ниже сетевая задержка, чем при развёртывании собственной песочницы на k8s.
- Полнота языка: полный CPython с любой библиотекой
- Безопасность: профессионально управляемая изоляция контейнеров
- Задержка запуска: сетевой обход и время запуска контейнера. У меня получилось ~1s холодного старта с Daytona EU из Лондона; Daytona рекламирует задержку ниже 90ms, предположительно для уже существующего контейнера, неясно, включает ли она сетевую задержку
- FOSS: оплата за выполнение или вычислительное время, некоторые реализации с открытым исходным кодом
- Сложность настройки: интеграция API, токены аутентификации — нормально для стартапов, но обычно не вариант для предприятий
- Монтирование файлов: загрузка/скачивание через вызовы API
- Снапшоты: возможно с решениями для долговечного выполнения, такими как Temporal; также сервисы предлагают некоторые решения для этого, думаю, основанные на docker-контейнерах
YOLO Python
Запуск Python напрямую через exec() (~0.1ms) или subprocess (~30ms).
- Полнота языка: полный CPython с любой библиотекой
- Безопасность: никакой — полный доступ к файловой системе, сети, переменным окружения, системным командам
- Задержка запуска: почти нулевая для
exec(), ~30ms для subprocess - Сложность настройки: никакой
- Монтирование файлов: прямой доступ к файловой системе (в этом и проблема)
- Снапшоты: возможно с решениями для долговечного выполнения, такими как Temporal
Часть стека Pydantic
Стек Pydantic — это всё, что нужно для выпуска AI-агентов продакшен-уровня:
- Pydantic AI — типобезопасный фреймворк для агентов
- Pydantic Logfire — наблюдаемость полного стека с приоритетом AI
- Logfire AI Gateway — единый прокси для LLM