A Python interpreter written in Rust.
Build a maintainable, sandbox-focused Rust Python rather than a full CPython clone. MiniPython should fully cover the Python syntax frontend where practical, incrementally implement core runtime semantics and a safe pure-memory standard library subset, and migrate CPython tests by public behavior. CPython internal implementation tests should be classified and tagged rather than copied as runtime requirements.
CPython is the behavior oracle, not an implementation source. MiniPython must
not wholesale port CPython Lib/; standard-library behavior is accepted only
when the supported and excluded sandbox surfaces are documented and backed by
direct differential evidence.
In scope:
- CPython-compatible syntax frontend coverage: tokenizer, parser, AST, compiler lowering, and user-visible syntax/error classification.
- Core runtime semantics: object model, descriptors, MRO, functions, closures, generators, async constructs, exceptions, containers, numbers, strings, bytes, bytearray, array, and memoryview behavior.
- Safe pure-memory standard library modules:
builtins,sys,types,collections,collections.abc,math,math.integer,array,copy,io.BytesIO,operator,functools,itertools, andjson. Additional pure-memory compatibility shims may exist to support migrated CPython tests, but they do not expand the default product scope unless they are added to the migration manifest with explicit supported and excluded surfaces. - CPython public behavior migration through executable differential tests.
Every bundled stdlib module must have a matching
cpython_diffcase before its supported surface is considered complete. Partial modules must document their supported API and excluded API in the migration and coverage notes.
Out of scope by default:
- Full CPython standard library coverage.
- Host I/O integration such as real
open(), file descriptors, TTY behavior,input(), andpty. - Network and process integration such as
socket,subprocess, andsignal. - C ABI and C extension compatibility, including modules such as
_ssl,_socket,_ctypes, and_testcapi. - CPython implementation internals such as refcounts, GC tracking,
bytecode/opcode identity, interpreter specialization, and exact
co_stacksize. - Default
pdbintegration and fullbreakpoint()environment-variable behavior. - locale-sensitive behavior unless it is explicitly promoted into the sandbox runtime requirements.
cargo build --releasemnpy script.py # run a file
mnpy -c "print(1+2)" # execute a string
mnpy -e "1 + 2 * 3" # evaluate an expression
echo "print(1)" | mnpy # pipe inputmnpy always runs code through the sandbox boundary:
mnpy --max-memory-bytes 134217728 -c "print(1 + 2)"mnpy applies the fixed safe-stdlib allowlist, source-size, instruction,
call-depth, captured-output, VM-allocation, and child-process memory limits. It
always launches an internal worker; there is no public flag that disables the
sandbox. On macOS the parent monitors the worker's physical footprint; on other
Unix hosts it uses kernel process limits. File execution uses the script's
directory as the default sandbox module root; -c and stdin expose no host
module root unless --root is passed. Library APIs remain useful for focused
runtime and parity tests, but in-process calls are not the supported untrusted-
code boundary.
Embedders use the same worker boundary as the CLI. Sandbox starts the supplied
mnpy worker executable, sends a length-framed MessagePack request, and returns
a structured result without exposing VM objects to the host:
use minipython::{Sandbox, SandboxInputs, SandboxValue};
let sandbox = Sandbox::new("target/release/mnpy");
let mut inputs = SandboxInputs::new();
inputs.insert("price".into(), SandboxValue::from(40_i64));
let result = sandbox.eval_with_inputs("price + 2", inputs);
assert!(result.is_success());
assert_eq!(result.value, Some(SandboxValue::from(42_i64)));ExecutionResult separates status, value, exact captured stdout/stderr,
exception phase/type/message, and resource usage. Inputs and returned values are
restricted to inert data (None, booleans, numbers, strings, bytes, bytearrays,
lists, tuples, and dictionaries). Unsupported runtime objects are output-only
Opaque descriptions and cannot be injected back into the sandbox. The worker
path is explicit so an embedding application controls which reviewed sidecar it
executes.
Host capabilities are opt-in external functions. A function exists in Python only after the embedding application registers its exact global name:
use minipython::{ExternalFunctionError, SandboxValue};
sandbox.register_external_function("lookup_price", |call| {
let Some(SandboxValue::String(symbol)) = call.args.first() else {
return Err(ExternalFunctionError::new("TypeError", "symbol must be a string"));
};
Ok(SandboxValue::from(format!("quote:{symbol}")))
})?;Calls and responses cross the worker protocol using the same inert value
allowlist. Runtime objects cannot be passed to the host, opaque host values
cannot be returned, callback errors become catchable Python exceptions, and a
callback panic cannot unwind through Sandbox::execute. External callbacks are
trusted host code: they may deliberately perform capabilities granted by the
application, and their own runtime is not forcibly cancellable by the worker's
wall-clock limit.
For stateful agent workflows, create a SandboxSession from the same configured
sandbox:
let mut session = sandbox.session()?;
let setup = session.run("balance = 40");
assert!(setup.is_success());
let result = session.eval("balance + 2");
assert_eq!(result.value, Some(SandboxValue::from(42_i64)));
session.close()?;A session keeps one isolated worker and preserves Python globals, function
closures, mutations, and the module cache without replaying earlier source.
Calls are sequential through &mut self; every call receives fresh VM and
wall-clock budgets. A process-memory or wall-clock termination closes the whole
session, and later calls fail instead of reusing a possibly corrupted worker.
Dropping a session performs a bounded close and reaps the worker.
The python/ package exposes the same product surface to a Python host without
loading MiniPython into that host process:
from minipython_sandbox import Sandbox
with Sandbox(executable="target/release/mnpy") as sandbox:
result = sandbox.eval("price + 2", {"price": 40})
assert result.is_success
assert result.value == 42Sandbox supports run, eval, check, explicit external_functions, and
one persistent session() using the same inert values and structured results
as the Rust API. The package is pure Python and uses only the standard library.
It starts a private JSON-line broker in the reviewed mnpy executable; that
broker delegates every execution to the Rust Sandbox API, which still starts
and monitors the isolated MessagePack worker. There is no C extension,
in-process VM path, shell invocation, or flag that disables worker limits.
CLI execution is bounded to 1,000,000 VM instructions by default. Use
--max-steps N to select a smaller or larger budget. Library callers can use
RuntimeOptions::with_max_instructions; SandboxPolicy also applies the same
finite default to virtual and sandbox-directory modules. The budget is shared
across functions, generators, coroutines, dynamic execution, and imports.
The worker also has a 5-second wall-clock deadline by default, covering parsing,
compilation, VM execution, and shutdown. Use --max-time-ms N to configure it.
Nested VM frames are also bounded to 3 by default; use --max-depth N or
RuntimeOptions::with_max_call_depth to configure that guard.
Captured output is bounded to 1 MiB by default and shares one byte budget across
nested execution; use --max-output-bytes N or
RuntimeOptions::with_max_output_bytes to configure it.
Core VM value materialization has a shared monotonic 8 MiB default budget,
configurable with --max-allocated-bytes N or
RuntimeOptions::with_max_allocated_bytes. This complements the existing
64 MiB single-allocation guard. The mnpy child-process boundary covers
compiler and host allocations outside VM value accounting.
Runnable boundary examples live under examples/sandbox/. For example,
mnpy examples/sandbox/blocked_host_capabilities.py shows the host I/O,
network, process, and C-ABI capabilities that the sandbox intentionally blocks.
The current whole-project sandbox completion state is tracked in
tests/README.md. A green parity corpus alone is not a completion signal.
The exact core runtime stopping point, including the disposition of every
CPython coverage row that remains partial, is in tests/README.md.
Run tests/run.sh --focused while developing sandbox controls,
tests/run.sh --discovery for differential discovery only, and
tests/run.sh for the complete release gate. This is now the only test
entrypoint; the old sandbox and gap-sweep runners were removed.
tests/run.sh --focused
tests/run.sh --discovery
tests/run.sh --discovery --seed 20260710 --generated-cases 1024
tests/run.sh
tests/run.sh --module json
tests/run.sh --root-cause json-loads-coreThe unified pipeline runs driver unit tests, the checked-in corpus, and
deterministic generated programs through real CPython and the sandbox-default
mnpy. The fixed seed is balanced across syntax, runtime, stdlib, and security
layers. Unexpected generated differences are minimized and written under
reports/differential-repros/; one representative per root cause is minimized
while every original difference remains in the report. Reports preserve original
and minimized source, classification, root cause, seed, interpreter output, and
shrink attempts.
The release and discovery modes fail while a generated must_fix or
should_fix root cause remains open. The pipeline uses
/opt/homebrew/bin/python3 as the fixed CPython oracle, checks it against the
pinned .python-version, and builds mnpy. Promoted behavior still needs focused
cpython_subset, cpython_diff, manifest, coverage, and migration evidence.
Gap reports record both the required pinned CPython version and the actual
oracle/driver interpreter paths so a stale oracle cannot hide in the results.
Use --module to focus a batch run on one affected surface, for example
json, collections.abc, or math.integer. The report keeps interpreter
status separate from workflow triage_status: passing cases, accepted
sandbox/compatibility gaps, and unexpected diffs that need root-cause work are
machine-readable in the JSON output.
Use --root-cause when moving from discovery to repair so a commit can address
one grouped cause while covering all affected cases in the report. JSON reports
also include open_root_causes, the current machine-readable repair queue for
root causes that still have needs_triage cases. The runner enables
--fail-on-open so unexpected open root causes fail the batch while accepted
sandbox/compatibility gaps stay visible in the report. Open root-cause reports
also include the focused tests/run.sh --root-cause ...
command to rerun the grouped repair slice.
Rust host / CLI -----------------> Sandbox API -> MessagePack worker -> Register VM
Python host -> private JSON broker -----------^
A register-based VM with 80+ instructions and 60+ value types.
MIT