Назад к обновлениям
New releaseJul 25, 2026

monty v0.0.19

Минимальный, безопасный интерпретатор Python, написанный на Rust для использования ИИ.

Поделиться

Monty

Минимальный, безопасный интерпретатор Python, написанный на Rust для использования ИИ.

CI Codspeed Coverage PyPI versions license Join Slack

Экспериментально — проект всё ещё находится в разработке и пока не готов к использованию в продакшене.

Минимальный, безопасный интерпретатор 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 крайне ограничен и предназначен для одного сценария использования:

Для запуска кода, написанного агентами.

О мотивации, почему вам может это понадобиться, см.:

Если очень просто, идея всего вышеперечисленного в том, что 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, получаешь один из двух ответов:

  1. О боже, это решает столько проблем, я хочу это.
  2. Почему не 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/python wasmer 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

Категории