code and embedded-python only.Expand description
§monty
The core interpreter crate of Monty — a minimal, secure Python interpreter written in Rust for use by AI.
Experimental — this project is still in development, and not ready for prime time.
Monty lets you safely run Python code written by an LLM inside your own process, without the cost, latency and complexity of a container based sandbox. It parses Python with Ruff’s parser and executes it on its own bytecode VM — no CPython, no FFI, no C dependencies. Startup takes microseconds, not hundreds of milliseconds.
The sandbox has no ambient access to the host: filesystem, environment and network are only reachable through external function calls and mounts that you explicitly provide.
This crate is the pure-Rust core. Most users want one of the bindings built on top of it:
- Python:
pydantic-monty - JavaScript/TypeScript:
@pydantic/monty - CLI: the
montybinary from themonty-runtimecrate
See the project README for the full feature matrix, motivation, and supported Python subset.
§Basic usage
MontyRun parses and compiles code once; run executes it with input values and returns the value of the final expression as a MontyObject:
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));Errors are returned as MontyException, with a traceback matching what CPython would produce. PrintWriter controls where print() output goes: Stdout, Disabled, or collected into a String / (stream, text) tuples for the host to inspect.
§Resource limits
Untrusted code shouldn’t be able to hog the host. ResourceTracker enforces execution-time and recursion limits and configures GC scheduling. Memory limits additionally require monty-alloc as the executable’s global allocator:
use std::time::Duration;
use monty::MontyRun;
use monty_types::{CompileOptions, ResourceTracker, PrintWriter, ResourceLimits};
let limits = ResourceLimits {
max_duration: Some(Duration::from_millis(20)),
..ResourceLimits::default()
};
let runner = MontyRun::new("while True: pass".to_owned(), "spin.py", vec![], CompileOptions::default()).unwrap();
let err = runner.run(vec![], ResourceTracker::new(limits), PrintWriter::Stdout).unwrap_err();
assert!(err.to_string().contains("time limit exceeded"));§External functions and snapshotting
The defining feature of the crate: instead of running to completion, MontyRun::start returns a RunProgress that pauses execution whenever the sandboxed code calls a function provided by the host. The host runs the real function (an API call, a database query, an LLM tool) and resumes with the result:
use monty::{MontyRun, RunProgress};
use monty_types::{CompileOptions, ResourceTracker, MontyObject, PrintWriter, ResourceLimits};
let code = "data = get_data(3)\ndata * 2";
let runner = MontyRun::new(code.to_owned(), "main.py", vec!["get_data".to_owned()], CompileOptions::default()).unwrap();
// pass the external function in as an input
let get_data = MontyObject::Function { name: "get_data".to_owned(), docstring: None };
let progress = runner.start(vec![get_data], ResourceTracker::default(), PrintWriter::Stdout).unwrap();
// execution pauses at the `get_data(3)` call
let RunProgress::FunctionCall(call) = progress else { panic!("expected a function call") };
assert_eq!(call.function_name, "get_data");
assert_eq!(call.args, vec![MontyObject::Int(3)]);
// the host computes the result and resumes
let progress = call.resume(MontyObject::Int(21), PrintWriter::Stdout).unwrap();
let RunProgress::Complete(result) = progress else { panic!("expected completion") };
assert_eq!(result, MontyObject::Int(42));A REPL session is a self-contained snapshot of the interpreter: serialize it with dump(), store it in a file or database, and Dump::load() + keep feeding it later — in a different process or on a different machine. The dump carries the session metadata (script name, type-check stubs) alongside the state, behind a version this build checks on load:
use monty::{Dump, MontyRepl, Session, SessionRef, dump};
use monty_types::{CompileOptions, MontyObject, PrintWriter, ResourceTracker};
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!("expected an idle session")
};
let result = restored.feed_run("x + 1", vec![], PrintWriter::Stdout).unwrap();
assert_eq!(result, MontyObject::Int(42));MontyRun and RunProgress have no dump format of their own, but both implement serde::Serialize/Deserialize, so a host that wants to cache parsed code or a paused run can serialize them with whatever format it already uses.
Async host functions are supported too: FunctionCall::resume_pending continues execution with a pending future the sandboxed code can await; when all tasks are blocked, execution yields RunProgress::ResolveFutures for the host to supply results.
§Other pieces
MontyRepl— a REPL-style interface: feed code snippet by snippet with state persisting between snippets.fsmodule — mount real host directories into the sandbox at virtual paths (read-write, read-only, or copy-on-write in-memory overlay), with path resolution hardened against escapes.RunProgress::OsCall— filesystem and otheros-level operations the host can intercept or delegate.
§Monty crates
monty— the core interpreter: Python parser, bytecode VM, and sandbox. this cratemonty-types— the shared boundary data types (values, exceptions, OS calls, resource limits) hosts use without linking the interpreter.monty-fs— host-side filesystem mounts: maps virtual sandbox paths to real host directories.monty-runtime— themontybinary: REPL, file runner, and subprocess worker mode.monty-pool— an elastic pool of crash-isolatedmontyworker subprocesses.monty-proto— the protobuf wire protocol spoken between pool parents and workers.monty-type-checking— type checking of sandboxed code, powered by ty.monty-typeshed— the trimmed typeshed stubs describing the stdlib subset Monty implements.monty-macros— the proc macros behindmonty’s argument parsing.
§License
MIT
Macros§
- defer_
drop - The preferred way to ensure a [
DropWithContext] value is cleaned up on every code path. - defer_
drop_ mut - Like [
defer_drop!], but rebinds$valueas&mut Vvia [DropGuard::as_parts_mut].
Structs§
- Dump
- A complete REPL session snapshot: the interpreter state plus the session-scoped context that lives outside it.
- Function
Call - Execution paused at an external function call or dataclass method call.
- Monty
Repl - Stateful REPL session that executes snippets incrementally without replay.
- Monty
Run - Primary interface for running Monty code.
- Name
Lookup - Execution paused for an unresolved name lookup.
- OsCall
- Execution paused for an OS-level operation.
- Repl
Function Call - REPL execution paused at an external function call or dataclass method call.
- Repl
Name Lookup - REPL execution paused for an unresolved name lookup.
- Repl
OsCall - REPL execution paused for an OS-level operation.
- Repl
Resolve Futures - REPL execution state blocked on unresolved external futures.
- Repl
Start Error - Error returned when a REPL snippet raises a Python exception during
start()orresume(). - Resolve
Futures - Execution state paused while waiting for external future results.
Enums§
- Dump
Error - Why a dump could not be restored.
- Repl
Continuation Mode - Parse-derived continuation state for interactive REPL input collection.
- Repl
Progress - Result of a single suspendable REPL snippet execution.
- RunProgress
- Result of a single step of iterative execution.
- Session
- Where a dumped session was paused. The variant order is mirrored by
SessionRefand encoded as a postcard discriminant — keep them in step. - Session
Ref - Borrowed counterpart of
Sessionused when dumping, so a live session can be serialized without moving the repl out of the host’s own state.
Constants§
- DUMP_
VERSION - Version of the dump’s postcard schema.
Functions§
- detect_
repl_ continuation_ mode - Detects whether REPL source is complete or needs more input.
- dump
- Serializes a live session and its metadata into a versioned dump, readable
by
Dump::load.