본문으로 건너뛰기
5분 AI.
GITHUB TRENDING RUST MONTHLY #18 딥다이브 · 2026-10-23 분석

monty 딥다이브
— AI가 쓴 Python에 권한의 울타리를 세우는 법

샌드박스·라이브러리

빠른 판단

무엇
Python 일부 문법을 Rust VM에서 실행하고 호스트 도구·중단·저장·재개를 제어하는 임베디드 샌드박스
이럴 때
AI 생성 Python의 계산을 실행하되 파일·네트워크 등 외부 기능은 명시적으로 연결한 도구만 제공할 때
입력
지원 Python 코드·데이터와 호스트가 공개한 기능
결과
계산 결과 또는 저장·재개 가능한 실행 상태
도입 전 확인
일반 CPython 전체를 제공하는 것이 아니라 자체 실행기가 지원하는 문법·기능을 사용한다
AI가 작성한 코드를 내 서비스에서 실행하려면 무엇부터 설계해야 할까요? monty는 Rust로 Python 실행기를 만들고, 호스트 기능을 명시적으로 연결하는 방식으로 이 문제를 풉니다. 바이트코드 VM, 워커 프로세스 풀, 중단·재개, 파일 접근 권한을 하나의 흐름으로 읽어 봅니다. 저장소 pydantic/monty · 스타 8,761 · 포크 471 · MIT · 최신 릴리스 v1.3.0 · 2026-10-23 GitHub 기준. 소스 설명은 커밋 93227e8 기준입니다.
목차
  1. 프로젝트 한줄 요약
  2. 왜 주목받는가
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 분석
  5. 디렉토리 구조 해부
  6. 학습 포인트 (기술별)
  7. 하드웨어 / 시스템 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한줄 요약

Python 코드를 받지만, 실행 규칙과 바깥세상으로 통하는 문은 Rust가 정한다

monty는 AI가 쓴 Python 코드를 제한된 환경에서 실행하는 임베디드 샌드박스입니다. 기존 CPython에 금지 목록을 덧붙이는 대신, Python 문법의 일부를 자체 실행기로 처리합니다. Ruff로 문법을 읽고, 자체 바이트코드로 바꾼 뒤, Rust로 작성한 VM에서 실행합니다.

샌드박스가 바깥세상에서 할 수 있는 일은 호스트가 연결해 준 기능으로 결정됩니다. 파일 읽기와 도구 호출은 VM이 운영체제에 직접 요청하는 일이 아닙니다. 실행을 멈추고 요청 내용을 호스트에 전달한 뒤, 호스트가 보낸 결과로 이어 갑니다. 기본 시계와 난수 정책에는 예외가 있으므로 보안 경계는 4절과 11절에서 구체적으로 다룹니다.

용어 풀이
샌드박스와 호스트
샌드박스는 신뢰하지 않는 코드를 제한된 규칙 안에서 실행하는 공간입니다. 호스트는 그 공간을 만들고 도구와 데이터를 전달하는 바깥 프로그램입니다. 이 글에서 모델이 쓴 Python은 샌드박스 쪽, 우리가 작성한 가격 조회 함수는 호스트 쪽입니다.
한 문장 비유

실습실의 학생에게 건물 열쇠 대신, 조교에게 제출할 요청서를 준다

학생은 실습실 안에서 계산하고 메모할 수 있습니다. 창고의 재료가 필요하면 요청서를 내고 기다립니다. 조교는 허용된 재료인지 확인한 뒤 결과만 전달합니다. 학생에게 창고 열쇠나 교직원 계정을 넘길 필요가 없습니다.

monty의 외부 함수 호출도 같습니다. 다만 조교가 요청서를 보고 아무 문이나 열어 주면 울타리의 의미가 줄어듭니다. 호스트 함수의 입력 검증도 설계의 일부인 이유입니다.

1-1. 가장 작은 사용 흐름

다음은 Python 호스트 API 예제입니다. Monty는 워커 풀, checkout()은 상태가 유지되는 세션, feed_run()은 코드 조각 실행을 뜻합니다. 마지막 표현식의 값이 결과가 됩니다. API 근거는 docs/quickstart/python.md입니다.

from pydantic_monty import Monty

# 이 함수는 샌드박스 밖의 Python 호스트에서 실행된다.
def price(sku: str) -> int:
    return {'A1': 3500, 'B2': 12000}[sku]

with Monty() as pool:
    with pool.checkout() as session:
        result = session.feed_run(
            "price('A1') * quantity",
            inputs={'quantity': 3},
            external_lookup={'price': price},
        )
        print(result)  # 예상 출력: 10500

quantity는 미리 전달한 값입니다. price는 필요할 때 external_lookup에서 찾는 호스트 함수입니다. 샌드박스가 호스트의 전체 전역 변수나 모듈 목록을 받아 가는 구조가 아닙니다.

공부의 초점
실행기와 서비스 운영 경계를 함께 배울 수 있다

컴파일러만 읽으면 코드가 어떻게 실행되는지 배웁니다. monty에서는 그 코드를 어디에서 실행하고, 어떤 데이터가 경계를 넘으며, 워커가 죽으면 누가 복구하는지도 배울 수 있습니다. 처음에는 문법 전체보다 입력 → 실행 → 호스트 요청 → 결과 한 경로를 따라가는 것이 좋습니다.

2왜 주목받는가

코드를 잘 생성하는 문제 다음에는, 생성된 코드를 통제하는 문제가 온다

2-1. 에이전트의 여러 도구 호출을 코드 한 조각으로 묶는다

주문 목록을 읽고, 가격을 조회하고, 합계를 계산하는 에이전트를 생각해 봅시다. 모델이 작은 도구 호출마다 판단을 반복하는 대신, 반복문과 조건문을 가진 코드를 작성할 수 있습니다. README는 monty가 Pydantic AI의 Code Mode를 실행한다고 설명합니다. 계산과 제어 흐름은 샌드박스에 두고, 실제 업무 도구는 호스트가 제공하는 구성이 자연스럽습니다.

이런 수요와 Python의 익숙한 문법, Rust 실행기의 조합은 관심을 설명할 만한 요인입니다. 다만 트렌딩 순위를 만든 원인을 통계로 입증한 것은 아닙니다. GitHub Trending Rust Monthly #18은 2026-10-23의 선정 지표이고, 스타 8,761개는 그날의 누적 관심도입니다. 스타만으로 보안성이나 실제 도입 규모를 판단할 수는 없습니다.

2-2. 낮은 세션 시작 비용이라는 설계 목표

README는 이미 실행 중인 OSS 풀에서 새 샌드박스를 얻는 시간이 1ms 미만이라고 소개합니다. docs/alternatives.md의 상세 조건은 유휴 워커에서 세션을 빌려 1 + 1의 결과를 받는 20회 왕복의 중앙값 약 0.77ms이며, 2026-09-24의 1.0.0b3 측정입니다. 풀을 새로 만드는 비용과 Python 코드 전체의 처리 시간은 서로 다른 지표입니다.

빠른 시작 수치는 준비된 워커에서 새 세션을 만들고 짧은 계산 결과를 받는 비용과, 풀을 처음 만드는 비용을 구분해야 이해할 수 있습니다. 이 수치는 v1.3.0의 모든 환경을 보장하지 않습니다. 비교 문서도 macOS의 OSS 측정과 Linux 컨테이너의 Full Monty 측정을 직접 비교하기 어렵다고 밝힙니다.

2-3. 비슷한 문제를 푸는 선택지와 비교

다음 표는 docs/alternatives.md의 구분을 중심으로 정리한 것입니다. 속도 순위보다 필요한 Python 범위와 격리 경계를 비교하는 표로 읽으세요.

선택지얻는 것선택할 때 확인할 점
OSS Monty작은 워커, 명시적 호스트 연결, 실행 중단과 덤프Python 부분집합이며, 언어 수준 격리를 사용한다
Docker + CPython일반 Python과 서드파티 라이브러리, OS 기능을 이용한 격리이미지·데몬·자원 정책을 운영해야 한다
PyodideWebAssembly의 CPython과 지원 패키지 생태계런타임 로딩 비용과 호스트 연결 범위를 검토한다
WASI 실행 환경WebAssembly와 부여한 시스템 기능의 경계어떤 Python 실행기와 WASI 기능을 넣는지 별도로 정한다
원격 샌드박스 서비스실행 인프라를 애플리케이션 호스트에서 분리네트워크 왕복, 운영 비용, 공급자의 격리 모델을 검토한다
Full Monty같은 샌드박스를 WebSocket 서비스로 제공하는 상용 제품MIT 저장소와 상용 서버의 구현·보장 범위를 구분한다
비유

