bashkit

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:

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#

OperationExample
Open/readopen('f.txt').read()
Open/writeopen('f.txt', 'w').write('data')
Open/appendopen('f.txt', 'a').write('more')
Path openPath('f.txt').open('r')
Read textPath('f.txt').read_text()
Read bytesPath('f.txt').read_bytes()
Write textPath('f.txt').write_text('data')
Write bytesPath('f.txt').write_bytes(b'data')
ExistsPath('f.txt').exists()
Is file/dirPath('f.txt').is_file(), .is_dir()
MkdirPath('d').mkdir(parents=True, exist_ok=True)
DeletePath('f.txt').unlink()
List dirPath('.').iterdir()
StatPath('f.txt').stat().st_size
RenamePath('old').rename('new')
Env varsos.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();
LimitDefaultPurpose
Duration30 secondsExecution timeout
Memory64 MBHeap memory cap, also caps collected print output
Recursion200Call 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.