6.3 KiB
Sandbox Infrastructure
Kotaemon's sandbox layer provides a safe, flexible execution environment for agent-driven code and file operations. It is agent-agnostic — any reasoning strategy (ReAct, ReWOO, CodeAct, etc.) can use the sandbox to interact with the host filesystem or evaluate Python code without touching lower-level subprocess APIs directly.
Architecture Overview
The sandbox is split into two complementary abstractions with distinct responsibilities:
kotaemon.agents.sandbox
├── base.py → SandboxBackendProtocol + BaseSandbox (stateless file ops)
├── local.py → LocalSandbox (concrete: local host)
└── repl.py → LocalPythonInterpreter (stateful Python REPL)
1. Stateless Sandbox — BaseSandbox / LocalSandbox
Provides safe file-system primitives (ls, read, write, edit, grep,
glob) that any agent tool can call. Every operation is implemented as a
self-contained python3 -c "..." script executed via the abstract execute()
method. Paths and content payloads are always Base64-encoded before being
interpolated into the command string, completely preventing shell injection via
user-controlled filenames or content.
from kotaemon.agents.sandbox import LocalSandbox
sb = LocalSandbox()
result = sb.read("/path/to/file.txt")
sb.edit("/path/to/file.txt", old_string="foo", new_string="bar")
2. Stateful Python REPL — LocalPythonInterpreter
Designed for the CodeAct reasoning strategy where the LLM generates iterative Python code blocks and variable state must persist between steps:
from kotaemon.agents.sandbox import LocalPythonInterpreter
with LocalPythonInterpreter() as interp:
interp("df = pd.read_csv('data.csv')") # step 1: load data
result = interp("print(df.describe())") # step 2: df still in scope ✓
A single python -i subprocess is kept alive across calls. A sentinel-based
stdout protocol (__KOTAEMON_EXEC_DONE__) signals completion without polling.
A daemon reader thread drains output asynchronously. On timeout, the process is
killed and automatically restarted for the next call.
Sandbox Comparison
| Feature | LocalSandbox |
LocalPythonInterpreter |
|---|---|---|
| Primary use | File I/O for any agent tool | Code execution for CodeAct |
| State across calls | ❌ Stateless (fresh process each call) | ✅ Persistent (same process) |
| Injection safety | ✅ Base64-encoded inputs | ✅ Code wrapped in try/except |
| Shell access | ✅ Full shell via execute() |
❌ Python only |
| Timeout handling | ✅ subprocess.run(timeout=N) raises TimeoutError |
✅ Kill + auto-restart |
| Error isolation | Process exits and returns exit code | Exception captured, session survives |
| File operations | ls, read, write, edit, grep, glob |
None (use LocalSandbox alongside) |
| Memory footprint | Minimal (spawned per call) | One persistent Python process |
| Concurrency | ✅ Naturally parallel (independent processes) | ⚠️ Single-threaded (one REPL per agent) |
| Backend-swappable | ✅ Yes (Docker, remote via subclass) | ❌ Local only |
Security Model
!!! warning "MVP Security Notice" Both sandbox implementations run with the same filesystem and network permissions as the parent Kotaemon process. This is intentional for the MVP to avoid Docker infrastructure overhead, but means untrusted user-provided code has host-level access.
The following mitigations are in place:
- Shell injection prevention: All user-controlled strings (file paths,
content) passed to
LocalSandbox.execute()commands are Base64-encoded before interpolation. A path like"; rm -rf /"becomes harmless base64 inside the python script. - Runaway loop protection:
LocalPythonInterpreterenforces a per-calltimeout(default 30s). On expiry the subprocess isSIGKILL-ed and restarted. - Exception isolation: User code in
LocalPythonInterpreteris wrapped intry/exceptso runtime errors (includingSyntaxError) are captured and returned without crashing the persistent session. - Runtime warning:
LocalSandboxemits aUserWarningon instantiation to notify developers of the permission boundary.
Pros & Cons
LocalSandbox
| ✅ Pros | ⚠️ Cons |
|---|---|
| Injection-safe by design (Base64 protocol) | shell=True required for heredoc patterns |
| Backend-agnostic (swap local → Docker by subclassing) | No persistent state between calls |
| Full filesystem API for any agent to use | Host-level permissions (MVP limitation) |
| Simple to reason about (each call is isolated) |
LocalPythonInterpreter
| ✅ Pros | ⚠️ Cons |
|---|---|
| True multi-turn state (critical for CodeAct) | Local-only; not swappable to remote |
| Errors don't terminate the session | One REPL per agent instance (not concurrent) |
| Automatic kill + restart on timeout | python -i interactive mode may differ slightly from script mode |
| Output capped to prevent memory exhaustion | Sentinel protocol adds a small per-call overhead |
Extending the Sandbox
To add a new backend (e.g. Docker, remote code execution service), subclass
BaseSandbox and implement three methods:
from kotaemon.agents.sandbox.base import BaseSandbox, ExecuteResponse, FileUploadResponse, FileDownloadResponse
class DockerSandbox(BaseSandbox):
@property
def id(self) -> str:
return self._container_id
def execute(self, command: str, *, timeout: int | None = None) -> ExecuteResponse:
# run command inside Docker container
...
def upload_files(self, files: list[tuple[str, bytes]]) -> list[FileUploadResponse]:
# docker cp or tar stream
...
def download_files(self, paths: list[str]) -> list[FileDownloadResponse]:
# docker cp
...
All higher-level operations (ls, read, write, edit, grep, glob) are
inherited automatically from BaseSandbox with no changes required.
Related
kotaemon.agents.sandbox— sourcetests/test_sandbox.py— test suite- CodeAct Agent — the primary consumer of
LocalPythonInterpreter