Docker가 각 팀에 별도 실습실을 마련하는 선택이라면, monty의 언어 경계는 실습실 안에서 쓸 수 있는 도구 자체를 설계하는 선택입니다. 두 접근을 함께 사용할 수도 있습니다. Python 패키지가 필요하다는 요구와 신뢰할 수 없는 코드를 제한한다는 요구는 별도로 적어 두어야 합니다.

2-4. 소스가 보여 주는 강점과 README 표현의 범위

crates/monty-pool/src/pool.rs는 최소 워커를 미리 만들고, 상한까지 필요할 때 늘리는 구조를 실제로 구현합니다. crates/monty/src/run_progress.rs는 외부 요청을 상태로 돌려주며, crates/monty/src/dump_format.rs는 중단 상태를 저장합니다. 빠른 시작·제어권 반환·상태 저장은 홍보 문구만 있는 기능이 아닙니다.

반면 README의 “파일시스템·환경 변수·네트워크가 샌드박스에 없다”는 문장은 호스트 연결 없는 상태의 권한을 설명합니다. 소스에는 os·pathlib 모듈과 마운트 구현이 존재합니다. 모듈 이름의 존재와 실제 파일 접근 권한의 존재를 구분하면 모순이 풀립니다.

3기술 스택 전체 지도

웹 서비스의 서버·화면 구분보다 실행기·바인딩·배포 계층 구분이 잘 맞는다

monty는 일반적인 웹 백엔드와 프론트엔드를 제공하는 제품이 아닙니다. 핵심은 라이브러리와 워커 실행 파일입니다. 아래 버전은 커밋 93227e8의 의존성 선언값이며, 범위 표현은 그대로 보존했습니다. 잠금 파일이 선택한 정확한 버전이나 독자의 설치 버전과 같다는 뜻은 아닙니다.

3-1. 실행기와 백엔드에 해당하는 계층

기술·선언값실제 파일역할
Rust 1.96 / edition 2024Cargo.tomlworkspace의 최소 Rust 버전과 기본 언어 에디션
ruff_python_parser 0.0.14Cargo.toml, crates/monty/Cargo.tomlPython 소스를 AST로 읽는 파서
ty_python_semantic 0.0.14Cargo.toml선택적 정적 타입 검사
salsa 0.28.2Cargo.toml타입 검사에 필요한 증분 계산 기반
serde 1.0Cargo.toml상태·값의 직렬화 인터페이스
minicbor-serde 0.7Cargo.toml세션 덤프의 CBOR 인코딩
prost =0.14.4Cargo.toml워커 통신의 Protocol Buffers, 정확한 버전 고정
tokio 1Cargo.toml비동기 풀·파이프·타이머 실행 기반
cap-std 4.0Cargo.toml열린 디렉터리를 기준으로 파일 접근 범위 제한
num-bigint 0.4Cargo.tomlPython의 큰 정수 표현
fancy-regex 0.17.0crates/monty/Cargo.tomlre 모듈의 정규식 엔진
logfire 0.12Cargo.toml선택적 관측 기능의 기반, CLI exporter는 기본 비활성

workspace에는 19개 멤버가 있고 기본 빌드 대상은 crates/monty-runtime입니다. 내부 주요 crate는 1.3.0 버전을 함께 사용합니다. 모든 작업을 한 crate에 넣는 대신 monty-types가 경계의 공통 타입을 제공하고, 실행기는 monty, 호스트는 monty-pool과 monty-fs로 나눕니다.

용어 풀이
crate와 workspace
crate는 Rust의 컴파일 단위입니다. workspace는 여러 crate가 버전·잠금 파일·설정을 공유하는 묶음입니다. 같은 저장소에 있어도 의존성을 연결하지 않으면 해당 기능이 자동으로 바이너리에 들어가지는 않습니다.

3-2. Python·Node.js 연결 계층

연결 방식버전·설정 근거읽을 점
Python 확장루트 Cargo.toml: pyo3 = "0.29.2", pyo3-async-runtimes = "0.29.0"Python 객체를 공통 경계 값으로 변환하고 Rust 풀에 연결
Python 최소 버전packages/pydantic-monty/pyproject.toml: >=3.10호스트 Python 버전과 샌드박스 목표인 Python 3.14는 다르다
Python 배포메타패키지가 pydantic-monty-client==1.3.0, pydantic-monty-runtime==1.3.0을 함께 고정모듈과 워커 실행 파일을 별도 배포하면서 버전을 맞춘다
Node.js 확장crates/monty-js/Cargo.toml: napi = "3.8", napi-derive = "3.0.0"N-API를 통해 Rust 풀을 호출
Node.js 최소 버전crates/monty-js/package.json: >= 20패키지 이름은 @pydantic/monty, 버전은 1.3.0

crates/monty-js/Cargo.toml만 에디션이 2021입니다. 설정 주석은 napi-derive 생성 코드의 호환성 때문이라고 설명합니다. “이 저장소는 Rust 2024”라는 설명에도 이런 예외가 있습니다. 바인딩 crate가 자체 Python 인터프리터를 호스트 안에 넣는 것으로 이해하면 안 됩니다.

3-3. 프론트엔드·브라우저 계층

핵심 패키지에 React나 Vue 화면은 없습니다. 대신 브라우저에서 같은 실행 모델을 쓰기 위한 WebAssembly 워커 경로가 있습니다. crates/monty-wasm-runtime/Cargo.toml은 cdylib과 wit-bindgen = "0.46"을 선언합니다. JS 빌드 명령은 wasm32-wasip1로 컴파일한 뒤 컴포넌트로 변환합니다.

선언값파일쓰임
TypeScript ^5.9.2crates/monty-js/package.json공통 세션 API와 워커 풀 구현
@bytecodealliance/jco ^1.25.2같은 파일의 개발 의존성WASM 컴포넌트의 JS 연동 빌드
preview2-shim ^0.19.0같은 파일의 실행 의존성WASI 컴포넌트 연동 지원
Vite ^6.3.5 / Vitest ^3.2.4같은 파일의 개발 의존성개발·테스트 도구, 샌드박스 엔진 자체와는 별개
Vite ^6.3.5examples/antigravity/package.json브라우저 예제의 개발 서버와 빌드

패키지의 exports는 Node 경로와 브라우저 경로를 나눕니다. crates/monty-js/ts/worker/pool.ts의 WorkerPool.create(), checkout()은 브라우저 워커나 Node worker thread를 빌려 줍니다. 네이티브의 자식 프로세스와 브라우저의 Web Worker는 같은 격리 장치가 아니므로 배포 환경별 차이를 읽어야 합니다.

3-4. 인프라·빌드·품질 검사

릴리스 프로필은 lto = "fat", codegen-units = 1, strip = true입니다. 최종 최적화와 배포 크기를 중시하는 설정이지만, 빌드 시간과 디버깅 편의에는 비용이 있습니다. Python client의 Maturin 선언은 >=1.9.4,<2.0, runtime은 >=1.14.1,<2.0이고, 루트 개발 도구는 >=1.15.0,<2.0으로 따로 적혀 있습니다.

기능을 어느 패키지에 넣는지는 빌드 속도뿐 아니라 신뢰해야 할 코드의 범위도 결정합니다. 이 커밋에는 Dockerfile이나 Docker Compose 설정이 없습니다. README가 언급하는 Full Monty 컨테이너를 이 OSS 저장소의 배포 파일로 혼동하지 마세요.

4아키텍처 심화 분석

한 줄의 Python이 어떤 경계를 거쳐 결과가 되는지 따라간다

4-1. 시스템 구조도: 계산은 워커, 권한은 호스트

