Experimental. Monty is an early-stage Python interpreter that may have undiscovered crash or security bugs. Resource limits are enforced by Monty's runtime. Do not rely on it for untrusted-input safety without additional hardening.
Implemented (experimental)
BashKit provides sandboxed Python execution via python and python3 builtins,
powered by the Monty embedded Python
interpreter written in Rust.
Enable with:
[dependencies]
bashkit = { version = "0.1", features = ["python"] }Python builtins are not auto-registered. Enable via builder:
use bashkit::Bash;
// Default limits
let bash = Bash::builder()
.python()
.env("BASHKIT_ALLOW_INPROCESS_PYTHON", "1")
.build();
// Custom limits
use bashkit::PythonLimits;
use std::time::Duration;
let bash = Bash::builder()
.python_with_limits(
PythonLimits::default()
.max_duration(Duration::from_secs(5))
.max_memory(16 * 1024 * 1024)
)
.build();The python feature flag enables compilation; .python() on the builder enables
registration. This matches the git pattern (Bash::builder().git(config).build()).
- Pure Rust, no CPython dependency
- Sub-microsecond startup
- Built-in resource limits (memory, allocations, time, recursion depth)
- No filesystem/network access by design (sandbox-safe)
- Snapshotable execution state
# Inline code
python3 -c "print('hello')"
# Expression evaluation (REPL-like: last expression printed)
python3 -c "2 + 2"
# Script file (from VFS)
python3 script.py
# Stdin
echo "print('hello')" | python3
python3 - <<< "print('hi')"
# Version
python3 --version
python3 -VMonty enforces its own resource limits independent of BashKit's shell limits.
All limits are configurable via PythonLimits:
| Limit | Default | Builder Method | Purpose |
|---|---|---|---|
| Max allocations | 1,000,000 | .max_allocations(n) |
Prevent memory exhaustion |
| Max duration | 30 seconds | .max_duration(d) |
Prevent infinite loops |
| Max memory | 64 MB | .max_memory(bytes) |
Prevent memory exhaustion |
| Max recursion | 200 | .max_recursion(depth) |
Prevent stack overflow |
use bashkit::PythonLimits;
use std::time::Duration;
// Tighter limits for untrusted code
let limits = PythonLimits::default()
.max_duration(Duration::from_secs(5))
.max_memory(16 * 1024 * 1024) // 16 MB
.max_allocations(100_000)
.max_recursion(50);Monty implements a subset of Python 3.12:
Supported:
- Variables, assignments, augmented assignments
- Arithmetic, comparison, logical operators
- Control flow: if/elif/else, for, while, break, continue
- Functions: def, return, default args, *args, **kwargs
- Star unpacking in calls:
func(*args, **kwargs)(since Monty 0.0.4) - PEP 448 generalised unpacking in literals:
[*a, *b],{**d1, **d2}(since Monty 0.0.8) - Data structures: list, dict, tuple, set, frozenset, namedtuple
- Dict constructor:
dict(iterable),dict(zip(...)),dict(iterable, **kwargs)(since Monty 0.0.8) - Tuple comparison:
<,>,<=,>=(since Monty 0.0.8) - Dict view and set/frozenset operators (since Monty 0.0.8)
strandbytescomparison operators (since Monty 0.0.8)- List/dict/set comprehensions, generator expressions
- String operations, f-strings
- Exception handling: try/except/finally/raise
- Property descriptors (
@property) (since Monty 0.0.4) - Built-in functions: print, len, range, enumerate, zip, map, filter, sorted, reversed, sum, min, max, abs, round, int, float, str, bool, list, dict, tuple, set, type, isinstance, hasattr, getattr, id, repr, ord, chr, hex, oct, bin, all, any, input
- Standard modules: sys, typing, math (~50 functions), pathlib, os (getenv/environ), json, datetime
datetime.date.today(),datetime.datetime.now()with optional timezone (since Monty 0.0.11)- JSON:
json.dumps(),json.loads()(since Monty 0.0.9) - Multi-module imports:
import a, b, c(since Monty 0.0.10) TYPE_CHECKINGguard support (since Monty 0.0.4)
Not supported (Monty limitations):
- Classes (planned upstream)
- Match statements
- Import of third-party libraries
open()builtin — Monty does not implementopen(). Usepathlib.Pathinstead:Path('f.txt').read_text()notopen('f.txt').read()- HTTP/network I/O — no
socket,urllib,requests,http.clientmodules. Monty has no OsCall variants for network operations, so there is no way to bridge these. - Most standard library modules
Python pathlib.Path operations are bridged to BashKit's virtual filesystem
via Monty's OsCall pause/resume mechanism. This enables Python code to read
and write files that are shared with the bash environment.
# Write from bash, read from Python
echo "data" > /tmp/shared.txt
python3 -c "from pathlib import Path; print(Path('/tmp/shared.txt').read_text())"
# Write from Python, read from bash
python3 -c "from pathlib import Path; Path('/tmp/out.txt').write_text('hello\n')"
cat /tmp/out.txt
# Create directories, check existence
python3 -c "from pathlib import Path
Path('/tmp/new').mkdir()
print(Path('/tmp/new').is_dir())"
# Environment variables
python3 -c "import os; print(os.getenv('HOME'))"Supported operations:
Path.read_text(),Path.read_bytes()— read from VFSPath.write_text(),Path.write_bytes()— write to VFSPath.exists(),Path.is_file(),Path.is_dir(),Path.is_symlink()Path.mkdir()(withparents=True,exist_ok=Truekwargs)Path.unlink(),Path.rmdir()— delete from VFSPath.iterdir()— list directory contentsPath.stat()— file metadata (st_size, st_mode, st_mtime, etc.)Path.rename()— move/renamePath.resolve(),Path.absolute()— path resolutionos.getenv(),os.environ— environment variable accessdatetime.date.today()— current date from host systemdatetime.datetime.now(tz=None)— current datetime (naive or timezone-aware)
Architecture:
Python code → Monty VM → OsCall(ReadText, path) → BashKit VFS → resume
Monty pauses execution at filesystem operations, yields an OsCall event
with the operation type and arguments, BashKit bridges it to the VFS, and
resumes execution with the result (or a Python exception).
Note: Monty 0.0.10+ includes native filesystem mounting (
MountTable,MountDir,MountMode) that can handle file operations directly against host directories. BashKit uses the OsCall bridge instead because our VFS is in-memory and may not be backed by host directories. The native mount system is suited for real-filesystem use cases where Monty is used standalone.
Monty runs directly in the host process. Resource limits (memory, allocations, time, recursion) are enforced by Monty's own runtime. Since Monty 0.0.4, the parser enforces a nesting depth limit (200 in release, 35 in debug) to prevent stack overflow from deeply nested expressions.
use bashkit::{Bash, PythonLimits};
// Default limits
let bash = Bash::builder()
.python()
.env("BASHKIT_ALLOW_INPROCESS_PYTHON", "1")
.build();
// Custom limits
let bash = Bash::builder()
.python_with_limits(PythonLimits::default().max_duration(Duration::from_secs(5)))
.env("BASHKIT_ALLOW_INPROCESS_PYTHON", "1")
.build();For security, python/python3 execution is runtime-gated and requires
BASHKIT_ALLOW_INPROCESS_PYTHON=1 (builder .env(...) or export).
Host applications can register async external function handlers that Python code
can call by name. This enables Python scripts to invoke host-provided capabilities
(e.g., tool calls, data lookups) without serialization overhead — arguments arrive
as raw MontyObject values.
Builder API:
use bashkit::{Bash, PythonLimits, PythonExternalFnHandler};
use bashkit::{MontyObject, ExtFunctionResult};
use std::sync::Arc;
let handler: PythonExternalFnHandler = Arc::new(|name, args, kwargs| {
Box::pin(async move {
ExtFunctionResult::Return(MontyObject::Int(42))
})
});
let bash = Bash::builder()
.python_with_external_handler(
PythonLimits::default(),
vec!["get_answer".into()],
handler,
)
.build();Handler signature: (function_name: String, positional_args: Vec<MontyObject>, keyword_args: Vec<(MontyObject, MontyObject)>) -> Pin<Box<dyn Future<Output = ExtFunctionResult> + Send>>
Return values:
ExtFunctionResult::Return(MontyObject)— success, value returned to PythonExtFunctionResult::Error(MontyException)— raises a Python exception
Dispatch: A single handler receives all registered function names; dispatch by
function_name inside the handler.
Trust model: External function handlers follow the same trust model as
BashBuilder::builtin() and ScriptedTool callbacks — the host application
registers trusted Rust code, untrusted scripts invoke it by name.
Unstable re-exports: MontyObject, ExtFunctionResult, MontyException, and
ExcType are re-exported from the monty crate (git-pinned, not on crates.io).
These types may change in breaking ways between bashkit releases.
See specs/threat-model.md section "Python / Monty Security (TM-PY)"
for the full threat analysis.
Bash variables are expanded before reaching the Python builtin. This is
by-design consistent with all other builtins. Use single quotes to prevent
expansion: python3 -c 'print("hello")'.
Monty enforces independent resource limits. Even if BashKit's shell limits are generous, Python code cannot exceed Monty's allocation/time/memory caps.
All pathlib.Path operations go through BashKit's virtual filesystem.
Python code cannot access the real host filesystem. /etc/passwd in Python
reads from VFS (where it doesn't exist), not the host.
Monty has no os.system(), subprocess, or socket implementations.
These modules raise errors when used.
Python print output is captured in memory. The 64 MB memory limit on Monty prevents unbounded output generation.
Relative paths are resolved against the shell's cwd. Path traversal via
../.. is constrained by the VFS's path normalization.
- Syntax errors: Exit code 1, Python traceback on stderr
- Runtime errors: Exit code 1, Python traceback on stderr, any stdout produced before error preserved
- File not found: Exit code 2, error on stderr
- Missing
-cargument: Exit code 2, error on stderr - Unknown option: Exit code 2, error on stderr
When Python is registered via BashToolBuilder::python(), the builtin contributes
a hint to help() and system_prompt() documenting its limitations:
python/python3: Embedded Python (Monty). Stdlib: math, pathlib, os.getenv, sys, typing. File I/O via pathlib.Path only (no open()). No HTTP/network. No classes. No third-party imports.
Regex module re is intentionally disabled in BashKit due to catastrophic
backtracking DoS risk in untrusted code execution.
This uses the general Builtin::llm_hint() mechanism — any builtin can provide
hints that are automatically deduplicated and included in LLM-facing documentation.
python/python3both map to the same builtin- Works in pipelines:
echo "data" | python3 -c "import sys; ..."- Note: stdin piping provides code, not data (matches real python behavior for no-arg invocation)
- Works in command substitution:
result=$(python3 -c "print(42)") - Works in conditionals:
if python3 -c "1/0"; then ... else ... fi - Shebang lines (
#!/usr/bin/env python3) are stripped automatically
# Build with python feature
cargo build --features python
# Run unit tests
cargo test --features python --lib -- python
# Run spec tests
cargo test --features python --test spec_tests -- python
# Run security tests
cargo test --features python --test threat_model_tests -- python