pydantic-monty

repository·main·Indexed 27 days ago

https://github.com/pydantic/monty

A minimal, secure Python interpreter written in Rust designed for AI agents to execute LLM-generated code with low latency. It provides crash isolation via a worker pool and supports safe host-side filesystem mounts through the monty-fs crate. Available as a Rust library and an npm package (@pydantic/monty) for Node.js and browser environments, featuring resource limits, snapshot serialization, and lazy name resolution via externalLookup.

Tokens
43.4K
Snippets
45
Records
253
Agent score
93%

What's inside pydantic-monty

  1. Overview of monty-proto wire protocol

    main

    The monty-proto crate defines the Protobuf-based wire protocol used to connect Monty worker processes to their parent drivers. This protocol allows parents and children to communicate over framed stdio or WebSockets. Because it uses Protobuf instead of Monty's internal postcard format, implementations can be written in any language.

    Key characteristics:

    • Isolation: Designed for a subprocess architecture where a parent (like monty-pool) drives children. If a child crashes due to memory errors, the parent can simply replace it.
    • Security: The protocol is designed with untrusted children in mind. Conversions from proto to Rust are fallible, and decoding enforces depth and size budgets to prevent exploitation of malformed data.
    • Version Lockstep: The protocol does not support in-band negotiation. The parent and child must be deployed in lockstep using the MONTY_VERSION constant to ensure compatibility.
  2. Use monty-types for shared boundary data types

    main

    The monty-types crate provides owned, heap-free data types used to cross the boundary between the Monty sandboxed Python interpreter and its host environments. It contains no interpreter implementation, making it suitable for host-side crates that need to communicate with Monty without linking the full interpreter binary.

    Key types provided include:

    • Python Values: MontyObject and MontyType (including datetime family like MontyDate, MontyDateTime, MontyTimeDelta, MontyTimeZone), DictPairs, and MontyFileHandle.
    • Exceptions: MontyException and ExcType which include tracebacks (StackFrame, CodeLoc) and structured payloads (ExcData).
    • OS Calls: OsFunctionCall for typed payloads used when sandboxed code suspends (e.g., file reads/writes, open(), os.getenv) and stat_result builders.
    • Resource Management: ResourceTracker and ResourceLimits for enforcing time, memory, and recursion limits.
    • I/O: PrintStream and PrintWriter for capturing print() output.
    • Compilation & Lookup: CompileOptions, ExtFunctionResult, NameLookupResult, and FileMode.
    • Formatting: CPython-compatible formatting helpers for repr() outputs.
    use monty_types::MontyObject;
    
    let value = MontyObject::List(vec![MontyObject::Int(1), MontyObject::String("x".to_owned())]);
    assert_eq!(value.py_repr(), "[1, 'x']");
  3. Understand the `typing` module behavior in Monty

    main

    The typing module in Monty is a set of inert marker objects designed solely to prevent ModuleNotFoundError in type-annotated code. No runtime type checking is performed. Subscripting types like list[int] or Union[int, str] returns a placeholder value but does not validate data.

    Key behaviors:

    • TYPE_CHECKING is always False at runtime.
    • Many standard typing utilities are not implemented, including get_type_hints, get_args, get_origin, cast, assert_type, overload, and NewType.
    • Annotation introspection on functions and modules is not supported; __annotations__ will not be populated for them.

    If your logic requires actual type validation, you must perform it on the host side outside of the Monty sandbox.

  4. Understand Monty resource limits and error types

    main

    Monty enforces hard limits on memory, time, and recursion to bound untrusted code. It is important to distinguish between terminal errors and catchable errors:

    • Terminal Errors (Cannot be caught by sandboxed code):
      • MemoryError: Occurs when memory limits are exceeded.
      • TimeoutError (or ResourceError): Occurs when time limits are exceeded.
    • Catchable Errors:
      • RecursionError: Occurs when the call depth limit is reached (similar to CPython).

    Note: After a terminal MemoryError or TimeoutError, the VM state is unreliable. The host should discard the VM instance rather than attempting to recover or continue execution.

  5. Understand Monty's file opening and descriptor model

    main

    Monty uses a one-shot OS call model for file I/O. Unlike CPython, Monty never keeps a native file handle (file descriptor) alive between calls.

    When you call open(), read(), or write(), Monty performs a complete round-trip to the host OS (e.g., opening, acting, and closing the file immediately).

    Key implications for developers:

    • Serialization Safety: Sessions can be serialized at pause points and resumed later without dangling host resources.
    • No Concurrency Protection: External processes can modify or remove the underlying file between Monty's individual calls. There is no protection against the file changing between a read() and a subsequent write().
    • Memory Usage: For readable files, the first read operation loads the entire file into a heap-resident buffer. This buffer is used for all subsequent reads and seek() operations. The file size counts against your configured max_memory.
  6. Understand the Monty execution model and worker types

    main

    Monty runs the type checker, compiler, and interpreter in a separate process (a worker) to ensure that sandbox crashes (like stack overflows) only kill the worker and not the host application.

    There are two primary transport modes:

    1. Subprocess Worker (monty subprocess): The default mode for both Python (pydantic_monty) and JS (@pydantic/monty). It provides a full sandbox with resource limits and no subprocess access. The language semantics are identical to embedding the interpreter directly.
    2. WebSocket Worker (pydantic_monty.AsyncMontyWebsocket): Available in the Python package. This connects to a remote child that is not necessarily a sandbox. A remote child might run in real CPython with no resource limits and full filesystem/network access. Do not rely on Monty's in-process safety guarantees when using WebSocket transport.
  7. Understand the Monty sandbox filesystem boundary

    main

    The Monty sandbox has no default filesystem access. All filesystem operations are restricted to directories explicitly mounted by the host via a MountTable.

    Key constraints:

    • Paths outside of an explicit mount are invisible; attempting to access them with open() or pathlib methods will raise FileNotFoundError.
    • Standard system paths like /tmp, /etc, /proc, /dev, ~, or the host's current working directory are not available unless explicitly mounted.
    • Paths are always treated as POSIX (using forward slashes /) regardless of the host operating system. For example, Path("C:/Users/foo") is treated as a literal POSIX path, not a Windows path.
    • Path.resolve() returns virtual paths, never host paths.
  8. Understand the one-shot I/O execution model

    main

    In the Monty sandbox, open() and pathlib I/O operations do not maintain OS handles between calls. Each read or write is treated as a separate, one-shot operation performed by the host.

    This design ensures that subprocess dump/load is safe, but it also means that external processes can observe partial state between individual write operations.

  9. Understand Monty's concurrency model

    main

    Concurrency in Monty is cooperative and host-driven. There is no internal event loop, scheduler, threads, or preemption within the sandbox.

    When using asyncio.gather, Monty suspends execution whenever all branches are blocked on an external call, hands control to the host, and resumes once the host returns the results.

  10. Use monty-pool to run untrusted Python code with crash isolation

    main
    The monty-pool crate provides an elastic pool of monty worker processes. It is designed to run untrusted Python code by isolating crashes (like segfaults or stack overflows) within worker subprocesses. If a worker crashes, the pool detects the death, replaces the worker, and ensures the parent process remains healthy. This is the recommended way to run Monty from Rust and serves as the engine for pydantic-monty (Python) and @pydantic/monty (JavaScript).
  11. Use monty-fs for host-side filesystem mounts

    main

    The monty-fs crate provides the MountTable utility, which allows you to map virtual POSIX paths used inside the Monty sandboxed Python interpreter (e.g., /mnt/data) to actual directories on your host machine.

    Key features:

    • Configurable Access Modes: Supports read-write, read-only, or in-memory overlay modes.
    • Security: All path resolution is enforced through path_security::resolve_path, which prevents symlink escapes and ensures the sandbox cannot access files outside of the defined mount boundaries.
    • Memory Management: Each mount has an aggregate memory budget (defaulting to 100 MB) shared by in-memory overlay data and transient filesystem results. If an operation exceeds this budget, it returns a MemoryError to prevent unbounded reads.
  12. Understand Monty's `re` module engine differences

    main

    Monty's re module is powered by the Rust fancy-regex crate rather than CPython's engine. While most patterns behave identically, you should be aware of several key differences:

    • No Bytes Support: Monty does not support bytes patterns or bytes subjects. Using a bytes pattern will raise first argument must be string or compiled pattern. Using a bytes subject will raise a mixed-types error.
    • Backreference Limits: Only backreferences \1 through \9 are supported. Syntax like \10 and higher is not recognized.
    • Error Handling: Invalid patterns raise re.PatternError. Unlike CPython, these errors do not include pattern, pos, lineno, or colno attributes.
    • Compiled Size Limits: Extremely large patterns (e.g., huge counted repeats like a{5000000}) will raise re.PatternError due to compiled-size limits enforced by the engine.