[모델이 작성한 Python 문자열] | v +------------------------------------------------------------+ | 호스트 애플리케이션 | | Python: pydantic_monty / Node: @pydantic/monty | | | | 입력 값 / 허용 함수 / 외부 모듈 / 마운트 정책 | | | | | monty-pool: 워커 대여, 대기, 기한 감시, 장애 워커 폐기 | | | ^ | | | 요청 | 호스트 호출 | +-----------------|-------------------------|----------------+ | 길이 접두부 + protobuf | 결과를 회신 v | +------------------------------------------------------------+ | monty subprocess 워커 | | monty-proto::worker::Child::handle | | | | | Ruff 파서 -> Monty 표현 -> 준비 단계 -> 바이트코드 컴파일 | | | | | v | | VM + Heap + ResourceTracker | | | | | | v v | | 최종 결과 / 예외 함수·OS 요청에서 중단 | +------------------------------------------------------------+ [파일 요청을 받은 호스트] MountTable::handle_os_call | +-- ReadOnly / ReadWrite / OverlayMemory | +-- 열린 cap_std::fs::Dir 기준으로 경로 처리 [브라우저 경로] TypeScript WorkerPool -> Web Worker -> WASM 컴포넌트 네이티브 subprocess 대신 워커·컴포넌트 경계를 사용

위 그림에서 워커가 호스트 파일을 직접 여는 화살표가 없는 점을 보세요. crates/monty/Cargo.toml은 monty-fs에 의존하지 않습니다. 반대로 Python·Node 호스트 바인딩은 monty-types와 풀에 의존하고, 실행기 monty를 직접 연결하지 않습니다. 이 의존성 방향이 설계 의도를 코드 구조로 고정합니다.

4-2. 패턴 ① 단계별 변환 파이프라인

crates/monty/src/run.rs의 MontyRun::new()는 Executor::new()로 이어집니다. 여기서 입력 이름을 검사하고 소스 중첩을 점검한 뒤, compile_module_source()를 호출합니다. 이 함수의 핵심 순서는 다음과 같습니다.

// crates/monty/src/run.rs의 호출 순서만 요약
parse_with_interner(...)
    -> prepare_with_existing_names(...)
    -> Compiler::compile_module(...)
    -> interns.commit()
단계파일·식별자초보자의 질문
문법 읽기crates/monty/src/parse.rs의 parse_module_with_filename_id(), Ruff parse_module()문자열에서 함수 호출과 덧셈을 어떻게 구분할까?
이름·스코프 준비crates/monty/src/prepare.rs의 prepare_with_existing_names()이 변수는 지역 변수일까, 이전 세션의 전역 변수일까?
명령 생성crates/monty/src/bytecode/compiler.rs의 Compiler::compile_module()반복문과 조건문을 어떤 점프 명령으로 표현할까?
명령 실행crates/monty/src/bytecode/vm/mod.rs의 VM::run()스택에 어떤 값이 올라오고 어떤 연산이 값을 꺼낼까?
용어 풀이
AST와 바이트코드
AST는 코드의 문법 구조를 나무처럼 표현한 것입니다. 바이트코드는 실행기가 처리할 작은 명령들의 나열입니다. “가격에 수량을 곱하라”를 먼저 문장 구조로 이해하고, “값 두 개를 가져와 곱하라”는 작업 지시로 바꾼다고 생각하면 됩니다.

crates/monty/src/bytecode/op.rs의 Opcode는 #[repr(u8)]이며 명령 코드는 1바이트입니다. 인수는 뒤의 바이트에서 별도로 읽습니다. LoadSmallInt는 작은 정수를, LoadConst는 상수 테이블의 값을 가져옵니다. 다음 예시는 실제 덤프 출력이 아니라 스택 동작을 이해하기 위한 개념도입니다. 실제 컴파일러는 특수 명령과 최적화를 사용할 수 있습니다.

# 개념적인 x + 1의 실행 흐름
값 x를 스택에 넣기       스택: [41]
작은 정수 1 넣기         스택: [41, 1]
BinaryAdd               스택: [42]
결과 반환                결과: 42

여기서 배울 점은 “파서를 만들지 않고도 실행기를 만들 수 있다”입니다. Ruff가 Python 문법 해석을 맡아도, 지원할 문법의 범위·값의 표현·실행 규칙·권한은 monty가 정합니다. 외부 파서 도입이 전체 Python 호환성을 자동으로 주지는 않습니다.

4-3. 패턴 ② 바깥 작업을 실행 결과의 한 종류로 표현한다

crates/monty/src/run_progress.rs의 RunProgress를 보세요. 실행 결과가 성공과 실패 두 가지뿐이지 않습니다. FunctionCall, OsCall, ResolveFutures, NameLookup, Complete를 구분합니다. 외부 기능이 필요하다는 상황을 명시적인 상태로 반환합니다.

// crates/monty/src/run_progress.rs에서 변형 목록을 발췌
pub enum RunProgress {
    FunctionCall(FunctionCall),
    OsCall(OsCall),
    ResolveFutures(ResolveFutures),
    NameLookup(NameLookup),
    Complete(MontyObject),
}

FunctionCall에는 함수 이름, 인수, call_id, 호출 위치, 내부 실행 스냅샷이 담깁니다. 호스트는 요청을 검사하고 결과나 예외를 전달합니다. 기다리는 작업을 미래 값으로 표현할 수도 있습니다. 언어 내부의 호출 스택과 호스트의 비동기 작업을 분리할 수 있는 지점입니다.

외부 작업을 직접 실행하지 않고 요청 상태로 돌려주면, 호스트가 실행·거절·기록·재개 정책을 선택할 수 있습니다. 흔히 효과를 바깥에서 처리하는 설계와 연결해 이해할 수 있습니다. 다만 이 저장소가 특정 효과 처리 라이브러리를 사용한다는 뜻은 아닙니다.

Python에서는 feed_start()와 FunctionSnapshot으로 이 흐름을 관찰할 수 있습니다. 아래의 resume()은 실제 호스트 함수를 호출하지 않고, 우리가 결정한 반환값을 넣습니다. 근거는 docs/snapshots.md입니다.

from pydantic_monty import FunctionSnapshot, Monty, MontyComplete

with Monty() as pool:
    with pool.checkout() as session:
        paused = session.feed_start('greet(name) + "!"', inputs={'name': 'Ada'})
        assert isinstance(paused, FunctionSnapshot)
        print(paused.function_name, paused.args)
        # 예상 출력: greet ('Ada',)
        done = paused.resume({'return_value': '안녕하세요, Ada'})
        assert isinstance(done, MontyComplete)
        print(done.output)  # 예상 출력: 안녕하세요, Ada!

한 세션에는 활성 중단 지점이 하나만 있습니다. 스냅샷 객체는 실행 상태의 독립 복사본이 아닙니다. 여러 갈래로 실험하려면 dump()로 저장한 뒤 서로 다른 새 세션에 load_snapshot()해야 합니다. feed 사이의 유휴 세션은 load_session()으로 복원합니다.

저장 형식은 crates/monty/src/dump_format.rs에 있습니다. MONTY\0 매직 바이트, 리틀엔디언 버전, CBOR 본문을 사용하며 DUMP_VERSION = 13입니다. 현재 최소 지원 버전도 13입니다. 구조체의 필드 이름과 enum 변형 이름이 저장 계약이므로, 바이트코드 의미를 바꾸는 수정과 단순 필드 추가는 호환성 영향이 다릅니다.

4-4. 패턴 ③ 실행기 프로세스와 호스트를 분리한다

crates/monty-pool/src/pool.rs의 Pool::new()는 최소 워커를 미리 준비합니다. checkout_with()는 세션에 워커 하나를 전용으로 배정합니다. 유휴 워커가 없으면 상한까지 새로 만들고, 상한에 도달하면 반납을 기다립니다. PoolConfig::subprocess()의 기본값은 최소 1개, 최대는 사용 가능한 병렬성 수이며 조회 실패 시 4개입니다.

crates/monty-runtime/src/subprocess.rs의 serve()는 요청 프레임을 읽고 Child::handle()로 넘깁니다. 상태 전이의 본체는 crates/monty-proto/src/worker.rs에 있습니다. 한 요청 뒤에는 0개 이상의 출력 이벤트와 정확히 하나의 턴 종료 이벤트가 옵니다. stdout은 이 프로토콜 전용이며 진단은 stderr로 나갑니다.

부모 자식 Configure ----------------------------> 세션 설정 <------------------------- 설정 응답 Feed ---------------------------------> 새 코드 컴파일·실행 <------------------------- Print (0회 이상) <------------------------- 함수 요청: 중단 Resume -------------------------------> 반환값을 넣고 실행 재개 <------------------------- 완료 또는 다음 중단 자식이 예고 없이 종료 / 프레임 도중 EOF -> 부모가 장애로 분류하고 워커를 폐기 -> 풀은 이후 요청에 필요한 워커를 다시 확보

