Files
kotaemon/docs/development/sandbox.md
GSD Agent d908e9c50e wip
2026-04-02 16:48:15 +00:00

161 lines
6.3 KiB
Markdown

# 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.
```python
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:
```python
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**: `LocalPythonInterpreter` enforces a per-call
`timeout` (default 30s). On expiry the subprocess is `SIGKILL`-ed and
restarted.
- **Exception isolation**: User code in `LocalPythonInterpreter` is wrapped in
`try/except` so runtime errors (including `SyntaxError`) are captured and
returned without crashing the persistent session.
- **Runtime warning**: `LocalSandbox` emits a `UserWarning` on 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:
```python
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`](../../kotaemon/libs/kotaemon/kotaemon/agents/sandbox/) — source
- [`tests/test_sandbox.py`](../../kotaemon/libs/kotaemon/tests/test_sandbox.py) — test suite
- [CodeAct Agent](./codeact-agent.md) — the primary consumer of `LocalPythonInterpreter`