
Ein minimaler, sicherer Python-Interpreter, geschrieben in Rust, zur Verwendung durch KI.
Experimentell – Dieses Projekt befindet sich noch in der Entwicklung und ist noch nicht bereit für den produktiven Einsatz.
Ein minimaler, sicherer Python-Interpreter, geschrieben in Rust, für die Nutzung durch KI.
Monty vermeidet die Kosten, Latenz, Komplexität und den allgemeinen Aufwand einer vollständigen containerbasierten Sandbox für die Ausführung von LLM-generiertem Code.
Stattdessen können Sie damit sicher Python-Code ausführen, der von einem LLM geschrieben wurde und in Ihrem Agenten eingebettet ist – mit Startzeiten im einstelligen Mikrosekundenbereich, nicht in Hunderten von Millisekunden.
Was Monty kann:
sys, os, typing, asyncio, re, datetime, json, dataclasses (bald)Was Monty nicht kann:
Kurz gesagt, Monty ist extrem begrenzt und für einen Anwendungsfall konzipiert:
Um von Agenten geschriebenen Code auszuführen.
Wenn Sie wissen möchten, warum Sie das tun sollten, siehe:
In einfachen Worten: Die Idee hinter all dem ist, dass LLMs schneller, kostengünstiger und zuverlässiger arbeiten können, wenn sie gebeten werden, Python- (oder Javascript-)Code zu schreiben, anstatt sich auf traditionelles Tool Calling zu verlassen. Monty macht das möglich, ohne die Komplexität einer Sandbox oder das Risiko, Code direkt auf dem Host auszuführen.
Hinweis: Monty wird (bald) zur Implementierung von codemode in Pydantic AI verwendet.
Monty kann aus Python, JavaScript/TypeScript oder Rust aufgerufen werden.
Zur Installation:```bash uv add pydantic-monty
(Oder `pip install pydantic-monty` für die Boomer)
`pydantic-monty` ist ein Metapaket, das `pydantic-monty-client` (das
`pydantic_monty`-Modul) mit `pydantic-monty-runtime` (die `monty`-Worker-
Binärdatei) bündelt. Installiere `pydantic-monty-client` allein, wenn die Binärdatei bereits von
anderswo stammt.
Verwendung:```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())
Die Ausführung erfolgt in einem Pool von monty-Worker-Subprozessen, sodass selbst ein Speicherfehler, der durch adversarischen Code ausgelöst wird (Stack-Overflow, Allocator-Abbruch), niemals deinen Prozess zum Absturz bringen kann – der Worker stirbt, wirft MontyCrashedError und wird ersetzt. Es gibt auch eine vollständig synchrone 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
Zur Installation:```bash
npm install @pydantic/monty
Das JS-Paket ist eine native (napi) Bindung über denselben Rust-Worker-Pool, den das Python-Paket verwendet — die Bindung und die monty-Worker-Binärdatei werden über plattformspezifische npm-Pakete ausgeliefert:```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' }, })
Für Browser (oder überall dort, wo Subprozesse unmöglich sind) stellt dasselbe Paket einen In-Process-WebAssembly-Build unter dem `@pydantic/monty/wasm`-Unterpfad bereit (keine Crash-Isolation: Ein Sandbox-Absturz ist dort ein Host-Absturz).
### Rust
Für die Ausführung von nicht vertrauenswürdigem Code aus Rust empfehlen wir die
[`monty-pool`](https://crates.io/crates/monty-pool) Crate anstelle der unten beschriebenen In-Process-API.
`monty-pool` führt Code nur in `monty`-Worker-Subprozessen aus, was zusätzliche Schutzmaßnahmen bietet:
Ein Absturz, der durch bösartigen Code ausgelöst wird (Stack-Überlauf, Allocator-Abbruch), tötet nur den Worker —
der Pool erkennt den Tod und ersetzt den Worker — und ein Watchdog auf der Elternseite kann Worker beenden,
die ein hartes Timeout überschreiten. Es ist dieselbe Engine, auf der die oben genannten Python- und JavaScript-Pakete
basieren. Siehe [monty-pool README](https://github.com/pydantic/monty/tree/main/crates/monty-pool)
zur Verwendung.
Die `monty`-Crate selbst stellt den In-Process-Interpreter bereit:```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));
Eine REPL-Sitzung kann mit dump() serialisiert und mit Dump::load() wiederhergestellt werden. Der Dump enthält die Sitzungsmetadaten (Skriptname, Typprüfungs-Stubs) zusammen mit dem Interpreterzustand, hinter einer Version, die der ladende Build prüft:```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` und `RunProgress` haben kein eigenes Dump-Format, aber beide implementieren `serde::Serialize`/`Deserialize`, sodass ein Host geparsten Code oder einen pausierten Lauf mit dem Format serialisieren kann, das er bereits verwendet.
## Speichergrenzen in Workern
Das `max_memory` einer Session wird vom Allocator des Workers gemessen. Der Interpreter
meldet einen sauberen `MemoryError`, nachdem die weiche Grenze überschritten wurde; eine höhere harte
Grenze beendet und ersetzt den Worker, wenn eine einzelne Allokation zwischen Checkpoints zu weit springt.
Siehe [`limitations/resource_limits.md`](https://github.com/pydantic/monty/blob/HEAD/limitations/resource_limits.md) dafür, wie
sich das Überschreiten einer Grenze für einen Host bemerkbar macht, und `monty-alloc` für den Allocator, unter dem sowohl
die Subprocess- als auch die WebAssembly-Worker laufen.
## PydanticAI-Integration
Monty wird den Code-Modus in
[Pydantic AI](https://github.com/pydantic/pydantic-ai) antreiben. Anstatt
sequenzielle Tool-Aufrufe durchzuführen, schreibt das LLM Python-Code, der Ihre Tools
als Funktionen aufruft, und Monty führt ihn sicher aus.```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())
Es gibt im Allgemeinen zwei Reaktionen, wenn man Menschen Monty zeigt:
Wobei X für eine alternative Technologie steht. Seltsamerweise werden diese Reaktionen oft kombiniert, was darauf hindeutet, dass die Leute noch keine Alternative gefunden haben, die für sie funktioniert, aber ungläubig sind, dass es wirklich keine gute Alternative dazu gibt, eine komplette Python-Implementierung von Grund auf neu zu erstellen.
Ich werde versuchen, die offensichtlichsten Alternativen durchzugehen und zu erklären, warum sie für das, was wir wollten, nicht das Richtige sind.
HINWEIS: Alle diese Technologien sind beeindruckend und haben weit verbreitete Anwendungen; diese Anmerkungen zu ihren Einschränkungen für unseren Anwendungsfall sollten nicht als Kritik verstanden werden. Die meisten dieser Lösungen wurden nicht mit dem Ziel entwickelt, eine LLM-Sandbox bereitzustellen, weshalb sie darin nicht unbedingt großartig sind.
Siehe ./scripts/startup_performance.py für das Skript, mit dem die Startleistungszahlen berechnet wurden.
Details zu jeder Zeile unten:
pip install pydantic-monty oder npm install @pydantic/monty, ~4.5MB Downloaddump() und load() macht es trivial, die Ausführung zu pausieren, fortzusetzen und zu forkenpython:3.14-alpine ist 50MB groß - Docker kann nicht von PyPI installiert werdenSiehe starlark-rust.
Python in WebAssembly über Wasmer ausführen.
python/python-wasmer-Paket keine README, keine Lizenz, keinen Quellcode-Link und keinen Hinweis darauf, wie es erstellt wird - die kürzlich hochgeladenen Versionen zeigen die Größe als „0B" an, obwohl der Download ~50MB beträgt - der Build-Prozess für die Python-Binärdatei ist nicht klar und transparent. (Falls ich hier falsch liege, erstellt bitte ein Issue, um mich zu korrigieren)Dienste wie Daytona, E2B, Modal.
Es gibt ähnliche Herausforderungen, mehr Einrichtungsaufwand, aber geringere Netzwerklatenz, wenn man sein eigenes Sandbox-Setup mit k8s aufsetzt.
Python direkt über exec() (~0.1ms) oder Unterprozess (~30ms) ausführen.
exec(), ~30ms für UnterprozesseDer Pydantic-Stack ist alles, was du brauchst, um produktionsreife KI-Agenten auszuliefern:
| Tech | Language completeness | Security | Start latency | FOSS | Setup complexity | File mounting | Snapshotting |
|---|
| Monty | teilweise | streng | 0.06ms | kostenlos / OSS | einfach | einfach | einfach |
| Docker | vollständig | gut | 195ms | kostenlos / OSS | mittel | einfach | mittel |
| Pyodide | vollständig | schlecht | 2800ms | kostenlos / OSS | mittel | einfach | schwierig |
| starlark-rust | sehr eingeschränkt | gut | 1.7ms | kostenlos / OSS | einfach | nicht verfügbar? | unmöglich? |
| WASI / Wasmer | teilweise, fast vollständig | streng | 66ms | kostenlos * | mittel | einfach | mittel |
| Sandboxing-Dienst | vollständig | streng | 1033ms | nicht kostenlos | mittel | schwierig | mittel |
| YOLO Python | vollständig | nicht vorhanden | 0.1ms / 30ms | kostenlos / OSS | einfach | einfach / beängstigend | schwierig |