monty 0.0.21

A sandboxed, snapshotable Python interpreter written in Rust.
Documentation

monty

CI crates.io license

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:

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.
  • fs module — 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 other os-level operations the host can intercept or delegate.

Monty crates

  • monty — the core interpreter: Python parser, bytecode VM, and sandbox. this crate
  • monty-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 — the monty binary: REPL, file runner, and subprocess worker mode.
  • monty-pool — an elastic pool of crash-isolated monty worker 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 behind monty's argument parsing.

License

MIT