Minisandbox for Python interpreter.
Project description
DCNR Minisandbox
A secure, lightweight Python sandbox interpreter that allows safe execution of untrusted Python code with strict limitations, plus a global object registry and LPC (Local Procedure Call) system for application data management and anonymous method execution.
Features
- Secure AST-based execution - Uses Python's AST parser to validate and execute code without using
eval()orexec() - Restricted syntax - Only allows a safe subset of Python constructs
- Built-in safety limits - Prevents infinite loops with iteration limits
- Customizable environment - Pre-populate variables and control available functions
- Comprehensive error handling - Clear error messages for syntax and runtime violations
- Global object registry - Hierarchical storage and access system for application objects and data
- LPC (Local Procedure Call) - Anonymous execution of registered methods by name
Installation
pip install dcnr-minisandbox
Components
This package provides three main components:
- Sandbox Interpreter (
SandboxInterpreter,sandbox_exec) - Secure execution of Python code with AST validation - Object Registry (
get_registry,register_object) - Global hierarchical storage for application objects - LPC (Local Procedure Call) (
register_proc,exec) - Anonymous execution of registered methods
Quick Start
Sandbox Execution
The sandbox can be used either via the convenience function sandbox_exec or directly through the SandboxInterpreter class.
from dcnr.minisandbox import sandbox_exec
# Simple calculation
code = """
x = 10
y = 20
result = x + y * 2
"""
variables = {}
result = sandbox_exec(code, variables)
print(result) # {'x': 10, 'y': 20, 'result': 50}
# Pre-populate with variables
initial_vars = {'data': [1, 2, 3, 4, 5]}
code = """
total = sum(data)
average = total / len(data)
"""
result = sandbox_exec(code, initial_vars)
print(result) # {'data': [1, 2, 3, 4, 5], 'total': 15, 'average': 3.0}
# Using attribute access and method calls
code = """
text = "hello world"
upper_text = text.upper()
words = text.split()
data = [1, 2, 3]
data.append(4)
"""
result = sandbox_exec(code, {})
print(result) # {'text': 'hello world', 'upper_text': 'HELLO WORLD', 'words': ['hello', 'world'], 'data': [1, 2, 3, 4]}
Global Object Registry
from dcnr.minisandbox import register_object, get_registry
# Register functions and data
def fetchone():
return {"id": 1, "name": "Alice"}
register_object("database.prod.fetchone", fetchone)
register_object("database.prod.version", "1.0")
register_object("config.debug", True)
# Access through registry
registry = get_registry()
print(registry.database.prod.fetchone()) # {'id': 1, 'name': 'Alice'}
print(registry.database.prod.version) # '1.0'
print(registry.config.debug) # True
LPC (Local Procedure Call)
from dcnr.lpc import register_proc, exec
# Register procedures
def calculate_sum(a, b):
return a + b
def format_message(name, age):
return f"{name} is {age} years old"
register_proc("math.sum", calculate_sum)
register_proc("string.format", format_message)
# Execute procedures anonymously
result = exec("math.sum", 10, 5)
print(result) # 15
message = exec("string.format", "Alice", 30)
print(message) # "Alice is 30 years old"
# Using decorator syntax
from dcnr.lpc import lpc_proc
@lpc_proc("math.multiply")
def multiply(x, y):
return x * y
result = exec("math.multiply", 4, 7)
print(result) # 28
Allowed Features
Data Types
- Numbers:
int,float - Strings:
str - Collections:
list,tuple,dict,set - Booleans:
bool
Operators
- Arithmetic:
+,-,*,/,//,%,** - Comparison:
==,!=,<,<=,>,>=,in,not in,is,is not - Logical:
and,or,not
Control Flow
- Conditional statements:
if,elif,else - Loops:
for,while(with iteration limits) - Loop control:
break,continue
Built-in Functions
- Math:
abs(),min(),max(),sum() - Type conversion:
int(),float(),str(),bool() - Collections:
len(),range(),list(),tuple(),dict(),set(),sorted()
Variable Operations
- Assignment:
x = value - Augmented assignment:
x += 1,x -= 1, etc. - Tuple/list unpacking:
a, b = (1, 2) - Subscript access:
data[0],data[1:3] - Attribute access:
obj.attr,obj.method()
Prohibited Features
For security reasons, the following are not allowed:
- Imports: No
importorfrom ... importstatements - Function definitions: No
def,lambda, orclassdefinitions - Dangerous attribute access: No access to
__class__,__dict__,__module__, etc. - Exception handling: No
try,except,finally,raise - File I/O: No file operations or system calls
- Advanced features: No comprehensions, generators, decorators, or async code
Global Object Registry for storing and accessing application objects, functions, and configuration data.
Registry Features
- Dot-notation access - Navigate the registry like
registry.database.prod.version - Safe path validation - Prevents conflicts with Python keywords and reserved names
- Singleton pattern - Single global registry instance across your application
- Type safety - Automatic wrapping of nested dictionaries for consistent access
Registry API
from dcnr.minisandbox import get_registry, register_object
# Get the global registry instance
registry = get_registry()
# Register objects at various paths
register_object("app.config.debug", True)
register_object("app.database.host", "localhost")
register_object("app.database.port", 5432)
# Alternative: use registry methods directly
registry.register_object("services.auth.enabled", True)
# Access registered objects
print(registry.app.config.debug) # True
print(registry.app.database.host) # 'localhost'
print(registry.services.auth.enabled) # True
# Check if path exists
if registry.has_path("app.config.debug"):
print("Debug mode is configured")
# Get object programmatically
db_host = registry.get_object("app.database.host")
# Remove objects
registry.unregister_object("app.config.debug")
# Clear entire registry
registry.clear()
Path Restrictions
For safety and consistency, certain path components are not allowed:
# ❌ These will raise ValueError:
register_object("app.items.test", 1) # 'items' is reserved (dict method)
register_object("app.__class__.x", 1) # Names starting with '_' not allowed
register_object("app.for.x", 1) # 'for' is a Python keyword
register_object("app..x", 1) # Empty components not allowed
SandboxInterpreter Class
For advanced use cases, use SandboxInterpreter directly. This gives you full control over the interpreter lifecycle, debug logging, and the ability to inject callable functions that interact with the sandbox environment.
Basic Class Usage
from dcnr.minisandbox import SandboxInterpreter
interp = SandboxInterpreter(variables={"x": 10, "y": 5})
env = interp.execute("result = x + y")
print(env["result"]) # 15
# Re-use the same instance for multiple executions
# (environment accumulates across calls)
interp.execute("z = result * 2")
print(interp.env["z"]) # 30
Debug Mode
SandboxInterpreter has a built-in debug buffer that records every statement, its input values and its result in execution order.
from dcnr.minisandbox import SandboxInterpreter
interp = SandboxInterpreter(debug=True)
interp.execute("""
x = 4
y = x * 3
items = [1, 2, 3]
items.append(y)
""")
print(interp.get_debug_log())
Example output:
[Constant]
=> 4
[Assign.Name]
name: 'x'
=> 4
[Assign]
targets: ['x']
value: 4
=> 4
[BinOp]
op: 'Mult'
left: 4
right: 3
=> 12
...
Debug mode can also be toggled at runtime and the buffer cleared between executions:
interp = SandboxInterpreter() # debug off by default
interp.set_debug(True)
interp.execute("a = 1 + 2")
log = interp.get_debug_log()
interp.clear_debug_log() # reset buffer for next run
Registering Callable Functions in the Environment
Any callable placed in the variables dict is directly callable from sandbox code by its key name. This is the primary mechanism for extending the sandbox with host-side logic.
from dcnr.minisandbox import SandboxInterpreter
def fetch_price(product_id):
prices = {1: 9.99, 2: 14.99, 3: 4.49}
return prices.get(product_id, 0.0)
interp = SandboxInterpreter(variables={"fetch_price": fetch_price})
env = interp.execute("""
price = fetch_price(2)
total = price * 3
""")
print(env["total"]) # 44.97
Writing Back to the Sandbox Environment from a Registered Function
A registered function can write values directly into interp.env so those values appear as normal variables in the dict returned by execute(). The cleanest approach is to close over interp.env using a factory function:
from dcnr.minisandbox import SandboxInterpreter
def make_env_writer(env):
"""Return a callable the sandbox can call to write a named variable."""
def set_var(name, value):
env[name] = value
return set_var
interp = SandboxInterpreter()
# Register after creation so the closure captures the live env dict
interp.env["set_var"] = make_env_writer(interp.env)
env = interp.execute("""
set_var("status", "done")
set_var("count", 42)
""")
print(env["status"]) # "done"
print(env["count"]) # 42
A more elaborate example — a host-side function that fetches a record and writes multiple result variables back into the sandbox:
from dcnr.minisandbox import SandboxInterpreter
def make_db_fetch(env):
database = {
1: {"name": "Alice", "score": 95},
2: {"name": "Bob", "score": 82},
}
def db_fetch(record_id):
record = database.get(record_id)
if record:
for key, value in record.items():
env[key] = value # each field becomes a sandbox variable
env["found"] = True
else:
env["found"] = False
return db_fetch
interp = SandboxInterpreter(variables={"user_id": 1})
interp.env["db_fetch"] = make_db_fetch(interp.env)
env = interp.execute("""
db_fetch(user_id)
if found:
result = name + " scored " + str(score)
""")
print(env["result"]) # "Alice scored 95"
TrackedDict
TrackedDict is a dict subclass that records which keys were written or deleted since the last clear_updates() call. Pass it as the variables argument to observe exactly what the sandbox script produced or modified, without inspecting the whole environment.
from dcnr.minisandbox import SandboxInterpreter, TrackedDict
initial = TrackedDict({"x": 10, "y": 5})
interp = SandboxInterpreter(variables=initial)
env = interp.execute("""
result = x + y
label = "sum"
""")
# Only keys written during execution are reported
for key, value in env.updated_items():
print(f" {key} = {value!r}")
# result = 15
# label = 'sum'
print(env.deleted_keys()) # []
Resetting the Tracker Between Runs
Call clear_updates() before each execute() call to get only the changes from that specific run:
env.clear_updates()
interp.execute("z = result * 2")
new_writes = dict(env.updated_items())
print(new_writes) # {'z': 20}
Combining TrackedDict with a Registered Writer Function
TrackedDict works transparently with the env-writer pattern — writes made by the registered function are tracked just like writes made by sandbox statements:
from dcnr.minisandbox import SandboxInterpreter, TrackedDict
tracked = TrackedDict()
interp = SandboxInterpreter(variables=tracked)
interp.env["set_var"] = lambda name, value: tracked.__setitem__(name, value)
interp.execute("""
set_var("api_result", 42)
total = api_result * 2
""")
print(dict(tracked.updated_items()))
# {'api_result': 42, 'total': 84}
TrackedDict API Reference
| Method | Description |
|---|---|
updated_items() |
Generator of (key, value) for every key written since last clear_updates() |
deleted_keys() |
List of keys deleted since last clear_updates() |
clear_updates() |
Reset both the written and deleted sets |
LPC (Local Procedure Call)
The LPC package provides anonymous execution of registered callable methods, enabling dynamic procedure dispatch without direct references to the implementation.
LPC Features
- Anonymous execution - Call procedures by name without importing or referencing them directly
- Dynamic registration - Register procedures at runtime from any part of your application
- Decorator support - Use
@lpc_procdecorator for clean registration syntax - Type safety - Validates that registered objects are callable
- Error handling - Clear exceptions for missing procedures or execution failures
- Procedure management - List, check, and remove registered procedures
Core API
Registration Functions
from dcnr.lpc import register_proc, lpc_proc
# Function registration
def add_numbers(a, b):
return a + b
register_proc("math.add", add_numbers)
# Decorator registration
@lpc_proc("math.subtract")
def subtract_numbers(a, b):
return a - b
# Lambda registration
register_proc("math.square", lambda x: x ** 2)
Execution Function
from dcnr.lpc import exec
# Execute registered procedures
result1 = exec("math.add", 10, 5) # 15
result2 = exec("math.subtract", 10, 5) # 5
result3 = exec("math.square", 4) # 16
# With keyword arguments
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
register_proc("utils.greet", greet)
message = exec("utils.greet", "Alice", greeting="Hi") # "Hi, Alice!"
Management Functions
from dcnr.lpc import has_proc, list_procs, proc_count, unregister_proc, clear_procs
# Check if procedure exists
if has_proc("math.add"):
print("Math add procedure is available")
# List all registered procedures
procedures = list_procs()
print("Available procedures:", procedures)
# Get procedure count
count = proc_count()
print(f"Total procedures: {count}")
# Remove specific procedure
unregister_proc("math.add")
# Clear all procedures
clear_procs()
Advanced Usage
Procedure Pipelines
from dcnr.lpc import register_proc, exec
# Register data processing pipeline
register_proc("data.load", lambda: [1, 2, 3, 4, 5])
register_proc("data.filter_even", lambda data: [x for x in data if x % 2 == 0])
register_proc("data.sum", lambda data: sum(data))
# Execute pipeline
raw_data = exec("data.load")
filtered_data = exec("data.filter_even", raw_data)
result = exec("data.sum", filtered_data)
print(result) # 6 (2 + 4)
Conditional Procedure Execution
from dcnr.lpc import exec, has_proc
def safe_exec(proc_name, *args, **kwargs):
if has_proc(proc_name):
return exec(proc_name, *args, **kwargs)
else:
print(f"Procedure '{proc_name}' not available")
return None
# Safe execution
result = safe_exec("math.add", 1, 2) # Works if registered
result = safe_exec("missing.proc", 1, 2) # Prints warning, returns None
Dynamic Procedure Loading
from dcnr.lpc import register_proc, exec
# Dynamic loading based on configuration
config = {
"processors": ["data.clean", "data.transform", "data.validate"]
}
def clean_data(data):
return [x for x in data if x is not None]
def transform_data(data):
return [x * 2 for x in data]
def validate_data(data):
return all(isinstance(x, (int, float)) for x in data)
# Register based on configuration
processors = {
"data.clean": clean_data,
"data.transform": transform_data,
"data.validate": validate_data
}
for proc_name in config["processors"]:
if proc_name in processors:
register_proc(proc_name, processors[proc_name])
# Execute processing chain
data = [1, None, 3, 4.5]
for proc_name in config["processors"]:
data = exec(proc_name, data)
print(data) # [2, 6, 9.0] (cleaned, transformed, validated)
Error Handling
The LPC package provides specific exception types for different error scenarios:
from dcnr.lpc import exec, register_proc
from dcnr.lpc import ProcedureNotFoundError, ProcedureExecutionError, LPCError
# Handle procedure not found
try:
result = exec("nonexistent.procedure", 1, 2, 3)
except ProcedureNotFoundError as e:
print(f"Procedure not found: {e}")
# Handle execution errors
def risky_procedure(x):
if x < 0:
raise ValueError("Negative values not allowed")
return x * 2
register_proc("math.risky", risky_procedure)
try:
result = exec("math.risky", -5)
except ProcedureExecutionError as e:
print(f"Procedure execution failed: {e}")
# Original exception available via e.__cause__
# Handle registration errors
try:
register_proc("invalid.proc", "not_a_function")
except TypeError as e:
print(f"Registration failed: {e}")
# Catch all LPC errors
try:
result = exec("some.procedure", arg1, arg2)
except LPCError as e:
print(f"LPC error occurred: {e}")
Best Practices
Naming Conventions
Use hierarchical naming for better organization:
# Good naming patterns
register_proc("auth.login", login_user)
register_proc("auth.logout", logout_user)
register_proc("data.users.create", create_user)
register_proc("data.users.update", update_user)
register_proc("utils.validation.email", validate_email)
register_proc("utils.formatting.currency", format_currency)
Error-Safe Execution
from dcnr.lpc import exec, has_proc, ProcedureNotFoundError
def safe_procedure_call(proc_name, *args, default=None, **kwargs):
"""Execute procedure with fallback to default value."""
try:
if has_proc(proc_name):
return exec(proc_name, *args, **kwargs)
else:
print(f"Warning: Procedure '{proc_name}' not registered")
return default
except Exception as e:
print(f"Error executing '{proc_name}': {e}")
return default
# Usage
result = safe_procedure_call("math.divide", 10, 2, default=0)
Procedure Documentation
from dcnr.lpc import register_proc, list_procs
def documented_procedure(x, y):
"""
Add two numbers together.
Args:
x: First number
y: Second number
Returns:
Sum of x and y
"""
return x + y
register_proc("math.add", documented_procedure)
# Access documentation
from dcnr.lpc import get_proc
proc = get_proc("math.add")
print(proc.__doc__) # Prints the docstring
Error Handling
Sandbox Errors
The sandbox raises specific exceptions for different types of violations:
from dcnr.minisandbox import sandbox_exec, SandboxSyntaxError, SandboxRuntimeError
try:
# This will raise SandboxSyntaxError
sandbox_exec("import os", {})
except SandboxSyntaxError as e:
print(f"Syntax violation: {e}")
try:
# This will raise SandboxRuntimeError
sandbox_exec("unknown_variable", {})
except SandboxRuntimeError as e:
print(f"Runtime error: {e}")
Registry Errors
The registry raises standard Python exceptions for invalid operations:
from dcnr.minisandbox import register_object, get_registry
try:
# Invalid path component
register_object("app.for.test", 1) # 'for' is a Python keyword
except ValueError as e:
print(f"Invalid path: {e}")
try:
# Path not found
registry = get_registry()
value = registry.get_object("nonexistent.path")
except KeyError as e:
print(f"Path not found: {e}")
Safety Features
Iteration Limits
Loops are automatically limited to 100,000 iterations to prevent infinite loops:
# This will raise SandboxRuntimeError after 100,000 iterations
code = """
i = 0
while True:
i += 1
"""
Memory Safety
Only safe data types and operations are allowed. No access to system resources or dangerous built-ins.
Use Cases
Sandbox Execution
- Educational platforms - Safe execution of student code
- Code challenges - Running untrusted submissions
- Configuration scripts - Controlled execution of user-defined logic
- Expression evaluation - Safe calculation of mathematical expressions
- Templating - Dynamic value computation in templates
Object Registry
- Application configuration - Centralized storage of settings and parameters
- Service registration - Registry for application services and dependencies
- Plugin systems - Dynamic registration and discovery of plugins
- Feature flags - Hierarchical feature toggle management
- Resource management - Centralized access to database connections, APIs, etc.
LPC (Local Procedure Call)
- Plugin architectures - Dynamic loading and execution of plugin methods
- Microservices - Anonymous procedure calls between service components
- Workflow engines - Step-by-step execution of named procedures in workflows
- API routing - Map request paths to procedure implementations
- Command pattern - Decouple command invocation from implementation
- Data processing pipelines - Chain procedures for data transformation
- Event handling - Register and execute event handlers by name
- Configuration-driven execution - Execute procedures based on configuration files
Limitations
- No custom function definitions
- No module imports or external libraries
- Limited to basic Python constructs
- No file system or network access
- Fixed iteration limits for loops
Development
To contribute to dcnr-minisandbox:
- Clone the repository
- Install development dependencies
- Run tests with your changes
- Submit a pull request
License
This project is licensed under the MIT License - see the LICENSE file for details.
Author
Peter Kollath (peter.kollath@gopal.home.sk)
Links
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file dcnr_minisandbox-1.0.7.tar.gz.
File metadata
- Download URL: dcnr_minisandbox-1.0.7.tar.gz
- Upload date:
- Size: 33.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cd9e14b9bd8fe6a7ae403bb21856638f1e23c5bc50d1a66b8b43f0710504b91b
|
|
| MD5 |
d7d17b8d51366826be0fd83436829ad7
|
|
| BLAKE2b-256 |
9cbcd32fa120fdecc3864a14de8d8ae992913ca427a1d9351f81f263e62aa9a6
|
File details
Details for the file dcnr_minisandbox-1.0.7-py3-none-any.whl.
File metadata
- Download URL: dcnr_minisandbox-1.0.7-py3-none-any.whl
- Upload date:
- Size: 30.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
88674f8c15b7e112158960733f5223a51081bc6b7ad618c3084480c95791730a
|
|
| MD5 |
1d618871b4de54bc03370a202a4bc555
|
|
| BLAKE2b-256 |
561de4fc782e3a6cb3f699292f723eb4e68c8a59defd3dc85a9a89a7af7d6cf5
|