Expand description
§monty-proto
The wire protocol connecting Monty worker processes to the parents that drive them.
Monty executes untrusted Python, and a Monty process can never be made fully
crash-proof against memory errors (stack overflow aborts, allocator aborts —
the monty-alloc allocator turns the
latter into this crate’s OOM_EXIT_CODE so a parent can classify them).
The subprocess architecture isolates those crashes: a parent — the
monty-pool crate, and through it the
Python and JavaScript packages — drives monty subprocess children over
framed stdio (or a WebSocket), and a dead child is simply replaced. This crate
defines the protocol both sides speak.
The protocol is protobuf (rather than Monty’s internal CBOR dump format) so a
parent or child can be implemented in any language — see
proto/monty/v1/monty.proto
for the schema and the protocol rules documented alongside it.
§What the crate provides
pb— prost-generated message types. The generated code is checked in; regenerate withmake generate-proto(CI enforces sync viamake check-proto).FrameReader/write_frame— 4-byte little-endian length-prefixed framing, with a hard cap on frame length.- Fallible conversions between
pbtypes and Monty’s public types (MontyObject/CallArgs/NamedValuesover aMontyGraph,MontyException, mounts, resource limits, …). - Host-object routing on the wire: host-backed
ClassInstance/ClassTypenodes carry host-generated uuids, andFunctionCall.object_id/NameLookup.object_idroute their method calls and lazy attribute lookups back to the parent’s per-session instance store; sandbox-defined classes and instances carry worker-generated uuids that never reach that store. Calling a host-backed instance arrives as a__call__method call on its uuid. - Host modules:
OsCall.importrequests a missing top-level module; answer with its value ornot_found. Dotted imports read further components as attributes of that value. Before type checking,import_stubsrequests host stubs once per module per session. Answer with a dict of dotted module paths to.pyisource orNone. PROTOCOL_VERSION/MIN_SUPPORTED_PROTOCOL_VERSION— the wire schema version a parent declares inConfigure, and the range a child serves. Versioned independently of the monty package: peers on different releases interoperate as long as their protocol versions overlap. There is no in-band negotiation, so a child rejecting a version reports its range in theFatalErrorfor the parent to downgrade to, then exits non-zero. Version0is always rejected;monty_versionis diagnostic metadata, not a compatibility check. Snapshot compatibility is checked separately using the dump-format version.python(cargo feature, off by default) — thepythonmodule: PyO3-based conversions between live Python objects andMontyObject/MontyException, used by thepydantic-monty-clientextension module. The feature pulls inpyo3(but never itsextension-modulefeature — how libpython is linked stays the top crate’s decision), so pure-Rust consumers pay nothing for it.
Repeated fields and byte buffers in protocol messages use BudgetVec<T>.
Construct them from standard vectors with .into() or collect an iterator directly; use .into_inner() to recover a standard vector without copying.
Host construction and cloning are unbudgeted; fallible try_push charges any growth to the active decode budget.
§Values are special-cased for performance
Values cross as one flat monty.v1.Arena per message: a post-order node arena in which containers hold child indexes.
A sub-object shared inside the sandbox, or between two arguments of one call, is sent once, and the carrying message
names its roots by index.
prost extern_path maps the message onto WireArena, a hand-written prost::Message implementation that encodes
borrowed monty_types::unstable::MontyNodes without cloning and decodes one generated protobuf node at a time.
Each node is validated and converted before being retained in the domain arena; temporary payloads and conversion allocations share the frame budget.
Strings, bytes and reference buffers transfer without copying; reference containers use WireIndexes, WireNodePairs and WireNamedTuple rather than temporary vectors of protobuf ids.
Duplicate fields follow protobuf merging rules, and value depth does not increase decoding recursion.
tests/differential.rs proves it byte-compatible against a fully prost-generated oracle (tests/oracle/, regenerated
and CI-checked together with the main codegen).
§Children are untrusted
A parent must treat every frame from a (possibly compromised) child as untrusted input: conversions from proto to Rust are fallible by design, decoding enforces a per-frame decode budget and validates every arena index, and nothing in this crate panics on malformed wire data.
Decode protocol types through decode_frame or FrameReader::read.
These functions manage the per-frame allocation budget automatically.
Direct Message::decode calls on these types fail if they allocate payload storage without a frame budget.
Frames are capped at 256 MiB, with a separate fixed 1 GiB budget for cumulative decoded allocation requests.
This budget is independent of the session’s max_memory.
Compact messages can require much more memory when decoded.
The budget covers arena slots, repeated-field capacity, strings, byte buffers, boxed payloads and BigInt storage.
Temporary protobuf buffers and conversions into domain nodes share this budget.
Child references include an allowance for host container storage; shared sub-objects are encoded once.
Growth charges the full replacement allocation, with no refunds for discarded payloads.
A frame can therefore exceed the budget even if its final decoded value occupies less than 1 GiB.
The receiver checks the budget before allocating payload storage.
The wire buffer, bounded stack and error storage, allocator metadata and subsequent host conversions are not counted.
Each concurrent decode has its own budget; this is not a process-memory limit.
See DEFAULT_MAX_DECODE_BYTES in src/frame.rs for the accounting contract.
The browser component uses separate decoded-value estimates for WIT arenas, with the same 1 GiB ceiling.
Those checks do not account for all allocations made by the component ABI or JavaScript conversion.
Invalid dates, timedeltas, exception names and other semantic values are rejected after parsing each protobuf node.
The browser component validates semantic values while converting WIT arenas.
A parent receiving an invalid frame discards the worker with a protocol error.
A worker receiving such a malformed request reports RuntimeError("protocol violation: malformed request: ...")
and keeps the session.
§Worker state machine
The worker cargo feature (off by default) adds the worker module: the
transport-agnostic child state machine, shared by the native monty subprocess
worker and the wasm worker. It links the monty interpreter, so only
worker-side crates enable it.
An external FunctionCall with allow_eager_await = true permits the parent to await a coroutine before replying.
The parent sends its value or exception in ResumeFutures, with exactly one result matching the call ID.
The worker creates a settled awaitable and continues, avoiding a separate ResolveFutures suspension.
Synchronous returns still use ResumeCall; parents may also ignore the hint and register a pending future as before.
Older workers omit the flag, which defaults to false, so newer parents retain the existing reply sequence.
§Monty crates
monty— the core interpreter: Python parser, bytecode VM, and sandbox.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— 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. this cratemonty-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.
Modules§
Structs§
- Budget
Vec - A protocol vector whose decoding growth is charged before allocation. Conversion to and from a standard vector transfers ownership without copying. Host construction and cloning do not require an active decode budget. In-place growth must propagate a decode error:
- Frame
Reader - Reads length-prefixed protobuf frames from a byte stream.
- Wire
Arena - The wire form of a
MontyGraph: what themonty.v1.Arenaproto message decodes into and encodes from. - Wire
Function Call - Wire form of
monty.v1.FunctionCall: the call’s argument arena plus the ids of its positional and keyword arguments. - Wire
Indexes - Wire
Indexesstored as domain ids, avoiding an intermediateVec<u32>. - Wire
Named Tuple - Wire named tuple with domain ids and budgeted names, ready for zero-copy conversion.
- Wire
Node Pairs - Wire
NodePairsstored as domain pairs, with no intermediate pair vector.
Enums§
- Frame
Error - Framing or decoding failure while reading or writing protocol messages.
- Proto
Convert Error - Why a wire value could not be converted into its monty equivalent.
Constants§
- DEFAULT_
MAX_ DECODE_ BYTES - Per-frame ceiling on cumulative decoded allocation requests (1 GiB). Generated protocol messages and hand-written values budget vectors, buffers and boxes before allocating, including full replacement buffers on growth. Freed payloads are not refunded. Excludes the wire buffer, bounded decode stack/error overhead and allocator metadata; multiplies per concurrent decode.
- DEFAULT_
PRINT_ FLUSH_ INTERVAL - How long the child holds buffered
print()output before emitting it as aPrintevent, whenpb::Configure::print_flush_interval_mssays nothing. - MAX_
FRAME_ LEN - Default maximum frame length (256 MiB).
- MIN_
SUPPORTED_ PROTOCOL_ VERSION - Oldest
PROTOCOL_VERSIONthis build still serves. - PROTOCOL_
VERSION - Version of the wire schema this build speaks, sent in
pb::Configure::protocol_versionand range-checked by the child.
Functions§
- check_
protocol_ version - Checks a peer’s declared
pb::Configure::protocol_versionagainst the range this build serves. - decode_
frame - Decodes one already-deframed message from
bytes. - encode_
framed_ into - Encodes
msgas one length-prefixed frame — prefix and body in a single buffer — intobuf(cleared first), enforcingMAX_FRAME_LENbefore encoding. - encode_
to_ capped_ vec - Encodes
msgto aVec<u8>, enforcingMAX_FRAME_LENbefore encoding (so a >256 MiB message is rejected without allocating it). - exceeds_
max_ frame_ len - The encoded frame length of
msgwhen it exceedsMAX_FRAME_LEN(saturating atu32::MAX), orNonewhen it fits. - ext_
result_ from_ proto - Validates a decoded call result against the arena its message carried.
- ext_
result_ to_ proto - Splits a call result into its wire kind and the arena a
Returnvalue indexes (Nonefor the other arms). - future_
results_ from_ proto - Converts wire future results into
(call_id, result)pairs forResolveFutures::resume; each returned value gets its own copy of the nodes it reaches. - future_
results_ to_ proto - Merges every returned value into one arena and points each
ReturnValueinto it. - named_
values_ from_ proto - Validates decoded named inputs against their arena.
- named_
values_ to_ proto - Splits named inputs into
NamedRefs and the arena they index. - os_
call_ from_ proto - Validates a decoded OS call envelope into its call id and typed call.
- os_
call_ to_ proto - Builds the
OsCallenvelope: call id, typed arm, the eager-await hint, the call’s source position and, forGetenv, the arena its default indexes. - resume_
call_ from_ proto - The result of a
ResumeCallreply, read with its arena byext_result_from_proto. - validate_
requirement - Rejects a requirement string that uv would interpret as a command-line option rather than a package specifier.
- write_
frame - Encodes
msgand writes it towriteras one length-prefixed frame, then flushes (see the module docs for why flushing every frame is required).