
Un interprete Python minimale e sicuro scritto in Rust per uso da parte dell'IA
Sperimentale - Questo progetto è ancora in fase di sviluppo e non è pronto per l'uso in produzione.
Un interprete Python minimale e sicuro scritto in Rust, pensato per l'uso da parte di IA.
Monty evita i costi, la latenza, la complessità e il fastidio generale di usare una sandbox completa basata su container per eseguire codice generato da LLM.
Invece, ti permette di eseguire in sicurezza codice Python scritto da un LLM integrato nel tuo agente, con tempi di avvio misurati in microsecondi a una cifra, non in centinaia di millisecondi.
Cosa Monty può fare:
sys, os, typing, asyncio, re, datetime, json, dataclasses (presto)Cosa Monty non può fare:
In breve, Monty è estremamente limitato e progettato per un caso d'uso:
Eseguire codice scritto da agenti.
Per capire perché potresti volerlo fare, vedi:
In parole molto semplici, l'idea alla base di tutto ciò è che gli LLM possono lavorare più velocemente, a costi inferiori e in modo più affidabile se viene chiesto loro di scrivere codice Python (o JavaScript) invece di affidarsi alle tradizionali chiamate a strumenti. Monty rende tutto questo possibile senza la complessità di una sandbox o il rischio di eseguire codice direttamente sull'host.
Nota: Monty verrà (presto) utilizzato per implementare codemode in Pydantic AI
Monty può essere richiamato da Python, JavaScript/TypeScript o Rust.
Per installare:```bash uv add pydantic-monty
(Oppure `pip install pydantic-monty` per i boomer)
`pydantic-monty` è un metapackage che abbina `pydantic-monty-client` (il
modulo `pydantic_monty`) con `pydantic-monty-runtime` (il binario del
worker `monty`). Installa `pydantic-monty-client` da solo se il binario proviene già da
un'altra parte.
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())
L'esecuzione avviene in un pool di subprocessi worker monty, quindi anche un errore di
memoria innescato da codice avversario (stack overflow, abort dell'allocatore) non può
mai mandare in crash il tuo processo — il worker muore, solleva MontyCrashedError e
viene sostituito. C'è anche un'API completamente sincrona:```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
Per installare:```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' }, })
Per i browser (o ovunque i sottoprocessi siano impossibili) lo stesso pacchetto
espone una build WebAssembly in-process tramite il sottopercorso `@pydantic/monty/wasm`
(nessun isolamento dai crash: un crash della sandbox lì è un crash dell'host).
### Rust
Per eseguire codice non fidato da Rust, consigliamo la
crate [`monty-pool`](https://crates.io/crates/monty-pool) piuttosto che l'API in-process qui sotto.
`monty-pool` esegue codice solo in sottoprocessi worker `monty`, il che offre protezioni aggiuntive:
un crash causato da codice avversario (overflow dello stack, abort dell'allocatore) uccide solo il worker —
il pool rileva la morte e sostituisce il worker — e un watchdog lato genitore può uccidere i worker
che superano un timeout rigido. È lo stesso motore su cui sono costruiti i pacchetti Python e JavaScript sopra.
Vedi il [README di monty-pool](https://github.com/pydantic/monty/tree/main/crates/monty-pool)
per l'utilizzo.
La crate `monty` stessa fornisce l'interprete in-process:```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));
Una sessione REPL può essere serializzata con dump() e ripristinata con Dump::load(). Il dump trasporta i metadati della sessione (nome dello script, stub di type-check) insieme allo stato dell'interprete, con una versione che la build di caricamento controlla:```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` e `RunProgress` non hanno un formato di dump proprio, ma entrambi implementano `serde::Serialize`/`Deserialize`, quindi un host può serializzare il codice parsato o un'esecuzione in pausa con qualsiasi formato già in uso.
## Limiti di memoria nei worker
Il `max_memory` di una sessione è misurato dall'allocator del worker. L'interprete
segnala un `MemoryError` controllato dopo aver superato il limite soft; un limite hard
più elevato termina e sostituisce il worker se un'allocazione compie un salto eccessivo tra i checkpoint.
Vedi [`limitations/resource_limits.md`](https://github.com/pydantic/monty/blob/HEAD/limitations/resource_limits.md) per come
il superamento di un limite si manifesta verso un host e `monty-alloc` per l'allocator
sotto cui girano sia i worker di sottoprocessi che quelli WebAssembly.
## Integrazione con PydanticAI
Monty alimenterà la modalità codice in
[Pydantic AI](https://github.com/pydantic/pydantic-ai). Invece di effettuare
chiamate di strumenti sequenziali, l'LLM scrive codice Python che chiama i tuoi strumenti
come funzioni e Monty lo esegue in modo sicuro.```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())
In genere ci sono due reazioni quando mostri Monty alle persone:
Dove X è una qualche tecnologia alternativa. Stranamente queste reazioni sono spesso combinate, suggerendo che le persone non hanno ancora trovato un'alternativa che funzioni per loro, ma sono increduli che non esista davvero una buona alternativa al dover creare da zero un'intera implementazione di Python.
NOTA: tutte queste tecnologie sono impressionanti e hanno usi diffusi; questo commento sulle loro limitazioni per il nostro caso d'uso non deve essere visto come una critica. La maggior parte di queste soluzioni non è stata concepita con l'obiettivo di fornire una sandbox per LLM, motivo per cui non sono necessariamente ottime in questo.
Vedi ./scripts/startup_performance.py per lo script usato per calcolare i valori delle prestazioni di avvio.
Dettagli su ogni riga qui di seguito:
pip install pydantic-monty o npm install @pydantic/monty, download di ~4.5MBdump() e load() rende banale mettere in pausa, riprendere e fare il fork dell'esecuzionepython:3.14-alpine è 50MB - docker non può essere installato da PyPIVedi starlark-rust.
Esecuzione di Python in WebAssembly tramite Wasmer.
python/python di wasmer non ha readme, né licenza, né link alla sorgente e nessuna indicazione su come è stato compilato; le versioni caricate di recente mostrano una dimensione di "0B" anche se il download è di ~50MB - il processo di build del binario Python non è chiaro e trasparente. (Se mi sbaglio, per favore apri un'issue per correggermi)Servizi come Daytona, E2B, Modal.
Ci sono sfide simili: maggiore complessità di configurazione ma una latenza di rete inferiore, se si configura la propria sandbox con k8s.
Eseguire Python direttamente tramite exec() (~0.1ms) o subprocess (~30ms).
exec(), ~30ms per subprocessIl Pydantic Stack è tutto ciò che serve per distribuire agenti AI di livello produzione:
| Tecnologia | Completezza del linguaggio | Sicurezza | Latenza di avvio | FOSS | Complessità di configurazione | Montaggio file | Snapshot |
|---|
| Monty | parziale | rigorosa | 0.06ms | gratuito / OSS | facile | facile | facile |
| Docker | completa | buona | 195ms | gratuito / OSS | intermedia | facile | intermedia |
| Pyodide | completa | scarsa | 2800ms | gratuito / OSS | intermedia | facile | difficile |
| starlark-rust | molto limitata | buona | 1.7ms | gratuito / OSS | facile | non disponibile? | impossibile? |
| WASI / Wasmer | parziale, quasi completa | rigorosa | 66ms | gratuito * | intermedia | facile | intermedia |
| servizio di sandboxing | completa | rigorosa | 1033ms | a pagamento | intermedia | difficile | intermedia |
| YOLO Python | completa | inesistente | 0.1ms / 30ms | gratuito / OSS | facile | facile / spaventoso | difficile |