
AI가 사용하도록 Rust로 작성된 최소형 보안 Python 인터프리터
실험적 - 이 프로젝트는 아직 개발 중이며, 본격적인 사용 준비가 되지 않았습니다.
AI 사용을 위해 Rust로 작성된 최소한의 안전한 Python 인터프리터입니다.
Monty는 LLM이 생성한 코드를 실행하기 위해 전체 컨테이너 기반 샌드박스를 사용할 때 발생하는 비용, 지연 시간, 복잡성 및 번거로움을 피합니다.
대신 에이전트에 내장된 LLM이 작성한 Python 코드를 안전하게 실행할 수 있으며, 시작 시간은 수백 밀리초가 아니라 한 자릿수 마이크로초 단위로 측정됩니다.
Monty가 할 수 있는 것:
sys, os, typing, asyncio, re, datetime, json, dataclasses (곧 지원 예정)Monty가 할 수 없는 것:
요컨대 Monty는 매우 제한적이며 한 가지 사용 사례를 위해 설계되었습니다:
에이전트가 작성한 코드를 실행하는 것.
이러한 방식을 원하는 이유에 대한 동기부여는 다음을 참조하세요:
아주 간단히 말하면, 위의 모든 접근 방식의 핵심 아이디어는 LLM이 전통적인 도구 호출에 의존하는 대신 Python(또는 Javascript) 코드를 작성하도록 요청받을 때 더 빠르고, 저렴하고, 더 안정적으로 작동할 수 있다는 것입니다. Monty는 샌드박스의 복잡성이나 호스트에서 코드를 직접 실행할 때의 위험 없이 이를 가능하게 합니다.
참고: Monty는 곧 Pydantic AI에서 codemode를 구현하는 데 사용될 예정입니다.
Monty는 Python, JavaScript/TypeScript 또는 Rust에서 호출할 수 있습니다.
설치하려면:```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 패키지는 Python 패키지가 사용하는 것과 동일한 Rust 워커 풀에 대한
네이티브(napi) 바인딩입니다 — 바인딩과 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' }, })
브라우저(또는 서브프로세스를 사용할 수 없는 모든 환경)의 경우 동일한 패키지는
`@pydantic/monty/wasm` 하위 경로 아래에 프로세스 내 WebAssembly 빌드를 제공합니다.
이 하위 경로에는 충돌 격리가 없습니다(샌드박스 충돌은 곧 호스트 충돌입니다).
### Rust
Rust에서 신뢰할 수 없는 코드를 실행하려면 아래의 프로세스 내 API보다
[`monty-pool`](https://crates.io/crates/monty-pool) 크레이트를 권장합니다.
`monty-pool`은 `monty` 워커 서브프로세스에서만 코드를 실행하므로 추가 보호를 제공합니다:
적대적 코드로 유발된 충돌(스택 오버플로, 할당자 중단)은 워커만 죽이며 —
풀은 그 죽음을 감지하고 워커를 교체합니다 — 그리고 상위 프로세스의 워치독은 하드 타임아웃을
초과하는 워커를 종료할 수 있습니다. 이는 위의 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`는 워커의 할당기(allocator)로 측정됩니다. 인터프리터는
소프트 한계를 초과하면 정상적인 `MemoryError`를 보고하며, 더 높은 하드
한계는 체크포인트 사이에 단일 할당이 너무 크게 점프하면 워커를 종료하고 교체합니다.
[`limitations/resource_limits.md`](https://github.com/pydantic/monty/blob/HEAD/limitations/resource_limits.md)에서 한도 초과가
호스트에 어떻게 표시되는지 확인하고, `monty-alloc`은 하위 프로세스와 WebAssembly 워커가 모두
실행 중 사용하는 할당기입니다.
## PydanticAI 통합
Monty는
[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())
일반적으로 사람들에게 Monty를 보여주면 두 가지 반응이 나옵니다:
여기서 X는 대체 기술입니다. 이상하게도 이 두 반응이 자주 결합되는데, 이는 사람들이 아직 자신에게 맞는 대안을 찾지 못했지만, 처음부터 완전한 Python 구현체를 만드는 것보다 정말로 나은 대안이 없다는 사실에 믿기 어려워한다는 것을 시사합니다.
가장 확실한 대안들과 그것들이 우리가 원하는 것에 적합하지 않은 이유를 살펴보겠습니다.
참고: 이 모든 기술은 인상적이고 널리 사용되며, 우리의 사용 사례에 대한 한계에 대한 이 논평은 비판으로 보아서는 안 됩니다. 대부분의 이러한 솔루션은 LLM 샌드박스를 제공할 목적으로 만들어진 것이 아니므로, 반드시 그 부분에 뛰어난 것은 아닙니다.
시작 성능 수치를 계산하는 데 사용된 스크립트는 ./scripts/startup_performance.py를 참조하세요.
아래 각 행에 대한 자세한 내용:
pip install pydantic-monty 또는 npm install @pydantic/monty를 실행하면 됩니다, 다운로드 약 4.5MBdump() 및 load()를 통한 일시 중지 및 재개 기능 덕분에 실행을 일시 중지, 재개, 포크하는 것이 매우 쉽습니다.python:3.14-alpine은 50MB입니다 - docker는 PyPI에서 설치할 수 없습니다.starlark-rust 참조.
Wasmer를 통해 WebAssembly에서 Python 실행.
python/python wasmer 패키지에는 readme, 라이선스, 소스 링크, 빌드 방식에 대한 표시가 전혀 없으며, 최근 업로드된 버전은 다운로드가 약 50MB임에도 크기가 "0B"로 표시됩니다 - Python 바이너리의 빌드 프로세스가 명확하고 투명하지 않습니다. (제가 여기서 틀렸다면, 저를 정정할 이슈를 생성해 주세요)k8s로 자체 샌드박스 설정을 구축하는 데는 비슷한 문제가 있고, 설정 복잡성은 더 크지만 네트워크 지연 시간은 더 낮습니다.
exec() (~0.1ms) 또는 subprocess (~30ms)를 통해 Python을 직접 실행.
exec()는 거의 0에 가깝고, subprocess는 약 30msPydantic 스택은 프로덕션 등급 AI 에이전트를 출시하는 데 필요한 모든 것입니다:
| 기술 | 언어 완전성 | 보안 | 시작 지연 시간 | FOSS | 설정 복잡성 | 파일 마운팅 | 스냅샷 |
|---|
| Monty | 부분적 | 엄격 | 0.06ms | 무료 / OSS | 쉬움 | 쉬움 | 쉬움 |
| Docker | 완전 | 좋음 | 195ms | 무료 / OSS | 중간 | 쉬움 | 중간 |
| Pyodide | 완전 | 취약 | 2800ms | 무료 / OSS | 중간 | 쉬움 | 어려움 |
| starlark-rust | 매우 제한적 | 좋음 | 1.7ms | 무료 / OSS | 쉬움 | 사용 불가? | 불가능? |
| WASI / Wasmer | 부분적, 거의 완전 | 엄격 | 66ms | 무료 * | 중간 | 쉬움 | 중간 |
| sandboxing service | 완전 | 엄격 | 1033ms | 유료 | 중간 | 어려움 | 중간 |
| YOLO Python | 완전 | 없음 | 0.1ms / 30ms | 무료 / OSS | 쉬움 | 쉬움 / 무서움 | 어려움 |