crates/monty-proto/src/frame.rs는 4바이트 리틀엔디언 길이 + protobuf 본문을 사용합니다. 프레임 상한은 256 MiB, 누적 디코딩 할당의 기본 예산은 그 4배인 1 GiB입니다. 이는 정상 작업의 권장 크기가 아니라 비정상 프레임을 위한 상한입니다. 현재 프로토콜 버전은 6이고 5~6을 지원합니다.

값은 메시지마다 하나의 평평한 그래프로 전달됩니다. crates/monty/src/object_bridge.rs의 GraphExporter는 같은 객체를 다시 만나면 기존 노드를 재사용합니다. 한 객체를 여러 경로에서 가리킬 때 중복 복사가 커지는 일을 줄입니다. 순환 참조는 별도 표현을 쓰므로 호스트 변환의 모든 객체 동일성이 그대로 유지된다고 일반화해서는 안 됩니다.

이 설계가 막으려는 장애에는 스택 오버플로와 메모리 할당 실패도 있습니다. 자식이 죽어도 호스트를 유지하는 장애 격리입니다. 워커는 기본적으로 애플리케이션의 OS 권한으로 실행되므로, 자식 프로세스라는 이유만으로 컨테이너와 같은 권한 격리를 얻지는 않습니다.

4-5. 패턴 ④ 경로 문자열보다 디렉터리 권한을 붙잡는다

crates/monty-fs/src/mount_table.rs의 MountRoot::open()은 마운트할 때 cap_std::fs::Dir을 열어 보관합니다. 이후 파일 작업은 이 디렉터리를 기준으로 수행됩니다. “정규화한 경로가 허용 경로로 시작하는가”만 확인한 뒤 일반 경로로 여는 방식과 차이가 있습니다.

비유

건물 주소가 허용 목록에 있는지만 확인하고 나중에 다시 찾아가면, 그 사이 안내 표지가 바뀔 수 있습니다. 디렉터리 핸들을 붙잡는 방식은 허가받은 방의 문을 실제로 열어 둔 채 그 방 안에서 작업하는 것에 가깝습니다. 검사와 사용 사이의 시간 틈을 줄이는 설계입니다.

MountTable::handle_os_call()은 요청을 해당 마운트로 보냅니다. crates/monty-fs/src/dispatch.rs는 이를 FsRequest로 바꾸고, 읽기 전용·읽기 쓰기·메모리 오버레이 정책에 따라 처리합니다. crates/monty-fs/src/path_security.rs의 resolve_virtual_path()는 가상 경로 정책을 담당하며, 호스트 파일 경계의 본체는 열린 Dir라고 파일 설명에 명시돼 있습니다.

경로 상한도 구체적입니다. 전체 길이 4096바이트, 구성요소 255바이트, 깊이 64개입니다. 절대 심볼릭 링크는 마운트 안쪽을 가리켜도 따라가지 않는 제약이 있습니다. 가상 경로는 Windows에서도 POSIX 형태이며, 마운트의 읽기 전용 정책을 우회할 중복 호스트 경로도 검사합니다.

경계의 의미
파일 권한을 닫아도 호스트 콜백이 다시 문을 열 수 있다

임의 경로를 읽는 함수나 임의 URL을 가져오는 함수를 허용하면, 그 함수가 호스트의 권한으로 실행됩니다. ReadOnly 마운트, 호스트 함수의 인수 검증, 워커 장애 격리는 서로 다른 책임입니다. 한 기능의 존재로 나머지 책임까지 해결됐다고 해석하지 마세요.

5디렉토리 구조 해부

큰 저장소를 기능 이름이 아니라 책임의 방향으로 나누어 읽기
monty/ +-- Cargo.toml / Cargo.lock Rust workspace와 버전 고정 +-- pyproject.toml / uv.lock Python 개발·문서 도구 +-- Makefile 여러 언어의 작업 명령 연결 +-- crates/ | +-- monty/ 파서 변환, 컴파일러, VM, 힙 | | +-- src/bytecode/ 명령 정의·컴파일·실행 | | +-- src/heap/ 주소가 안정적인 객체 저장소 | | +-- src/modules/ 표준 라이브러리의 지원 부분 | | +-- src/types/ Python 값의 동작 | | +-- test_cases/ Python 동작 비교용 사례 | | +-- tests/ Rust 통합 테스트 | +-- monty-types/ 공통 값·예외·OS 요청·자원 설정 | +-- monty-alloc/ 워커의 전역 할당기와 강제 메모리 상한 | +-- monty-fs/ 호스트 파일 마운트와 오버레이 | +-- monty-proto/ 스키마·프레이밍·워커 상태 머신 | +-- monty-pool/ 비동기 워커 풀과 세션 생명주기 | +-- monty-runtime/ monty CLI와 subprocess 진입점 | +-- monty-wasm-runtime/ WASM 컴포넌트 실행 진입점 | +-- monty-python/ PyO3 확장과 Python API·도우미 | +-- monty-js/ N-API 확장, TypeScript API·워커 | +-- monty-type-checking/ ty 기반 타입 검사 연결 | +-- monty-typeshed/ 지원 API의 타입 선언 | +-- monty-macros/ 반복 구현을 생성하는 매크로 | +-- monty-datatest/ Monty·CPython 결과 비교 도구 | +-- monty-fixture/ 테스트 사례 헤더 해석 | +-- monty-doctest/ 문서 테스트용 crate | +-- monty-bench/ 성능 비교용 작업 | +-- monty-apidoc/ Rust API 문서 생성 | +-- fuzz/ 비정상 입력 탐색 +-- packages/pydantic-monty/ Python client·runtime 메타패키지 +-- docs/ 개념·빠른 시작·API 문서 | +-- limitations/ CPython과 다른 동작의 상세 기록 +-- limitations -> docs/limitations 디렉터리의 심볼릭 링크 +-- examples/ SQL·스프레드시트·브라우저 등 예제 +-- scripts/ 검사·릴리스·측정 보조 도구 +-- .github/workflows/ CI와 벤치마크 자동화

5-1. 처음 읽을 파일은 다섯 묶음이면 충분하다

순서읽을 파일확인할 것
1. 사용자 계약docs/quickstart/python.md, docs/security.md누가 코드를 실행하고 어떤 권한을 연결하는가
2. 실행 진입점crates/monty/src/run.rs, crates/monty/src/repl.rs한 번 실행과 상태 유지 세션이 어떻게 다른가
3. 실행 제어crates/monty/src/run_progress.rs, crates/monty/src/dump_format.rs어디에서 멈추고 무엇을 저장하는가
4. 운영 경계crates/monty-pool/src/pool.rs, crates/monty-proto/src/frame.rs동시 요청과 손상된 입력을 어떻게 다루는가
5. 실제 권한crates/monty-fs/src/mount_table.rs, crates/monty-fs/src/path_security.rs가상 경로와 실제 파일을 어떤 기준으로 연결하는가

crates/monty/src/repl.rs의 MontyRepl은 힙과 전역 변수, 이름 테이블, 소스, 난수 상태를 유지합니다. 다음 코드를 실행할 때 과거 코드를 다시 실행하지 않습니다. 소스도 보관하는 이유는 이전 feed에서 정의한 함수의 오류 위치를 나중에 표시해야 하기 때문입니다. 세션 상태에는 결과값 이상의 정보가 필요합니다.

5-2. 코드 규모를 읽는 법

아래는 커밋 93227e8에서 확장자별 파일을 대상으로 센 물리적 줄 수입니다. 빈 줄·주석·테스트를 포함하고, .git과 심볼릭 링크는 제외했습니다. 따라서 limitations 링크를 중복 집계하지 않습니다. 생산 코드의 논리적 줄 수나 품질 지표는 아닙니다.

종류파일 수줄 수해석
Rust (.rs)417223,259테스트·프로토콜 생성물·비교용 oracle 포함
Python (.py)82493,936호스트 API·사례·예제·도구 포함
TypeScript (.ts)8917,415세션·워커·테스트·생성 선언 포함
Python 선언 (.pyi)5712,086vendor typeshed와 맞춤 선언 포함
JS 모듈 (.mjs)3270빌드·패키징 보조 코드
protobuf / WIT각 11,116 / 529프로세스·컴포넌트 인터페이스 정의

vendor·generated·oracle·component라는 경로 구성요소를 제외하면 Rust는 413개·219,885줄, TypeScript는 87개·16,773줄, .pyi는 19개·3,017줄입니다. 이 규칙도 모든 생성 코드를 완벽히 분류하는 기준은 아닙니다. 집계 범위를 공개해야 숫자를 비교할 수 있습니다.

