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

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: 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:

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.