업데이트로 돌아가기
New releaseJul 25, 2026

monty v0.0.19

AI가 사용하도록 Rust로 작성된 최소형 보안 Python 인터프리터

공유

Monty

AI 사용을 위해 Rust로 작성된 최소한의 안전한 Python 인터프리터입니다.

CI Codspeed Coverage PyPI versions license Join Slack

실험적 - 이 프로젝트는 아직 개발 중이며, 본격적인 사용 준비가 되지 않았습니다.

AI 사용을 위해 Rust로 작성된 최소한의 안전한 Python 인터프리터입니다.

Monty는 LLM이 생성한 코드를 실행하기 위해 전체 컨테이너 기반 샌드박스를 사용할 때 발생하는 비용, 지연 시간, 복잡성 및 번거로움을 피합니다.

대신 에이전트에 내장된 LLM이 작성한 Python 코드를 안전하게 실행할 수 있으며, 시작 시간은 수백 밀리초가 아니라 한 자릿수 마이크로초 단위로 측정됩니다.

Monty가 할 수 있는 것:

  • 합리적인 수준의 Python 코드 하위 집합을 실행합니다 - 에이전트가 수행하려는 작업을 표현하기에 충분합니다
  • 호스트 환경에 대한 접근을 완전히 차단합니다: 파일 시스템, 환경 변수 및 네트워크 접근은 모두 개발자가 제어할 수 있는 외부 함수 호출을 통해 구현됩니다
  • 호스트에서 함수를 호출합니다 - 접근 권한을 부여한 함수만 호출할 수 있습니다
  • 타입 검사를 실행합니다 - monty는 최신 Python 타입 힌트를 완전히 지원하며, 타입 검사를 실행하기 위해 단일 바이너리에 ty가 포함되어 있습니다
  • 외부 함수 호출 시점에 바이트로 스냅샷을 생성할 수 있습니다. 즉, 인터프리터 상태를 파일이나 데이터베이스에 저장하고 나중에 재개할 수 있습니다
  • 시작이 매우 빠르며(<1μs 만에 코드에서 실행 결과까지 도달), 런타임 성능은 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는 곧 Pydantic AI에서 codemode를 구현하는 데 사용될 예정입니다.

사용법

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 패키지는 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())

Community Bindings

  • Go: gomonty - Monty 인터프리터용 Go 바인딩
  • Dart/Flutter: dart_monty (github) (pub.dev)- Monty용 Dart/Flutter 바인딩

대안

일반적으로 사람들에게 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무료 *중간쉬움중간
sandboxing service완전엄격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

  • 언어 완전성: WASM으로 컴파일된 완전한 CPython, 거의 모든 라이브러리 사용 가능
  • 보안: 브라우저/WASM 샌드박스에 의존 - 서버 측 격리를 위해 설계되지 않았으며, Python 코드는 JS 런타임에서 임의 코드를 실행할 수 있고, deno만 격리를 허용하며, deno로 메모리 제한을 적용하는 것은 어렵거나 불가능합니다.
  • 시작 지연 시간: WASM 런타임 로딩이 느림 (콜드 스타트 약 2800ms)
  • 설정 복잡성: WASM 런타임을 로드하고 비동기 초기화를 처리해야 하며, pyodide NPM 패키지는 약 12MB, deno는 약 50MB입니다 - PyPI 패키지만으로는 Pyodide를 호출할 수 없습니다.
  • 파일 마운팅: 브라우저 API를 통한 가상 파일시스템
  • 스냅샷: 아마도 Temporal과 같은 지속 실행 솔루션으로 가능하겠지만 어렵습니다.

starlark-rust

starlark-rust 참조.

  • 언어 완전성: Python이 아닌 구성 언어 - 클래스, 예외, async 없음
  • 보안: 설계상 결정적이고 밀폐적(hermetic)입니다.
  • 시작 지연 시간: Monty처럼 프로세스에 내장되어 실행되므로 시작 시간이 뛰어납니다.
  • 설정 복잡성: starlark-pyo3를 통해 Python에서 사용 가능
  • 파일 마운팅: 아는 한 설계상 파일 처리가 없습니다?
  • 스냅샷: 아는 한 불가능?