Rust는 crates/monty에 143,724줄, monty-proto에 15,691줄, monty-pool에 14,834줄이 있습니다. 모두 해당 crate의 테스트 등을 포함한 값입니다. crates/monty/test_cases의 Python 사례 파일은 703개입니다. 이것을 자동으로 703개의 독립 테스트나 100% 호환성 증명으로 읽어서는 안 됩니다.

탐색 요령
동작 하나와 관련 파일 세 개를 연결한다

예를 들어 Path.read_text()라면 언어 모듈, OsFunctionCall 경계 타입, 마운트 처리 함수를 연결하세요. 파일 크기 순서로 읽는 것보다 “이 요청은 어디서 만들어지고 누가 실행하는가”라는 질문이 구조를 더 잘 드러냅니다.

6학습 포인트 (기술별)

각 기술에서 한 가지 질문을 고르고, 소스와 작은 실험을 연결한다

6-1. Rust: 소유권을 경계의 규칙으로 사용하기

MontyRun::start(self, ...)는 실행기를 소비하고, RunProgress가 다음 상태를 가집니다. 시작 전의 실행기를 그대로 두고 실행 중인 복사본도 함께 만지는 실수를 줄이는 API입니다. FunctionCall::resume() 계열을 읽으며 “누가 지금 실행 상태를 소유하는가”를 표시해 보세요.

힙에서는 Rust의 자동 메모리 관리만으로 Python의 객체 수명을 해결할 수 없습니다. Python 리스트끼리 서로 가리킬 수 있기 때문입니다. crates/monty/src/heap/mod.rs의 참조 수와 collect_cycles()는 언어가 관리하는 객체 그래프를 별도로 다룹니다. 호스트 프로그램의 Rust 값 수명과 샌드박스 객체의 수명을 구분하는 연습이 됩니다.

용어 풀이
참조 수와 순환 참조 수집
객체를 가리키는 참조 수가 0이면 해제할 수 있습니다. 하지만 A와 B가 서로 가리키면 바깥에서 둘 다 버려도 참조 수가 남습니다. 순환 참조 수집은 이렇게 서로만 붙잡고 있는 객체 묶음을 찾아 정리하는 작업입니다.

루트 Cargo.toml은 unsafe_code = "deny"를 선언하지만, crates/monty/src/heap/mod.rs와 crates/monty-alloc/src/lib.rs에는 근거를 명시한 예외가 있습니다. “Rust로 작성했다”와 “unsafe 코드가 전혀 없다”는 같은 말이 아닙니다. 안전한 외부 API가 의존하는 내부 불변식과 이를 검사하는 Miri·테스트까지 함께 읽어야 합니다.

crates/monty/src/heap/stable_heap.rs는 페이지당 256개 슬롯을 둡니다. 페이지 목록이 늘어나도 각 페이지의 실제 저장 위치가 바뀌지 않는 성질을 사용합니다. HeapReader::with()의 범위 제한과 함께, 주소 안정성과 대여 규칙이 어떻게 연결되는지 읽어 보세요. 저수준 힙을 수정하기보다 불변식을 먼저 문장으로 설명하는 과제가 적합합니다.

실습 아이디어: crates/monty/test_cases/refcount__cycle_mutual_reference.py를 읽고 객체와 참조를 화살표로 그리세요. 참조 수만으로 회수할 수 없는 시점을 표시하고, CcColor의 Black·Purple·Gray·White가 각각 어떤 단계인지 한국어로 적어 봅니다.

6-2. 컴파일러: 이름을 문자열에서 슬롯으로 바꾸는 이유

crates/monty/src/prepare.rs와 crates/monty/src/name_map.rs에서 이름 해석을, crates/monty/src/bytecode/compiler.rs의 compile_name()과 compile_store()에서 접근 명령 생성을 읽어 보세요. 매번 변수 이름 전체를 찾는 대신, 이름의 소속과 저장 위치를 준비 단계에서 정하는 흐름을 배울 수 있습니다.

실습 아이디어: 전역 x, 함수 인수 x, 바깥 함수의 x를 읽는 내부 함수를 각각 만들어 “같은 철자의 이름이 왜 다른 저장소를 가리키는가”를 설명합니다. crates/monty/test_cases/closure__param_shadows_outer.py가 읽기 출발점입니다. 문법을 아는 것과 스코프를 구현하는 것은 별도 문제입니다.

6-3. 비동기 시스템: 대기 중인 작업도 자원이다

crates/monty-pool/src/pool.rs는 짧고 await가 없는 구간에 동기 Mutex를 사용하고, 자리가 생기는 알림에는 Tokio Notify를 사용합니다. 비동기 함수 안이라고 모든 잠금을 비동기 Mutex로 바꾸는 방식은 아닙니다. 잠금 안에서 무엇을 하며 얼마나 오래 잡는지를 먼저 봅니다.

대여 대기 제한인 checkout_timeout, 워커 응답 제한인 request_timeout, VM 실행 시간 제한은 서로 다른 시계를 봅니다. Checkout의 feed·resume 경로를 읽으면서 “지금은 CPU 계산 중인가, 자리 대기 중인가, 호스트 작업 대기 중인가”를 구분해 보세요.

실습 아이디어: 워커 수를 하나로 가정하고 두 세션의 요청·대여·반납을 시간축으로 그립니다. 첫 세션이 실행을 끝내도 checkout을 계속 보유하면 두 번째 세션이 왜 기다리는지 설명하세요. 워커 개수와 함수 호출 개수는 일치하지 않습니다.

6-4. 타입 시스템: 실행 가능한 세계를 타입 선언으로 설명하기

타입 검사는 기본적으로 꺼져 있으며 type_check=True로 켭니다. crates/monty-type-checking/src/type_check.rs의 TypeChecker::run_with()는 메모리 기반 검사 환경에 소스와 선언을 배치하고 ty를 사용합니다. monty-typeshed는 monty가 지원하는 API에 맞춘 선언을 제공합니다.

from pydantic_monty import Monty

with Monty() as pool:
    with pool.checkout(
        type_check=True,
        type_check_stubs='def double(x: int) -> int: ...',
    ) as session:
        print(session.feed_run(
            'double(21)',
            external_lookup={'double': lambda x: x * 2},
        ))  # 예상 출력: 42

이 예제에서 선언은 검사기에 “이 함수가 있다”고 알려 주고, external_lookup는 실제 구현을 제공합니다. 선언만 넣으면 함수가 생기는 것이 아닙니다. 실습 아이디어: 인수를 문자열로 바꾸었을 때의 타입 진단과, 구현을 뺐을 때의 이름 해석 실패를 비교해 보세요.

타입 검사는 보안 정책이나 실행 성공의 증명이 아닙니다. docs/type-checking.md는 일부 upstream 선언이 실제 지원 범위보다 넓을 수 있다고 밝힙니다. 호스트 함수의 선언이 실제 동작과 일치하는지도 호스트 작성자의 책임입니다.

6-5. 프로토콜: 신뢰할 수 없는 자식의 응답도 검사한다

언어 경계가 안전하다는 목표와 통신 상대를 신뢰한다는 가정은 별개입니다. crates/monty-proto/src/frame.rs와 crates/monty-proto/src/decode_budget.rs는 프레임 크기와 디코딩 중 할당을 제한합니다. crates/monty-proto/tests/differential.rs는 수작업 wire 구현과 prost 생성 oracle의 바이트 호환성을 비교합니다.

실습 아이디어: 빈 프레임, 상한을 넘는 길이, 본문 도중 EOF, 정상 protobuf지만 잘못된 값이라는 네 경우를 표로 나눕니다. 각각 프레이밍 오류인지 값 변환 오류인지, 스트림을 계속 사용할 수 있는지 소스에서 찾아 적습니다. 임의 데이터를 실제 서비스에 보내는 실험은 필요하지 않습니다.

6-6. 테스트 설계: 결과뿐 아니라 의미를 비교한다

crates/monty-datatest/src/main.rs는 Python 사례를 Monty와 CPython에서 실행해 결과 차이를 찾는 도구입니다. fixture 헤더로 기대 실패·비동기 모드·마운트 같은 차이를 표현합니다. 의도적인 CPython 차이는 docs/limitations에 기록합니다. 지원 범위와 호환성 검증을 함께 유지하는 방식입니다.

