Embedded Python (Monty)#
Experimental. Monty is an early-stage Python interpreter that may have undiscovered crash or security bugs. Resource limits are enforced by Monty’s runtime and a per-VM allocator meter. The integration should be treated as experimental.
Bashkit embeds the Monty Python interpreter, a pure-Rust implementation of Python 3.12. Python runs entirely in-memory with configurable resource limits and no host access.
See also:
- Embedded CPython (WebAssembly) - Full CPython 3.14 in a wasm sandbox (
cpythonfeature) - Threat Model - Security considerations (TM-PY-*)
- Custom Builtins - Writing your own builtins
- Compatibility Reference - Bash feature support
knowledge/runtimes/python-builtin.md- Full specification
Quick Start#
Enable the python feature, install bashkit::MeteredAllocator as the global
allocator (Monty’s memory limit reads it; without it python3 refuses to run),
and register via builder:
use bashkit::Bash;
#[global_allocator]
static ALLOC: bashkit::MeteredAllocator = bashkit::MeteredAllocator;
let mut bash = Bash::builder()
.python()
.env("BASHKIT_ALLOW_INPROCESS_PYTHON", "1")
.build();
let result = bash.exec("python3 -c \"print('hello from Monty')\"").await?;
assert_eq!(result.stdout, "hello from Monty\n");
Usage Patterns#
Shared Tool Registry#
With the bashkit-scripted-tool crate
(feature python), one ToolRegistry can expose the same definitions and
callbacks as shell commands and Python functions:
use bashkit::Bash;
use bashkit_scripted_tool::{ToolArgs, ToolDef, ToolRegistry};
let registry = ToolRegistry::builder()
.tool_fn(ToolDef::new("orders.list", "List orders"), |_args: &ToolArgs| {
Ok(r#"[{"id": 1}]"#.to_string())
})
.build();
let bash = registry.install(Bash::builder()).build();
orders = tools.orders.list({"customer": "acme"})
matches = tools.discover({"category": "orders"})
Schema validation, policy, deadlines, sanitized errors, callbacks, and tracing
are shared with shell and TypeScript calls. Python execution still requires the
normal BASHKIT_ALLOW_INPROCESS_PYTHON=1 operator opt-in.
Inline Code#
python3 -c "print(2 ** 10)"
# Output: 1024
Expression Values Are Not Echoed#
Like CPython, python3 -c, script files and stdin do not print the value of
the last expression. Use print():
python3 -c "2 + 2" # prints nothing
python3 -c "print(2 + 2)" # Output: 4
Script Files (from VFS)#
cat > /tmp/script.py << 'EOF'
data = [1, 2, 3, 4, 5]
print(f"sum={sum(data)}, avg={sum(data)/len(data)}")
EOF
python3 /tmp/script.py
Pipelines and Command Substitution#
result=$(python3 -c "print(42 * 3)")
echo "Result: $result"
echo "print('piped')" | python3
Virtual Filesystem (VFS) Bridging#
Python open() and pathlib.Path operations are bridged to Bashkit’s virtual
filesystem. Files created by bash are readable from Python and vice versa.
Bash → Python#
echo "important data" > /tmp/shared.txt
python3 -c "
content = open('/tmp/shared.txt').read()
print(f'Got: {content.strip()}')
"
Python → Bash#
python3 -c "
with open('/tmp/result.txt', 'w') as f:
_ = f.write('computed by python\n')
"
cat /tmp/result.txt
Supported File Operations#
| Operation | Example |
|---|---|
| Open/read | open('f.txt').read() |
| Open/write | open('f.txt', 'w').write('data') |
| Open/append | open('f.txt', 'a').write('more') |
| Path open | Path('f.txt').open('r') |
| Read text | Path('f.txt').read_text() |
| Read bytes | Path('f.txt').read_bytes() |
| Write text | Path('f.txt').write_text('data') |
| Write bytes | Path('f.txt').write_bytes(b'data') |
| Exists | Path('f.txt').exists() |
| Is file/dir | Path('f.txt').is_file(), .is_dir() |
| Mkdir | Path('d').mkdir(parents=True, exist_ok=True) |
| Delete | Path('f.txt').unlink() |
| List dir | Path('.').iterdir() |
| Stat | Path('f.txt').stat().st_size |
| Rename | Path('old').rename('new') |
| Env vars | os.getenv('KEY'), os.environ |
Architecture#
Python code → Monty VM → OsCall(Open/ReadText, path) → Bashkit VFS → resume
Monty pauses at filesystem operations, Bashkit bridges them to the VFS, then
resumes execution with the result (or a Python exception like FileNotFoundError).
Resource Limits#
Default limits prevent runaway Python code. Customize via PythonLimits:
use bashkit::{Bash, 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) // 16 MB
.max_recursion(50)
)
.build();
| Limit | Default | Purpose |
|---|---|---|
| Duration | 30 seconds | Execution timeout |
| Memory | 64 MB | Heap memory cap, also caps collected print output |
| Recursion | 200 | Call stack depth |
Memory is counted per run from real allocator bytes, so concurrent runs in one
process (one per tenant, for example) never share or steal each other’s
budget, and hitting the cap raises MemoryError in that run only. This needs
bashkit::MeteredAllocator as the global allocator of your binary; the CLI and
the Python, Node and C bindings already install it. It wraps the system
allocator and only does work while a Monty run is executing on the thread.
LLM Tool Integration#
When using BashTool for AI agents, call .python() on the tool builder:
use bashkit::{BashTool, Tool};
let tool = BashTool::builder()
.python()
.build();
// help() and system_prompt() automatically document Python limitations
let help = tool.help(); // Includes a Markdown Notes section with Python hints
The builtin’s llm_hint() is automatically included in the tool’s documentation,
so LLMs know file I/O is VFS-scoped, HTTP is unavailable and classes are limited.
BashTool::builder().python_with_external_handler(limits, names, handler) exposes the same named callbacks through the Rust tool builder. The Python runtime gate still applies. Foreign bindings can use bashkit::runtime_value for bounded JSON conversion; native Rust callbacks keep their Monty value types.
Limitations#
VFS-only file I/O. open() and pathlib.Path read/write Bashkit’s virtual
filesystem, not the host filesystem:
from pathlib import Path
with open('/tmp/data.txt', 'w') as f:
f.write('hello')
content = Path('/tmp/data.txt').read_text()
No HTTP/network. No socket, urllib, requests, or http.client modules.
Monty has no network primitives and no OsCall variants for network operations.
Simple classes only. Classes with methods and attributes work; inheritance,
metaclasses and match statements are not supported by Monty yet.
No third-party imports. Only Monty’s bundled modules (json, math, re,
datetime, pathlib, os, sys, typing, csv, collections, itertools,
functools, dataclasses, hashlib, base64, random, decimal,
unicodedata) are available. No pip install, no import numpy.
re is bounded. Plain patterns run on a linear-time engine; patterns with
backreferences or lookaround hit a backtracking cap (re.PatternError) rather
than hanging, and loops of matches stop at the time limit.
Fixed clock. datetime.now() and date.today() return a fixed virtual
instant (2024-01-01), and time.sleep returns immediately.
Security#
All Python execution runs in a virtual environment:
- No host filesystem access: all paths resolve through the VFS
- No network access: no sockets, HTTP, or DNS
- No process spawning: no
os.system(),subprocess, or__import__('os') - Resource limited: time, memory (including collected output), and recursion caps
- Path traversal safe:
../..is resolved by VFS path normalization
See threat IDs TM-PY-001 through TM-PY-035 in the threat model.