WASI / Wasmer

Wasmer를 통해 WebAssembly에서 Python 실행.

  • 언어 완전성: 완전한 CPython, 순수 Python 외부 패키지는 마운팅을 통해 작동하지만 C 바인딩이 있는 외부 패키지는 작동하지 않습니다.
  • 보안: 원칙적으로 WebAssembly는 강력한 샌드박싱 보장을 제공해야 합니다.
  • 시작 지연 시간: wasmer python 패키지는 3년 동안 업데이트되지 않았고, Python에서 wasmer의 Python을 호출하는 방법에 대한 문서를 찾을 수 없어서 subprocess로 호출했습니다. 시작 지연 시간은 66ms였습니다.
  • 설정 복잡성: wasmer 다운로드는 100MB, "python/python" 패키지는 50MB입니다.
  • FOSS: 비용이 0이므로 "무료 *"로 표시했지만 모든 것이 오픈소스인 것은 아닙니다. 2026-02-10 기준으로 python/python wasmer 패키지에는 readme, 라이선스, 소스 링크, 빌드 방식에 대한 표시가 전혀 없으며, 최근 업로드된 버전은 다운로드가 약 50MB임에도 크기가 "0B"로 표시됩니다 - Python 바이너리의 빌드 프로세스가 명확하고 투명하지 않습니다. (제가 여기서 틀렸다면, 저를 정정할 이슈를 생성해 주세요)
  • 파일 마운팅: 지원됨
  • 스냅샷: 저널링을 통해 지원됨

sandboxing service

Daytona, E2B, Modal 같은 서비스.

k8s로 자체 샌드박스 설정을 구축하는 데는 비슷한 문제가 있고, 설정 복잡성은 더 크지만 네트워크 지연 시간은 더 낮습니다.

  • 언어 완전성: 모든 라이브러리를 갖춘 완전한 CPython
  • 보안: 전문적으로 관리되는 컨테이너 격리
  • 시작 지연 시간: 네트워크 왕복 및 컨테이너 시작 시간. 런던에서 Daytona EU를 사용해 약 1초의 콜드 스타트 시간을 얻었습니다. Daytona는 90ms 미만의 지연 시간을 광고하는데, 아마도 기존 컨테이너에 대한 것이며 네트워크 지연이 포함되는지 여부는 명확하지 않습니다.
  • FOSS: 실행당 또는 컴퓨팅 시간당 지불, 일부 구현은 오픈소스
  • 설정 복잡성: API 통합, 인증 토큰 - 스타트업에는 적합하지만 일반적으로 기업에는 부적합합니다.
  • 파일 마운팅: API 호출을 통한 업로드/다운로드
  • 스냅샷: Temporal과 같은 지속 실행 솔루션으로 가능하며, 서비스들도 이에 대한 몇 가지 솔루션을 제공합니다. 제 생각에는 Docker 컨테이너 기반입니다.

YOLO Python

exec() (~0.1ms) 또는 subprocess (~30ms)를 통해 Python을 직접 실행.

  • 언어 완전성: 모든 라이브러리를 갖춘 완전한 CPython
  • 보안: 없음 - 전체 파일시스템, 네트워크, 환경 변수, 시스템 명령에 접근
  • 시작 지연 시간: exec()는 거의 0에 가깝고, subprocess는 약 30ms
  • 설정 복잡성: 없음
  • 파일 마운팅: 직접 파일시스템 접근 (그게 문제입니다)
  • 스냅샷: Temporal과 같은 지속 실행 솔루션으로 가능

Pydantic 스택의 일부

Pydantic 스택은 프로덕션 등급 AI 에이전트를 출시하는 데 필요한 모든 것입니다:

카테고리