호환성을 배우려면 성공한 예제보다 두 구현의 결과가 갈라지는 경계 조건을 읽는 편이 효과적입니다. 예를 들어 존재하지 않는 파일의 자기 자신으로 이름 바꾸기는 “아무 변화 없음”과 “파일 없음 오류” 중 어느 쪽이어야 할까요? 최신 커밋이 그 질문을 좋은 학습 사례로 제공합니다.

6-7. 자원 계측: 결과가 커지기 전에 예산을 확인하기

crates/monty/src/bytecode/vm/mod.rs의 VM::run()은 CHECK_INTERVAL = u8::MAX인 카운트다운으로 주기적인 dispatch_checkpoint()를 호출합니다. 명령마다 시계를 읽는 비용을 줄이면서 메모리·시간과 출력·GC 점검을 배치하는 선택입니다. 긴 네이티브 연산은 내부에서도 예산을 점검해야 하므로 VM 루프 한 곳만 검사해서 끝낼 수는 없습니다.

crates/monty-types/src/resource.rs의 check_large_result()는 예상 결과가 100,000바이트를 넘는 연산에서 사전 예산 점검에 쓰입니다. check_growth()는 컨테이너 버퍼가 커지는 시점을 다룹니다. 이미 크게 할당한 뒤 확인하는 방법과, 입력에서 증폭 크기를 예측해 거절하는 방법의 차이를 배울 수 있습니다.

협력적인 점검을 놓쳤을 때의 마지막 방어는 별도 계층입니다. LimitedAllocator는 강제 할당 상한을 넘으면 네이티브 워커를 종료하고, 부모는 종료 코드 65를 메모리 오류로 분류할 수 있습니다. Rust에서 실행기를 직접 연결한다면 max_memory 설정뿐 아니라 이 전역 할당기의 설치와 활성화 조건도 확인해야 합니다.

실습 아이디어: 문자열 반복, 리스트의 점진적 증가, 호스트 호출 대기를 각각 사전 크기 점검·주기 점검·호스트 정책 중 어디에서 제어해야 할지 분류합니다. 제한에 도달한 뒤 해당 세션을 버리는 정리 절차까지 그림에 넣으세요.

7하드웨어 / 시스템 요구사항

실행 요구사항, 개발 도구, 자원 예산을 나누어 생각한다

7-1. 무엇을 준비해야 하는가

사용 목적확인된 조건주의할 구분
Python 패키지 사용Python >=3.10, 플랫폼에 맞는 client와 runtime사전 빌드 wheel을 쓰면 Rust 도구가 필수는 아니다
Node 패키지 사용Node.js >=20네이티브와 WASM 진입점을 구분한다
Rust 소스 빌드Rust 1.96 이상, 대상 플랫폼의 링커·빌드 도구최소 버전 선언과 최신 stable CI를 구분한다
Python 바인딩 개발Rust, Python, uv, MaturinMakefile은 runtime과 bindings를 따로 빌드한다
브라우저 빌드wasm32-wasip1 target, JS 빌드 도구, 컴포넌트 변환최종 실행에는 해당 WebAssembly·Worker 지원 환경이 필요하다
OS 지원 확인Linux·macOS·Windows CI, npm의 x64·일부 arm64 대상 선언모든 CPU·OS 조합의 배포물을 보장한다는 뜻은 아니다
GPU·외부 서비스코드 실행 자체에는 GPU·모델 서버·데이터베이스가 필요하지 않다LLM 호출은 monty 바깥의 애플리케이션 책임이다

crates/monty-js/package.json의 네이티브 대상은 Windows x86_64, macOS x86_64·aarch64, Linux GNU x86_64·aarch64입니다. 이 목록을 Linux musl이나 Windows arm64까지 넓혀 읽으면 안 됩니다. 브라우저의 WASM 경로에서는 호스트 디렉터리 마운트가 지원되지 않습니다.

공식적인 최소 CPU 코어 수나 RAM 용량은 이 커밋의 설정에서 확인되지 않습니다. “메모리 2GB면 충분하다” 같은 하한을 임의로 정할 수 없습니다. 소스 읽기는 일반 개발 환경에서 시작하고, 실행 서버 용량은 동시 세션 수·세션별 데이터·타입 검사·호스트 콜백을 기준으로 잡아야 합니다.

7-2. 메모리 제한과 실제 프로세스 메모리는 다르다

max_memory는 워커의 전역 할당기를 통해 요청한 바이트를 기준으로 합니다. 프로세스 RSS 전체의 상한은 아닙니다. 스레드 스택, 바이너리 매핑, 할당기 부가 비용 등을 따로 고려해야 합니다. subprocess 워커의 스택 예약 크기는 crates/monty-runtime/src/subprocess.rs의 16 MiB이며, 예약했다고 전부 실제 메모리에 상주하는 것도 아닙니다.

crates/monty-types/src/resource.rs의 memory_limit_with_headroom()은 VM 제한에 일반 실행 4 MiB, 타입 검사 사용 시 32 MiB의 여유를 더해 강제 할당 상한을 계산합니다. 여기에 워커 baseline도 더해집니다. 마운트의 메모리 예산은 별도이며 기본값은 마운트당 50,000,000바이트입니다.

서버 메모리 검토의 출발점 워커 수 × (워커 기본 비용 + 세션 예산 + 실행 여유) + 호스트 프로그램 + 마운트·콜백·통신 버퍼 + OS·할당기·스택 여유 위 식은 용량 계획용 개념식이다. max_memory 값을 프로세스 RSS 상한으로 대입하면 안 된다.

7-3. 기본값부터 확인할 자원 설정

설정기본값·단위제한하는 것
max_memory미설정, 바이트할당기 기준 세션 메모리
max_feed_duration_secs미설정, 초feed 하나의 VM 실행 시간 합계
max_turn_duration_secs미설정, 초호스트 응답 사이의 VM 실행 구간
max_recursion_depth1,000Python 호출 깊이
max_suspensions1,000호스트가 처리할 세션의 중단 횟수
gc_interval100,000회GC 추적 할당을 기준으로 한 순환 수집 주기
max_total_sleep_secs미설정, 초기본 system 대기 모드의 누적 대기 시간

메모리와 실행 시간 제한은 직접 설정해야 합니다. 중단 횟수와 재귀 깊이는 미설정이어도 기본 상한이 있습니다. max_suspensions는 인터프리터가 혼자 강제하는 제한이 아니라 호스트에서 집행합니다. Rust로 직접 VM을 연결한다면 풀과 같은 책임을 누가 맡을지 정해야 합니다.

VM의 실행 시간 제한은 호스트 함수의 대기 시간과 서비스 요청 전체의 기한을 대신하지 않습니다. 호스트 호출을 기다리는 동안 실행 시계는 멈춥니다. 최초 feed의 컴파일도 그 실행 시간 예산에 포함되지 않습니다. 로컬 풀의 request_timeout 기본값 역시 기한 없음이므로 애플리케이션에 맞게 정해야 합니다.

8직접 해볼 수 있는 실습 과제

출력값 외에 무엇을 확인해야 하는지까지 적어 두는 다섯 과제

아래 코드는 독립된 연습 환경에서 사용할 예제입니다. Python 예제는 pydantic-monty==1.3.0 기준입니다. 각 과제는 “성공했다”보다 상태와 경계가 예상대로 유지되는가를 완료 기준으로 삼습니다.

과제 1

같은 세션과 새 세션 비교 입문

목표: feed가 프로세스 재시작이나 과거 코드 재실행과 다름을 확인합니다. 읽을 소스: crates/monty/src/repl.rs의 MontyRepl.

from pydantic_monty import Monty

with Monty() as pool:
    with pool.checkout() as session:
        session.feed_run('counter = 40')
        session.feed_run('counter += 1')
        print(session.feed_run('counter + 1'))  # 42
    with pool.checkout() as fresh:
        print(fresh.feed_run('2 + 3'))  # 5

완료 기준: 첫 세션의 값이 유지됨을 확인하고, 새 세션에서 counter를 읽으면 이름 해석이 실패함을 별도로 확인하세요. 워커를 재사용하는 일과 세션 전역 변수를 재사용하는 일이 왜 다른지 두 문장으로 정리합니다.

과제 2

타입 선언과 호스트 권한을 따로 연결 입문

