Sandbox Execution Examples¶
Basic Sandbox Usage¶
from agent_airlock import Airlock
@Airlock(sandbox=True)
def run_code(code: str) -> str:
"""Execute arbitrary Python code safely."""
result = eval(code)
return str(result)
# Executes in E2B sandbox, not your server
result = run_code(code="2 + 2")
print(result) # "4"
# Even dangerous code is safe
result = run_code(code="__import__('os').getcwd()")
# Returns sandbox directory, not your server's
Required Sandbox¶
from agent_airlock import Airlock
@Airlock(sandbox=True, sandbox_required=True)
def dangerous_operation(code: str) -> str:
"""This MUST run in sandbox - no fallback."""
return exec(code)
# If E2B is unavailable, the call returns a blocked response instead of running locally
result = dangerous_operation(code="import os; os.listdir('/')")
if isinstance(result, dict) and result.get("status") == "blocked":
print(f"Sandbox unavailable: {result['error']}")
Sandbox Timeout¶
sandbox_timeout is how long each E2B sandbox lives, not a per-call limit, and every
sandboxed call in the process shares one pool, built from the first sandboxed call's config.
Set it once and give every sandboxed tool the same config (see the
E2B Sandbox guide):
from agent_airlock import Airlock, AirlockConfig
# Each pooled sandbox lives 300 seconds from its creation
config = AirlockConfig(sandbox_timeout=300)
@Airlock(sandbox=True, config=config)
def quick_calc(expr: str) -> float:
return eval(expr)
@Airlock(sandbox=True, config=config)
def train_model(data: list) -> dict:
# Long-running ML task
import time
time.sleep(60) # Simulate training
return {"accuracy": 0.95}
Sandbox Pool Management¶
from agent_airlock import AirlockConfig
from agent_airlock.sandbox import execute_in_sandbox, get_sandbox_pool
config = AirlockConfig(sandbox_pool_size=2) # warm_up() creates 2 sandboxes
# The pool execute_in_sandbox() and @Airlock(sandbox=True, config=config) use for this config
pool = get_sandbox_pool(config)
pool.warm_up() # Create them now instead of on the first two calls
def process_code(code: str) -> str:
return str(eval(code))
# Execute using pool
result = execute_in_sandbox(process_code, args=("2 + 2",), config=config)
print(result.result) # 4
# Cleanup when done
pool.shutdown()
Each sandbox runs one call and is killed after it, so the two warm sandboxes cover the first
two calls; later calls create their own until warm_up() runs again. The pool does not cap
concurrency (an empty pool creates another sandbox) and keeps no statistics; see
Monitoring Sandbox Usage.
Error Handling¶
from agent_airlock import Airlock
@Airlock(sandbox=True, return_dict=True)
def risky_code(code: str) -> str:
return str(eval(code))
result = risky_code(code="1/0") # Division by zero, inside the sandbox
if not result["success"]:
print(result["status"]) # "blocked"
print(result["error"]) # "AIRLOCK_BLOCK: Unexpected error in 'risky_code'"
A decorated tool never raises for a sandbox failure. An exception inside the sandbox returns
this blocked response, the same one the tool returns when it raises outside a sandbox, and the
underlying error is logged as the unexpected_error event. A missing E2B SDK or API key, or
an E2B error, returns "block_reason": "sandbox_error" with
"AIRLOCK_BLOCK: 'risky_code' could not run in its sandbox", logged as the sandbox_failed
event. execute_in_sandbox returns the sandbox's own error text:
from agent_airlock.sandbox import execute_in_sandbox
def risky_code(code: str) -> str:
return str(eval(code))
result = execute_in_sandbox(risky_code, args=("1/0",))
print(result.success) # False
print(result.error) # "ZeroDivisionError: division by zero"
There is no timeout exception and no is_sandboxed() helper: a tool with sandbox=True
returns either its sandboxed result or the blocked response (see
Required Sandbox for the one local-fallback case).
Data Processing in Sandbox¶
from agent_airlock import Airlock
@Airlock(sandbox=True)
def process_data(data: list[dict]) -> dict:
"""Process user-provided data safely."""
total = sum(item.get("value", 0) for item in data)
count = len(data)
return {
"total": total,
"count": count,
"average": total / count if count > 0 else 0,
}
data = [
{"value": 10},
{"value": 20},
{"value": 30},
]
result = process_data(data=data)
print(result)
# {"total": 60, "count": 3, "average": 20.0}
Combining Sandbox with Other Features¶
from agent_airlock import Airlock, AirlockConfig, SecurityPolicy
config = AirlockConfig(
strict_mode=True,
sanitize_output=True,
mask_secrets=True,
sandbox_timeout=30,
)
policy = SecurityPolicy(
rate_limits={"execute_*": "10/minute"},
)
@Airlock(sandbox=True, config=config, policy=policy)
def execute_script(script: str) -> dict:
"""
Execute script with all protections:
- Strict mode (no ghost args)
- Sandbox execution (isolated)
- Output sanitization (no secrets leaked)
- Rate limiting (prevent abuse)
"""
result = exec(script)
return {"result": str(result)}
Direct Sandbox Execution¶
from agent_airlock.sandbox import execute_in_sandbox
def my_function(x: int, y: int) -> int:
return x + y
# Execute directly without decorator
result = execute_in_sandbox(
my_function,
args=(5, 3),
kwargs={},
)
print(result.result) # 8
execute_in_sandbox returns a SandboxResult, not the bare value: check result.success
before reading result.result. It takes no timeout argument.
Async Sandbox Execution¶
An async def tool can take sandbox=True. Its coroutine is awaited inside the sandbox, and
awaiting the decorated tool returns what it returned:
import asyncio
from agent_airlock import Airlock
@Airlock(sandbox=True)
async def count_words(text: str) -> int:
await asyncio.sleep(0) # runs on an event loop inside the sandbox
return len(text.split())
print(asyncio.run(count_words(text="one two three"))) # 3
Until 0.10.17 the sandbox never awaited the coroutine, and the tool returned the string form
of a coroutine object ('<coroutine object ...>').
Without the decorator, await execute_in_sandbox_async, which runs the sandbox call in a
worker thread:
import asyncio
from agent_airlock.sandbox import execute_in_sandbox_async
def process(data: str) -> dict:
"""Runs in the sandbox."""
return {"processed": data.upper()}
async def main():
result = await execute_in_sandbox_async(process, args=("hello",))
print(result.result) # {'processed': 'HELLO'}
asyncio.run(main())
Monitoring Sandbox Usage¶
The pool keeps no statistics. Each SandboxResult from execute_in_sandbox carries
success, execution_time_ms and sandbox_id, so aggregate those:
from agent_airlock.sandbox import execute_in_sandbox
def square(x: int) -> int:
return x * x
results = [execute_in_sandbox(square, args=(n,)) for n in range(5)]
ok = [r for r in results if r.success]
print(f"Total executions: {len(results)}")
print(f"Success rate: {len(ok) / len(results):.1%}")
if ok:
print(f"Average latency: {sum(r.execution_time_ms for r in ok) / len(ok):.0f}ms")
print(f"Sandboxes used: {len({r.sandbox_id for r in ok})}")
The pool also logs sandbox_created (with elapsed_ms) each time it creates a sandbox, and
the decorator logs sandbox_execution_success (with sandbox_id and execution_time_ms) for
each sandboxed call.