mirror of
https://github.com/Cinnamon/kotaemon.git
synced 2026-09-02 04:01:38 +02:00
161 lines
6.3 KiB
Markdown
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`
|