목표: 6-4절의 double 예제에서 정적 계약과 실제 구현을 구분합니다. 읽을 소스: crates/monty-type-checking/src/type_check.rs, docs/host-functions.md.

  1. 선언과 구현을 모두 넣고 double(21)의 결과를 확인합니다.
  2. 선언은 둔 채 double('21')로 바꾸어 MontyTypingError를 관찰합니다.
  3. 원래 호출로 되돌린 뒤 실제 구현만 제거해 이름 해석 실패를 관찰합니다.
  4. 호스트 함수를 고정된 항목만 조회하는 함수로 바꾸고, 허용하지 않은 항목을 거절하도록 만듭니다.

완료 기준: “타입이 맞다”와 “권한이 있다”가 다른 조건인 예를 적습니다. 타입 오류로 실행 전 거절된 세션은 수정 후 계속 사용할 수 있다는 문서 계약도 확인하세요.

과제 3

중단한 계산을 서로 다른 답으로 분기 중급

목표: 실행 상태를 값으로 다루는 감각을 익힙니다. 읽을 소스: crates/monty/src/run_progress.rs와 crates/monty/src/dump_format.rs, docs/snapshots.md.

  1. session.feed_start('quote() * 2')로 외부 함수 호출에서 멈춥니다.
  2. FunctionSnapshot인지 확인하고 dump()로 바이트를 저장합니다.
  3. 새 세션 A에서 load_snapshot(data)로 복원하고 resume({'return_value': 100})을 호출합니다.
  4. 다른 새 세션 B에서 같은 바이트를 복원하고 150을 반환합니다.

완료 기준: 결과 200과 300을 얻고, 같은 스냅샷 객체를 두 번 재개하는 방식이 왜 분기가 아닌지 설명합니다. 덤프를 외부에 저장한다면 생성 주체·버전·무결성을 무엇으로 확인할지도 적어 둡니다. 임의로 덤프 바이트를 변조하는 과제는 아닙니다.

과제 4

제한 초과를 정상 결과와 구분 중급

목표: 데이터 증폭과 시간 예산을 이해합니다. 읽을 소스: crates/monty-types/src/resource.rs의 check_large_result(), crates/monty-alloc/src/lib.rs의 set_hard_limit().

from pydantic_monty import Monty, MontyRuntimeError

with Monty() as pool:
    with pool.checkout(limits={
        'max_memory': 10_000_000,
        'max_feed_duration_secs': 0.5,
        'max_recursion_depth': 100,
    }) as session:
        try:
            session.feed_run('x = [0] * 100_000_000')
        except MontyRuntimeError as exc:
            print(exc.display(format='type-msg'))
    # 해당 세션을 정리한 뒤 별도 세션에서 다음 실험을 한다.

완료 기준: 메모리 오류가 호스트 쪽 예외로 드러남을 확인합니다. 시간 제한 실험도 별도 세션에서 수행하고, 오류 뒤의 같은 세션을 계속 쓰지 않습니다. 무한 반복 대신 제한된 반복으로 시작해 예산을 낮추어 관찰하세요. 수치 0.5초와 10MB는 실습 정책이며 최소 요구사항이 아닙니다.

docs/security.md는 메모리·시간 제한 발동 뒤 힙 상태와 참조 수를 보장하지 않으므로 세션을 버리라고 명시합니다. 풀 자체를 유지하는 것과 실패한 세션을 계속 사용하는 것은 다릅니다.

과제 5

최신 커밋의 작은 수정에서 불변식 찾기 고급

목표: 자기 자신으로 이름을 바꾸는 동작이 파일 객체를 훼손하지 않아야 한다는 계약을 읽습니다. 커밋 93227e8의 주제는 OSAccess의 이 동작입니다. 읽을 소스: crates/monty-python/python/pydantic_monty/os_access.py의 OSAccess.path_rename().

# path_rename()의 핵심 분기만 발췌
src_entry = self._get_entry(path)
if src_entry is None:
    raise FileNotFoundError(...)
if PurePosixPath(path) == PurePosixPath(target):
    return

FileNotFoundError(...)는 실제 오류 메시지를 생략한 표시입니다. 완료 기준: 존재 확인과 자기 경로 비교의 순서를 바꾸면 왜 잘못되는지 설명하세요. 없는 파일을 자기 자신으로 바꾸는 호출은 여전히 실패해야 합니다.

crates/monty-python/tests/test_os_access.py의 test_rename_file_to_itself(), test_rename_directory_to_itself(), test_rename_missing_path_to_itself_still_fails()를 읽습니다. 파일 내용뿐 아니라 deleted 플래그·파일 경로·디렉터리 목록·후속 쓰기까지 확인하는 이유를 적어 보세요.

확장 과제로 /test//file.txt와 /test/./file.txt의 동치 처리, 비어 있지 않은 디렉터리의 자기 이름 변경을 표로 정리합니다. 이 Python 메모리 파일 도우미의 수정이 Rust VM 전체의 파일 구현 변경은 아니라는 범위도 표시합니다.

9관련 기술 심화 학습 로드맵

6주 동안 사용자 API에서 실행기와 운영 경계까지 넓혀 간다

선수 지식은 Python 함수·예외·자료구조, 프로세스와 스레드의 기본 구분입니다. Rust가 처음이라면 1~2주차에 소유권·대여·enum·Result를 함께 익히세요. 각 주차의 결과물을 작게 남기면 큰 소스를 읽은 뒤에도 구조를 다시 떠올릴 수 있습니다.

주차학습 주제와 파일남길 결과물
1주차풀·세션·호스트 함수. docs/quickstart/python.md, docs/host-functions.md, docs/security.md입력과 권한을 구분한 그림, 과제 1·2의 관찰표
2주차파서·이름 해석·스코프. crates/monty/src/parse.rs, crates/monty/src/prepare.rs, crates/monty/src/name_map.rs지역·전역·클로저 이름 세 가지의 저장 위치 지도
3주차명령 생성과 스택 실행. crates/monty/src/bytecode/compiler.rs, crates/monty/src/bytecode/op.rs, crates/monty/src/bytecode/vm/mod.rs덧셈·조건문·반복문의 개념적 명령표
4주차중단·재개·저장 호환성. crates/monty/src/run_progress.rs, crates/monty/src/repl.rs, crates/monty/src/dump_format.rs과제 3의 분기 기록, 세션 덤프와 중단 스냅샷 비교표
5주차프로세스 풀·프레임·기한. crates/monty-pool/src, crates/monty-proto/src/frame.rs, crates/monty-proto/src/worker.rs정상 완료·중단·충돌·대기 초과의 상태 전이도
6주차파일 권한·메모리·호환성 검증. crates/monty-fs/src, crates/monty-types/src/resource.rs, crates/monty-datatest/src/main.rs권한별 사례표, 과제 4·5의 경계 조건 설명

9-1. 읽는 깊이를 조절하는 기준

브라우저 경로의 wasiImports()는 파일 preopen·환경 변수·인수를 빈 값으로 구성하면서 시계와 난수 인터페이스를 연결합니다. “WASM이므로 아무 외부 기능도 없다”라는 문장보다, 어떤 import를 주었는가를 확인하는 습관이 유용합니다. 네이티브 경로와 같은 질문을 다른 실행 환경에 적용하는 셈입니다.

9-2. 매주 스스로 답해 볼 세 질문

  1. 지금 읽은 함수가 사용하는 상태의 소유자는 누구이며, 오류가 나면 누가 정리하는가?
  2. 신뢰할 수 없는 값이 들어오는 지점과, 크기·형식·권한을 검사하는 지점은 어디인가?
  3. 이 동작이 지원된다는 근거는 구현·테스트·문서 중 어디에 있으며, 예외 조건도 같은가?

한 기능을 구현·테스트·한계 문서의 세 방향에서 읽으면, 코드를 외우는 대신 설계의 계약을 이해할 수 있습니다. 최신 커밋의 작은 회귀 테스트도 같은 방식으로 읽을 수 있습니다. 복잡한 기능을 추가하기 전에 바뀌면 안 되는 상태부터 나열하는 습관을 가져가세요.

10핵심 키워드 사전

