
monty v0.0.20
Un intérprete de Python minimalista y seguro escrito en Rust para uso por IA
Monty
Un intérprete de Python mínimo y seguro escrito en Rust para uso por parte de la IA.
Experimental - Este proyecto aún está en desarrollo y no está listo para producción.
Un intérprete de Python mínimo y seguro escrito en Rust para uso por parte de la IA.
Monty evita el costo, la latencia, la complejidad y la molestia general de usar un sandbox basado en contenedores completos para ejecutar código generado por LLM.
En cambio, te permite ejecutar de forma segura código Python escrito por un LLM integrado en tu agente, con tiempos de inicio medidos en microsegundos de un solo dígito, no en cientos de milisegundos.
Lo que Monty puede hacer:
- Ejecutar un subconjunto razonable de código Python, suficiente para que tu agente exprese lo que quiere hacer
- Bloquear por completo el acceso al entorno host: el sistema de archivos, las variables de entorno y el acceso a la red se implementan mediante llamadas a funciones externas que el desarrollador puede controlar
- Llamar funciones en el host, solo las funciones a las que le des acceso
- Ejecutar verificación de tipos: Monty admite los type hints modernos de Python y viene con ty incluido en un solo binario para ejecutar la verificación de tipos
- Ser capturado como una instantánea de bytes en las llamadas a funciones externas, lo que significa que puedes almacenar el estado del intérprete en un archivo o base de datos, y reanudarlo más tarde
- Iniciar extremadamente rápido (<1μs desde el código hasta el resultado de la ejecución), y tiene un rendimiento en tiempo de ejecución similar a CPython (generalmente entre 5 veces más rápido y 5 veces más lento)
- Poder ser llamado desde Rust, Python o Javascript: como Monty no tiene dependencias de cpython, puedes usarlo en cualquier lugar donde puedas ejecutar Rust
- Controlar el uso de recursos: Monty puede rastrear el uso de memoria, la profundidad de la pila y el tiempo de ejecución, y cancelar la ejecución si supera los límites preestablecidos
- Recopilar stdout y stderr y devolverlos al llamador
- Ejecutar código asíncrono o síncrono en el host mediante código asíncrono o síncrono en el host
- Usar un pequeño subconjunto de la biblioteca estándar:
sys,os,typing,asyncio,re,datetime,json,dataclasses(pronto)
Lo que Monty no puede hacer:
- Usar el resto de la biblioteca estándar
- Usar bibliotecas de terceros (como Pydantic); el soporte para bibliotecas externas de Python no es un objetivo
- definir clases (el soporte debería llegar pronto)
- usar sentencias match (de nuevo, el soporte debería llegar pronto)
En resumen, Monty es extremadamente limitado y está diseñado para un caso de uso:
Ejecutar código escrito por agentes.
Para motivación sobre por qué podrías querer hacer esto, consulta:
- Codemode de Cloudflare
- Programmatic Tool Calling de Anthropic
- Code Execution with MCP de Anthropic
- Smol Agents de Hugging Face
En términos muy simples, la idea de todo lo anterior es que los LLM pueden trabajar más rápido, de forma más económica y más fiable si se les pide que escriban código Python (o Javascript), en lugar de depender de las llamadas a herramientas tradicionales. Monty hace eso posible sin la complejidad de un sandbox ni el riesgo de ejecutar código directamente en el host.
Nota: Monty se usará (pronto) para implementar codemode en Pydantic AI
Uso
Se puede llamar a Monty desde Python, JavaScript/TypeScript o Rust.
Python
Para instalarlo:```bash uv add pydantic-monty
(O `pip install pydantic-monty` para los boomers)
`pydantic-monty` es un metapaquete que combina `pydantic-monty-client` (el módulo `pydantic_monty`) con `pydantic-monty-runtime` (el binario del worker `monty`). Instala `pydantic-monty-client` por sí solo si el binario ya viene de otro lugar.
Uso:```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())
La ejecución se realiza en un grupo de subprocesos worker monty, por lo que incluso un error
de memoria provocado por código adversarial (desbordamiento de pila, aborto del asignador)
nunca puede hacer fallar tu proceso — el worker muere, lanza MontyCrashedError, y
es reemplazado. También hay una API totalmente síncrona:```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
Para instalar:```bash
npm install @pydantic/monty
El paquete JS es un binding nativo (napi) sobre el mismo pool de workers Rust que
usa el paquete Python — el binding y el binario worker monty se distribuyen mediante
paquetes npm específicos de cada plataforma:```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' }, })
Para navegadores (o en cualquier lugar donde los subprocesos sean imposibles), el mismo paquete
expone una compilación WebAssembly en proceso bajo la subruta `@pydantic/monty/wasm`
(sin aislamiento de fallos: un fallo del sandbox es un fallo del host allí).
### Rust
Para ejecutar código no confiable desde Rust, recomendamos la
crate [`monty-pool`](https://crates.io/crates/monty-pool) en lugar de la API en proceso que se muestra a continuación.
`monty-pool` solo ejecuta código en subprocesos de trabajo de `monty`, lo que ofrece protecciones adicionales:
un fallo provocado por código adversario (desbordamiento de pila, aborto del asignador) mata solo al worker —
el pool detecta la muerte y reemplaza al worker — y un watchdog en el lado del padre puede matar workers
que excedan un tiempo límite estricto. Es el mismo motor sobre el que se construyen los paquetes de Python y JavaScript
mencionados anteriormente. Consulta el [README de monty-pool](https://github.com/pydantic/monty/tree/main/crates/monty-pool)
para más información.
La propia crate `monty` proporciona el intérprete en proceso:```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));
Serialización
Una sesión de REPL puede serializarse con dump() y restaurarse con Dump::load(). El volcado transporta los metadatos de la sesión (nombre del script, stubs de verificación de tipos) junto con el estado del intérprete, bajo una versión que la compilación de carga comprueba:```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` y `RunProgress` no tienen un formato de volcado propio, pero ambos implementan `serde::Serialize`/`Deserialize`, por lo que un host puede serializar código analizado o una ejecución en pausa con el formato que ya utilice.
## Límites de memoria en los workers
El `max_memory` de una sesión lo mide el asignador del worker. El intérprete
informa de un `MemoryError` controlado al superar el límite blando; un límite
duro más alto mata y reemplaza al worker si una sola asignación salta demasiado entre puntos de control.
Consulta [`limitations/resource_limits.md`](https://github.com/pydantic/monty/blob/HEAD/limitations/resource_limits.md) para ver cómo
la superación de un límite se manifiesta a un host, y `monty-alloc` para el
asignador que usan tanto el subproceso como los workers de WebAssembly.
## Integración con PydanticAI
Monty impulsará el modo de código en
[Pydantic AI](https://github.com/pydantic/pydantic-ai). En lugar de realizar
llamadas secuenciales a herramientas, el LLM escribe código Python que llama a tus herramientas
como funciones y Monty lo ejecuta de forma segura.```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())
Bindings de la comunidad
- Go: gomonty - Bindings de Go para el intérprete de Monty
- Dart/Flutter: dart_monty (github) (pub.dev)- Bindings de Dart/Flutter para Monty
Alternativas
Generalmente hay dos respuestas cuando le muestras Monty a la gente:
- Dios mío, esto resuelve tantos problemas, lo quiero.
- ¿Por qué no X?
Donde X es alguna tecnología alternativa. Curiosamente, estas respuestas suelen combinarse, lo que sugiere que la gente aún no ha encontrado una alternativa que les funcione, pero se muestran incrédulos de que realmente no haya una buena alternativa a crear una implementación completa de Python desde cero.
Intentaré repasar las alternativas más obvias y por qué no son adecuadas para lo que queríamos.
NOTA: todas estas tecnologías son impresionantes y tienen usos generalizados; este comentario sobre sus limitaciones para nuestro caso de uso no debe considerarse una crítica. La mayoría de estas soluciones no fueron concebidas con el objetivo de proporcionar un sandbox para LLM, por lo que no son necesariamente excelentes en ello.
| Tecnología | Completitud del lenguaje | Seguridad | Latencia de inicio | FOSS | Complejidad de configuración | Montaje de archivos | Instantáneas |
|---|---|---|---|---|---|---|---|
| Monty | parcial | estricto | 0.06ms | gratis / OSS | fácil | fácil | fácil |
| Docker | completo | bueno | 195ms | gratis / OSS | intermedio | fácil | intermedio |
| Pyodide | completo | deficiente | 2800ms | gratis / OSS | intermedio | fácil | difícil |
| starlark-rust | muy limitado | bueno | 1.7ms | gratis / OSS | fácil | ¿no disponible? | ¿imposible? |
| WASI / Wasmer | parcial, casi completo | estricto | 66ms | gratis * | intermedio | fácil | intermedio |
| sandboxing service | completo | estricto | 1033ms | de pago | intermedio | difícil | intermedio |
| YOLO Python | completo | inexistente | 0.1ms / 30ms | gratis / OSS | fácil | fácil / aterrador | difícil |
Ver ./scripts/startup_performance.py para el script utilizado para calcular los números de rendimiento de inicio.
Detalles de cada fila a continuación:
Monty
- Completitud del lenguaje: Sin clases (aún), stdlib limitada, sin bibliotecas de terceros
- Seguridad: Acceso a sistema de archivos, red y entorno explícitamente controlado, límites estrictos en tiempo de ejecución y uso de memoria
- Latencia de inicio: Se inicia en microsegundos
- Complejidad de configuración: solo
pip install pydantic-montyonpm install @pydantic/monty, descarga de ~4.5MB - Montaje de archivos: Estrictamente controlado, ver #85
- Instantáneas: La funcionalidad de pausa y reanudación de Monty con
dump()yload()hace que sea trivial pausar, reanudar y bifurcar la ejecución
Docker
- Completitud del lenguaje: CPython completo con cualquier biblioteca
- Seguridad: Aislamiento de procesos y sistema de archivos, políticas de red, pero existen escapes de contenedor, la limitación de memoria es posible
- Latencia de inicio: Sobrecarga de inicio del contenedor (~195ms medidos)
- Complejidad de configuración: Requiere demonio de Docker, imágenes de contenedor, orquestación,
python:3.14-alpinepesa 50MB - Docker no se puede instalar desde PyPI - Montaje de archivos: Los montajes de volúmenes funcionan bien
- Instantáneas: Posible con soluciones de ejecución duradera como Temporal, o capturando una imagen y guardándola como imagen de Docker.
Pyodide
- Completitud del lenguaje: CPython completo compilado a WASM, casi todas las bibliotecas disponibles
- Seguridad: Depende del sandbox del navegador/WASM - no está diseñado para aislamiento en servidor, el código Python puede ejecutar código arbitrario en el runtime de JS, solo deno permite aislamiento, los límites de memoria son difíciles/imposibles de aplicar con deno
- Latencia de inicio: La carga del runtime WASM es lenta (~2800ms de arranque en frío)
- Complejidad de configuración: Necesita cargar el runtime WASM, manejar la inicialización asíncrona, el paquete pyodide de NPM es ~12MB, deno es ~50MB - Pyodide no se puede usar solo con paquetes PyPI
- Montaje de archivos: Sistema de archivos virtual mediante APIs del navegador
- Instantáneas: Posible con soluciones de ejecución duradera como Temporal presumiblemente, pero difícil
starlark-rust
Ver starlark-rust.
- Completitud del lenguaje: Lenguaje de configuración, no Python - sin clases, excepciones, async
- Seguridad: Determinista y hermético por diseño
- Latencia de inicio: se ejecuta embebido en el proceso como Monty, de ahí su impresionante tiempo de inicio
- Complejidad de configuración: Usable en Python mediante starlark-pyo3
- Montaje de archivos: Sin manejo de archivos por diseño, ¿que yo sepa?
- Instantáneas: ¿Imposible que yo sepa?
WASI / Wasmer
Ejecutar Python en WebAssembly mediante Wasmer.
- Completitud del lenguaje: CPython completo, los paquetes externos de Python puro funcionan mediante montaje, los paquetes externos con bindings en C no funcionan
- Seguridad: En principio, WebAssembly debería proporcionar fuertes garantías de sandboxing.
- Latencia de inicio: El paquete de Python wasmer no se ha actualizado en 3 años y no pude encontrar documentación sobre cómo llamar a Python en wasmer desde Python, así que lo llamé mediante subprocess. La latencia de inicio fue de 66ms.
- Complejidad de configuración: la descarga de wasmer es de 100mb, el paquete "python/python" es de 50mb.
- FOSS: Lo marqué como "gratis *" ya que el coste es cero pero no todo parece ser de código abierto. A fecha de 2026-02-10, el paquete
python/pythonde wasmer no tiene readme, no tiene licencia, no tiene enlace al código fuente ni indicación de cómo está construido; las versiones subidas recientemente muestran un tamaño de "0B" aunque la descarga es de ~50MB - el proceso de compilación del binario de Python no es claro ni transparente. (Si me equivoco aquí, por favor crea un issue para corregirme) - Montaje de archivos: Compatible
- Instantáneas: Compatible mediante journaling
sandboxing service
Servicios como Daytona, E2B, Modal.
Hay desafíos similares, más complejidad de configuración pero menor latencia de red para montar tu propio sandbox con k8s.
- Completitud del lenguaje: CPython completo con cualquier biblioteca
- Seguridad: Aislamiento de contenedores gestionado profesionalmente
- Latencia de inicio: Tiempo de ida y vuelta de red y tiempo de inicio del contenedor. Obtuve ~1s de arranque en frío con Daytona EU desde Londres; Daytona anuncia latencia inferior a 90ms, presumiblemente para un contenedor existente, no está claro si incluye latencia de red
- FOSS: Pago por ejecución o tiempo de cómputo, algunas implementaciones son de código abierto
- Complejidad de configuración: Integración de API, tokens de autenticación - bien para startups pero generalmente un obstáculo para empresas
- Montaje de archivos: Subir/descargar mediante llamadas de API
- Instantáneas: Posible con soluciones de ejecución duradera como Temporal, también los servicios ofrecen algunas soluciones para esto, creo que basadas en contenedores docker
YOLO Python
Ejecutar Python directamente mediante exec() (~0.1ms) o subprocess (~30ms).
- Completitud del lenguaje: CPython completo con cualquier biblioteca
- Seguridad: Ninguna - acceso completo al sistema de archivos, red, variables de entorno, comandos del sistema
- Latencia de inicio: Casi cero para
exec(), ~30ms para subprocess - Complejidad de configuración: Ninguna
- Montaje de archivos: Acceso directo al sistema de archivos (ese es el problema)
- Instantáneas: Posible con soluciones de ejecución duradera como Temporal
Parte del Stack de Pydantic
El Stack de Pydantic es todo lo que necesitas para lanzar agentes de IA de nivel de producción:
- Pydantic AI - Framework de agentes con seguridad de tipos
- Pydantic Logfire - Observabilidad full-stack centrada en IA
- Logfire AI Gateway - Proxy unificado de LLM