낯선 이름은 이 저장소에서 맡는 일과 함께 기억한다
용어쉬운 설명
인터프리터프로그램이 적은 명령을 해석하고 실행하는 프로그램. monty는 Python의 지원 부분을 직접 실행한다.
언어 수준 샌드박스실행기가 제공하는 명령과 값의 규칙으로 권한을 제한하는 방식. OS 격리와 구분한다.
호스트샌드박스를 만들고 값·도구·파일 정책을 제공하는 바깥 애플리케이션.
ambient authority따로 전달하지 않아도 주변 환경 때문에 사용할 수 있는 권한. monty는 파일·환경·네트워크의 이런 권한을 없애는 방향이다.
capability특정 작업을 할 수 있는 권한을 가진 참조나 핸들. 열린 마운트 디렉터리가 대표 사례다.
AST소스의 문법 구조를 나무 모양 데이터로 나타낸 것. Ruff가 읽은 문법을 monty 표현으로 바꾼다.
바이트코드VM이 읽을 작은 명령의 바이트 배열. Python 소스와 CPU의 기계어 사이에 놓인다.
Opcode덧셈·값 적재·점프처럼 무슨 작업인지 지정하는 명령 번호.
VM이 글에서는 바이트코드 실행기. 컴퓨터 한 대를 흉내 내는 가상머신 제품과 구분한다.
스택·호출 프레임계산 중인 값의 쌓음과, 함수가 어디까지 실행됐는지 담는 기록.
힙·arena객체를 모아 두는 저장 공간. 이 저장소에서는 힙 객체 ID와 경계 메시지의 노드 ID를 구분해야 한다.
interning문자열·함수 등을 테이블에 모으고 ID로 참조하는 방식. 동일 문자열 중복과 이름 검색 비용을 줄이는 데 쓰인다.
참조 수객체가 몇 군데에서 소유되어 있는지 세는 값. 0일 때 해제할 수 있지만 순환에는 추가 처리가 필요하다.
GC더 쓰이지 않는 객체를 찾아 회수하는 작업. monty에서는 참조 수로 남는 순환도 다룬다.
REPL·feed상태를 유지하며 코드 조각을 차례로 실행하는 모델과, 그 세션에 넣는 코드 조각.
worker·pool·checkout실행 담당 프로세스, 그 묶음, 세션 하나를 위해 담당자를 빌리는 절차.
suspension·resume외부 결과가 필요해 멈추는 것과, 답을 받아 이어 실행하는 것.
스냅샷·세션 덤프중단한 실행 상태 또는 feed 사이의 세션을 저장한 바이트. 종류에 맞는 복원 API를 사용한다.
직렬화·CBOR객체를 저장 가능한 바이트로 바꾸는 작업과, 세션 덤프에 사용하는 형식.
protobuf·프레이밍통신 데이터의 구조를 정의하는 방식과, 바이트 스트림에서 메시지 길이를 구분하는 장치.
backpressure처리 능력이 모자라면 무한히 일을 늘리지 않고 기다리거나 거절하는 흐름 제어.
stub·typeshed실행 구현 대신 함수·모듈의 타입을 적은 선언과 그 모음. 실제 권한이나 구현을 만들지는 않는다.
PyO3·N-API각각 Python과 Node.js에서 Rust 기능을 호출하도록 연결하는 기술.
WASM·WASI·WIT이식 가능한 실행 형식, 그 형식에 시스템 기능을 연결하는 인터페이스, 컴포넌트 계약을 적는 언어.
overlay원본 위에 변경분을 따로 보관하는 층. 파일 읽기는 원본을 참고하고 쓰기는 메모리 변경분에 남길 수 있다.
TOCTOU검사한 뒤 실제 사용할 때까지 대상이 바뀌는 경쟁 조건. 경로 문자열 검증만으로 파일 권한을 지키기 어려운 이유다.
차등 테스트같은 입력을 두 구현에 넣어 차이를 찾는 검사. monty-datatest는 Monty와 CPython을 비교한다.
Miri·fuzzingRust의 메모리 규칙 위반을 찾는 도구와, 다양한 비정상 입력으로 실패 조건을 탐색하는 방법.

단어가 익숙해지면 “monty는 Rust로 만든 Python 실행기”라는 소개가 여러 계약으로 나뉘어 보입니다. 문법 계약, 값 전달 계약, 호스트 권한 계약, 저장 호환성 계약, 자원 정책이 각각 따로 존재합니다. 한 계약이 잘 설계됐다고 다른 계약까지 자동으로 충족되는 것은 아닙니다.

11참고 링크

공식 문서와 커밋에 고정한 소스를 함께 펼쳐 놓고 읽는다

11-1. 공식 저장소와 개념 문서

11-2. 핵심 구현으로 바로 가기

11-3. 연결해서 공부할 공식 기술 자료

현재 상태 (2026-10-23 확인)

항목확인된 상태
저장소·선정pydantic/monty · GitHub Trending Rust Monthly #18
스타 / 포크8,761 / 471 — 2026-10-23 GitHub 기준
주 언어 / 생성일Rust / 2023-05-28
최신 릴리스v1.3.0 · 2026-10-21
최근 push2026-10-23 — GitHub 기준
분석 커밋93227e8 · 2026-10-23 · OSAccess에서 자기 경로로 이름을 바꿀 때 상태 보존 (#1095)
코드 규모Rust 223,259줄 / Python 93,936줄 / TypeScript 17,415줄. 빈 줄·주석·테스트·생성물 포함, 상세 범위는 5절.
언어·패키지 기준샌드박스 목표 Python 3.14 부분집합 / 호스트 Python ≥3.10 / Node ≥20 / Rust 최소 1.96
저장·통신 버전덤프 13 (현재 최소도 13) / 프로토콜 6 (최소 지원 5). 제품 버전 1.3.0과 별개.
라이선스MIT · LICENSE 기준. Full Monty 상용 서버의 라이선스와 구분.

스타·포크·생성일·릴리스·push는 해당 날짜의 GitHub 지표입니다. 코드 규모와 설정값은 커밋 93227e8 기준입니다. 저장소 생성일만으로 현재 구현의 개발 기간을 계산하거나, 한 커밋의 코드 규모로 개발 속도를 추정하지 않습니다.

쓰기 전에 알아둘 한계
Python 호환성, 권한, 자원, 저장의 조건을 먼저 확인하세요

Python 전체가 아닙니다. 클래스 상속·메타클래스·사용자 예외 클래스·yield·async for 등이 지원되지 않습니다. 일부 반복 도구는 즉시 목록을 만들며, 일반 서드파티 패키지 설치 환경도 없습니다. 호스트의 external_modules는 선택한 기능을 노출하는 연결 장치입니다.

프로세스 분리는 OS 권한 격리와 다릅니다. OSS 워커는 호스트의 권한으로 실행됩니다. Rust crate를 직접 같은 프로세스에서 실행하면 충돌도 공유합니다. Python·Node API는 그런 동일 프로세스 실행 모드를 제공하지 않습니다. 브라우저 경로는 별도의 Worker·WASM 경계를 사용합니다.

기본 시계와 난수는 예외입니다. OsPolicy 기본값은 시스템 시계와 OS 엔트로피를 사용합니다. 모든 외부 정보가 차단된 결정적 환경이라고 가정하지 마세요. 호스트 함수·객체·외부 모듈·파일 콜백은 허용한 만큼 호스트의 권한을 실행합니다.

메모리·실행 시간 제한은 기본으로 켜져 있지 않습니다. 시간은 VM 실행 구간을 세며, 콜백·파일 I/O·서비스 전체의 기한을 대신하지 않습니다. 메모리·시간 제한이 발동한 뒤에는 해당 세션을 폐기해야 합니다. 타입 검사 통과도 실행 성공이나 안전성의 증명이 아닙니다.

덤프는 신뢰할 수 있는 원본만 복원합니다. Monty는 생성 주체와 무결성을 인증하지 않고 모든 의미를 검증하지도 않습니다. 성공적으로 디코딩됐다는 사실만으로 유효성을 판단할 수 없습니다. 파일 마운트와 호스트 객체 저장소 등 외부 상태는 덤프 한 개가 모두 복제해 주는 자원이 아닙니다.

성능 수치는 측정 조건에 묶여 있습니다. 문서의 이전 버전·특정 장비 측정을 v1.3.0의 성능 보장으로 해석하지 마세요. 예제의 예상 출력과 벤치마크의 측정값도 구분해야 합니다.

monty에서 가져갈 가장 큰 배움은 코드를 실행하는 기능과 그 코드에 권한을 부여하는 결정을 분리하는 방법입니다. 작은 호스트 함수 하나에서도 이 원칙을 연습할 수 있습니다. 계산은 샌드박스에, 자원과 권한 정책은 호스트의 명시적 계약에 두는 구조를 자신의 프로젝트에 적용해 보세요.