From b426827fc2421f769c5bc24db30a6cfe8cc09859 Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Thu, 5 Feb 2026 19:59:00 +0100 Subject: [PATCH] Docker-based sandbox. Reused within a single rlm() call for latency --- .github/workflows/test.yml | 19 + CHANGELOG.md | 5 +- docs/rlm.md | 50 +- .../haiku/rag/agents/rlm/__init__.py | 6 +- haiku_rag_slim/haiku/rag/agents/rlm/agent.py | 21 +- .../haiku/rag/agents/rlm/dependencies.py | 2 + .../haiku/rag/agents/rlm/docker_sandbox.py | 207 ++++ haiku_rag_slim/haiku/rag/agents/rlm/runner.py | 214 +++++ .../haiku/rag/agents/rlm/sandbox.py | 414 -------- haiku_rag_slim/haiku/rag/client.py | 26 +- haiku_rag_slim/haiku/rag/config/models.py | 2 + haiku_rag_slim/pyproject.toml | 1 + tests/agents/rlm/conftest.py | 43 +- tests/agents/rlm/test_agent.py | 77 +- tests/agents/rlm/test_sandbox.py | 909 ++++-------------- ...test_filter_applied_to_list_documents.yaml | 82 ++ ...ckerSandboxHaikuRAG.test_get_document.yaml | 42 + ...aikuRAG.test_list_documents_with_data.yaml | 42 + ...SandboxHaikuRAG.test_search_with_data.yaml | 42 + ...sql_injection_in_get_document_blocked.yaml | 82 -- uv.lock | 16 + 21 files changed, 974 insertions(+), 1328 deletions(-) create mode 100644 haiku_rag_slim/haiku/rag/agents/rlm/docker_sandbox.py create mode 100644 haiku_rag_slim/haiku/rag/agents/rlm/runner.py delete mode 100644 haiku_rag_slim/haiku/rag/agents/rlm/sandbox.py create mode 100644 tests/cassettes/test_sandbox/TestDockerSandboxContextFilter.test_filter_applied_to_list_documents.yaml create mode 100644 tests/cassettes/test_sandbox/TestDockerSandboxHaikuRAG.test_get_document.yaml create mode 100644 tests/cassettes/test_sandbox/TestDockerSandboxHaikuRAG.test_list_documents_with_data.yaml create mode 100644 tests/cassettes/test_sandbox/TestDockerSandboxHaikuRAG.test_search_with_data.yaml delete mode 100644 tests/cassettes/test_sandbox/TestSandboxEscapeVectors.test_sql_injection_in_get_document_blocked.yaml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index aa71c1bf..b6bc062a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -74,3 +74,22 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} files: ./coverage.xml fail_ci_if_error: false + + test-docker-sandbox: + needs: [lint] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v4 + with: + enable-cache: true + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version-file: "pyproject.toml" + - name: Install dependencies + run: uv sync --all-extras + - name: Build Docker image + run: docker build -t haiku-rag-slim:test -f docker/Dockerfile.slim . + - name: Run Docker integration tests + run: uv run pytest tests/agents/rlm/test_sandbox.py -v diff --git a/CHANGELOG.md b/CHANGELOG.md index c8444f2d..d0b4d3bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,13 +8,16 @@ - Allows disabling OCR via config when running docling-serve in read-only containers - **RLM Agent (Recursive Language Model)**: New agent for complex analytical tasks via sandboxed Python code execution - Solves problems traditional RAG can't handle: aggregation, computation, multi-document analysis - - Sandboxed execution with safe builtins and allowed imports (json, re, math, statistics, etc.) + - Docker-based sandbox with full Python environment (no import restrictions) + - Container reuse within a single `rlm()` call for reduced latency - Available functions: `search()`, `list_documents()`, `get_document()`, `get_docling_document()`, `llm()` - Pre-loaded documents support via `documents` variable - Context filter for scoping searches without LLM control - New `client.rlm(question)` method on HaikuRAG client - New `haiku-rag rlm` CLI command - New `rlm_question` MCP tool + - New config options: `docker_image`, `docker_memory_limit` +- **CI**: Docker sandbox integration tests run in GitHub Actions ### Fixed diff --git a/docs/rlm.md b/docs/rlm.md index 5cdd1e8d..81a0a722 100644 --- a/docs/rlm.md +++ b/docs/rlm.md @@ -132,19 +132,9 @@ for doc in documents: Each document dict has keys: `id`, `title`, `uri`, `content` -## Allowed Imports +## Imports -The following standard library modules can be imported: - -- `json` - JSON encoding/decoding -- `re` - Regular expressions -- `math` - Mathematical functions -- `statistics` - Statistical functions -- `collections` - Specialized containers -- `itertools` - Iterator utilities -- `functools` - Higher-order functions -- `datetime` - Date and time handling -- `typing` - Type hints +The sandbox runs in a Docker container with full Python available. Any module installed in the container image can be imported: ```python import re @@ -161,15 +151,17 @@ for r in results: print(Counter(error_types).most_common(10)) ``` -## Security +The default image (`ghcr.io/ggozad/haiku.rag-slim`) includes the Python standard library. Custom images can add additional packages like `pandas` or `numpy`. -The sandbox enforces several security measures: +## Docker Sandbox -- **Blocked builtins**: `eval`, `exec`, `compile`, `open`, `input`, `__import__`, `globals`, `locals`, `getattr`, `setattr`, `delattr` -- **Blocked imports**: `os`, `sys`, `subprocess`, `shutil`, `socket`, `requests`, `builtins` -- **Private attribute access blocked**: Cannot access `__dunder__` attributes (except common ones like `__init__`, `__str__`) -- **Execution timeout**: Code execution times out after configurable limit (default 60s) +Code executes in an isolated Docker container with: + +- **Read-only database**: The LanceDB database is mounted read-only +- **Memory limits**: Configurable memory limit (default 512MB) +- **Execution timeout**: Code times out after configurable limit (default 60s) - **Output truncation**: Large outputs are truncated to prevent memory issues +- **Container reuse**: Within a single `rlm()` call, the container stays warm for multiple code executions ## Context Filter @@ -201,4 +193,26 @@ rlm: code_timeout: 60.0 # Max seconds for code execution max_tool_calls: 20 # Max execute_code calls per question max_output_chars: 50000 # Truncate output after this many chars + docker_image: "ghcr.io/ggozad/haiku.rag-slim:latest" # Container image + docker_memory_limit: "512m" # Container memory limit +``` + +### Custom Docker Image + +To add additional Python packages, create a custom Dockerfile: + +```dockerfile +FROM ghcr.io/ggozad/haiku.rag-slim:latest +RUN pip install pandas numpy +``` + +Build and configure: + +```bash +docker build -t my-rlm-image . +``` + +```yaml +rlm: + docker_image: "my-rlm-image" ``` diff --git a/haiku_rag_slim/haiku/rag/agents/rlm/__init__.py b/haiku_rag_slim/haiku/rag/agents/rlm/__init__.py index 77ba8c73..d5380af3 100644 --- a/haiku_rag_slim/haiku/rag/agents/rlm/__init__.py +++ b/haiku_rag_slim/haiku/rag/agents/rlm/__init__.py @@ -1,16 +1,16 @@ from haiku.rag.agents.rlm.agent import create_rlm_agent from haiku.rag.agents.rlm.dependencies import RLMContext, RLMDeps +from haiku.rag.agents.rlm.docker_sandbox import DockerSandbox, SandboxResult from haiku.rag.agents.rlm.models import CodeExecution, RLMResult from haiku.rag.agents.rlm.prompts import RLM_SYSTEM_PROMPT -from haiku.rag.agents.rlm.sandbox import REPLEnvironment, REPLResult __all__ = [ "CodeExecution", + "DockerSandbox", "RLMContext", "RLMDeps", "RLMResult", "RLM_SYSTEM_PROMPT", - "REPLEnvironment", - "REPLResult", + "SandboxResult", "create_rlm_agent", ] diff --git a/haiku_rag_slim/haiku/rag/agents/rlm/agent.py b/haiku_rag_slim/haiku/rag/agents/rlm/agent.py index 9fc55309..1758e1da 100644 --- a/haiku_rag_slim/haiku/rag/agents/rlm/agent.py +++ b/haiku_rag_slim/haiku/rag/agents/rlm/agent.py @@ -3,24 +3,9 @@ from pydantic_ai import Agent, RunContext from haiku.rag.agents.rlm.dependencies import RLMDeps from haiku.rag.agents.rlm.models import CodeExecution, RLMResult from haiku.rag.agents.rlm.prompts import RLM_SYSTEM_PROMPT -from haiku.rag.agents.rlm.sandbox import REPLEnvironment from haiku.rag.config.models import AppConfig from haiku.rag.utils import get_model -_repl_cache: dict[int, REPLEnvironment] = {} - - -def _get_or_create_repl(ctx) -> REPLEnvironment: - """Get or create a REPL environment for this context.""" - key = id(ctx.deps) - if key not in _repl_cache: - _repl_cache[key] = REPLEnvironment( - client=ctx.deps.client, - config=ctx.deps.config.rlm, - context=ctx.deps.context, - ) - return _repl_cache[key] - def create_rlm_agent(config: AppConfig) -> Agent[RLMDeps, RLMResult]: """Create an RLM agent with code execution capability. @@ -54,7 +39,7 @@ def create_rlm_agent(config: AppConfig) -> Agent[RLMDeps, RLMResult]: modules (json, re, collections, math, statistics, itertools, functools, datetime, typing). - Use print() to output results. Variables persist between executions. + Use print() to output results. Args: code: Python code to execute. @@ -62,9 +47,7 @@ def create_rlm_agent(config: AppConfig) -> Agent[RLMDeps, RLMResult]: Returns: Structured result with success status, stdout, and stderr. """ - repl = _get_or_create_repl(ctx) - - result = await repl.execute_async(code) + result = await ctx.deps.sandbox.execute(code) execution = CodeExecution( code=code, diff --git a/haiku_rag_slim/haiku/rag/agents/rlm/dependencies.py b/haiku_rag_slim/haiku/rag/agents/rlm/dependencies.py index 8f022f32..aca51ebf 100644 --- a/haiku_rag_slim/haiku/rag/agents/rlm/dependencies.py +++ b/haiku_rag_slim/haiku/rag/agents/rlm/dependencies.py @@ -4,6 +4,7 @@ from typing import TYPE_CHECKING from haiku.rag.store.models import Document, SearchResult if TYPE_CHECKING: + from haiku.rag.agents.rlm.docker_sandbox import DockerSandbox from haiku.rag.agents.rlm.models import CodeExecution from haiku.rag.client import HaikuRAG from haiku.rag.config.models import AppConfig @@ -25,4 +26,5 @@ class RLMDeps: client: "HaikuRAG" config: "AppConfig" + sandbox: "DockerSandbox" context: RLMContext = field(default_factory=RLMContext) diff --git a/haiku_rag_slim/haiku/rag/agents/rlm/docker_sandbox.py b/haiku_rag_slim/haiku/rag/agents/rlm/docker_sandbox.py new file mode 100644 index 00000000..0e5efa5e --- /dev/null +++ b/haiku_rag_slim/haiku/rag/agents/rlm/docker_sandbox.py @@ -0,0 +1,207 @@ +"""Docker-based sandboxed execution.""" + +import asyncio +import json +import os +import subprocess +import sys +from dataclasses import dataclass +from typing import TYPE_CHECKING + +from haiku.rag.agents.rlm.dependencies import RLMContext +from haiku.rag.config.models import RLMConfig + +if TYPE_CHECKING: + from haiku.rag.client import HaikuRAG + + +@dataclass +class SandboxResult: + """Result of executing code in the sandbox.""" + + stdout: str + stderr: str + success: bool + + +class DockerSandbox: + """Execute code in a persistent Docker container. + + Use as an async context manager to manage container lifecycle: + + async with DockerSandbox(client, config, context) as sandbox: + result = await sandbox.execute("print('hello')") + result = await sandbox.execute("print('world')") + """ + + DEFAULT_IMAGE = "ghcr.io/ggozad/haiku.rag-slim:latest" + + def __init__( + self, + client: "HaikuRAG", + config: RLMConfig, + context: RLMContext, + image: str | None = None, + ): + self.haiku_client = client + self.config = config + self.context = context + self.image = image or self.DEFAULT_IMAGE + self._process: subprocess.Popen[bytes] | None = None + + def _build_docker_cmd(self) -> list[str]: + """Build the docker run command.""" + db_path = str(self.haiku_client.store.db_path) + + env_list = ["-e", "HAIKU_DB_PATH=/data/db.lancedb"] + if self.context.filter: + env_list.extend(["-e", f"HAIKU_FILTER={self.context.filter}"]) + + ollama_host = os.environ.get("OLLAMA_HOST", "") + ollama_base_url = os.environ.get("OLLAMA_BASE_URL", "") + + if sys.platform == "darwin": + if not ollama_host or "localhost" in ollama_host: + ollama_host = "http://host.docker.internal:11434" + if not ollama_base_url or "localhost" in ollama_base_url: + ollama_base_url = "http://host.docker.internal:11434" + + if ollama_host: + env_list.extend(["-e", f"OLLAMA_HOST={ollama_host}"]) + if ollama_base_url: + env_list.extend(["-e", f"OLLAMA_BASE_URL={ollama_base_url}"]) + + for key in [ + "ANTHROPIC_API_KEY", + "OPENAI_API_KEY", + "VOYAGE_API_KEY", + "COHERE_API_KEY", + ]: + if value := os.environ.get(key): + env_list.extend(["-e", f"{key}={value}"]) + + return [ + "docker", + "run", + "--rm", + "-i", + "-v", + f"{db_path}:/data/db.lancedb:ro", + f"--memory={self.config.docker_memory_limit}", + "--network=host", + *env_list, + self.image, + "python", + "-m", + "haiku.rag.agents.rlm.runner", + ] + + async def __aenter__(self) -> "DockerSandbox": + """Start the container.""" + loop = asyncio.get_running_loop() + await loop.run_in_executor(None, self._start_container) + return self + + async def __aexit__( + self, exc_type: object, exc_val: object, exc_tb: object + ) -> None: + """Stop the container.""" + loop = asyncio.get_running_loop() + await loop.run_in_executor(None, self._stop_container) + + def _start_container(self) -> None: + """Start the persistent container process.""" + if self._process is not None: + return + + cmd = self._build_docker_cmd() + self._process = subprocess.Popen( + cmd, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + def _stop_container(self) -> None: + """Stop the container process.""" + if self._process is None: + return + + try: + if self._process.stdin: + self._process.stdin.close() + self._process.terminate() + self._process.wait(timeout=5) + except subprocess.TimeoutExpired: + self._process.kill() + self._process.wait() + finally: + self._process = None + + async def execute(self, code: str) -> SandboxResult: + """Execute code in the container.""" + if self._process is None: + return SandboxResult( + stdout="", + stderr="Container not started. Use 'async with' context manager.", + success=False, + ) + + loop = asyncio.get_running_loop() + return await loop.run_in_executor(None, self._execute_sync, code) + + def _execute_sync(self, code: str) -> SandboxResult: + """Send code to container and read result.""" + assert self._process is not None and self._process.stdin is not None + + try: + message = json.dumps({"code": code}) + length_line = f"{len(message)}\n".encode() + self._process.stdin.write(length_line) + self._process.stdin.write(message.encode()) + self._process.stdin.flush() + + if self._process.stdout is None: + return SandboxResult( + stdout="", stderr="No stdout from container.", success=False + ) + + length_line = self._process.stdout.readline() + if not length_line: + stderr = "" + if self._process.stderr: + stderr = self._process.stderr.read().decode() + return SandboxResult( + stdout="", + stderr=stderr or "Container closed unexpectedly.", + success=False, + ) + + length = int(length_line.strip()) + response = self._process.stdout.read(length).decode() + result_data = json.loads(response) + + return SandboxResult( + stdout=result_data.get("stdout", ""), + stderr=result_data.get("stderr", ""), + success=result_data.get("success", False), + ) + + except subprocess.TimeoutExpired: + return SandboxResult( + stdout="", + stderr=f"Execution timed out after {self.config.code_timeout} seconds", + success=False, + ) + except json.JSONDecodeError as e: + return SandboxResult( + stdout="", + stderr=f"Invalid response from container: {e}", + success=False, + ) + except Exception as e: + return SandboxResult( + stdout="", + stderr=f"Execution error: {e}", + success=False, + ) diff --git a/haiku_rag_slim/haiku/rag/agents/rlm/runner.py b/haiku_rag_slim/haiku/rag/agents/rlm/runner.py new file mode 100644 index 00000000..2fa6f3e6 --- /dev/null +++ b/haiku_rag_slim/haiku/rag/agents/rlm/runner.py @@ -0,0 +1,214 @@ +"""Entry point for sandboxed code execution in Docker container.""" + +import asyncio +import json +import sys +import traceback +from io import StringIO +from typing import Any + + +def build_namespace( + client: Any, config: Any, context: Any, loop: asyncio.AbstractEventLoop +) -> dict[str, Any]: + """Build execution namespace with haiku.rag functions injected.""" + from haiku.rag.store.repositories.document import _escape_sql_string + + def run_async(coro: Any) -> Any: + """Run async coroutine from sync context using thread-safe scheduling.""" + future = asyncio.run_coroutine_threadsafe(coro, loop) + return future.result(timeout=config.rlm.code_timeout) + + def search(query: str, limit: int = 10) -> list[dict]: + async def _search() -> Any: + return await client.search(query, limit=limit, filter=context.filter) + + results = run_async(_search()) + context.search_results.extend(results) + return [ + { + "chunk_id": r.chunk_id, + "content": r.content, + "document_id": r.document_id, + "document_title": r.document_title, + "document_uri": r.document_uri, + "score": r.score, + "page_numbers": r.page_numbers, + "headings": r.headings, + } + for r in results + ] + + def list_documents(limit: int = 10, offset: int = 0) -> list[dict]: + async def _list() -> Any: + return await client.list_documents( + limit=limit, offset=offset, filter=context.filter + ) + + docs = run_async(_list()) + return [ + { + "id": d.id, + "title": d.title, + "uri": d.uri, + "created_at": str(d.created_at), + } + for d in docs + ] + + def get_document(id_or_title: str) -> str | None: + async def _get() -> str | None: + doc = await client.get_document_by_id(id_or_title) + if doc: + return doc.content + safe_input = _escape_sql_string(id_or_title) + docs = await client.list_documents(filter=f"title = '{safe_input}'") + if docs and docs[0].id: + full_doc = await client.get_document_by_id(docs[0].id) + return full_doc.content if full_doc else None + docs = await client.list_documents(filter=f"uri = '{safe_input}'") + if docs and docs[0].id: + full_doc = await client.get_document_by_id(docs[0].id) + return full_doc.content if full_doc else None + return None + + return run_async(_get()) + + def get_docling_document(id_or_title: str) -> Any: + async def _get() -> Any: + doc = await client.get_document_by_id(id_or_title) + if doc: + return doc.get_docling_document() + safe_input = _escape_sql_string(id_or_title) + docs = await client.list_documents(filter=f"title = '{safe_input}'") + if docs and docs[0].id: + full_doc = await client.get_document_by_id(docs[0].id) + return full_doc.get_docling_document() if full_doc else None + docs = await client.list_documents(filter=f"uri = '{safe_input}'") + if docs and docs[0].id: + full_doc = await client.get_document_by_id(docs[0].id) + return full_doc.get_docling_document() if full_doc else None + return None + + return run_async(_get()) + + def llm(prompt: str) -> str: + async def _llm() -> str: + from pydantic_ai import Agent + + from haiku.rag.utils import get_model + + model = get_model(config.rlm.model, config) + agent: Agent[None, str] = Agent(model, output_type=str) + result = await agent.run(prompt) + return result.output + + return run_async(_llm()) + + namespace: dict[str, Any] = { + "search": search, + "list_documents": list_documents, + "get_document": get_document, + "get_docling_document": get_docling_document, + "llm": llm, + } + + if context.documents: + namespace["documents"] = [ + {"id": d.id, "title": d.title, "uri": d.uri, "content": d.content} + for d in context.documents + ] + + return namespace + + +def execute_code( + code: str, namespace: dict[str, Any], max_output_chars: int +) -> dict[str, Any]: + """Execute code and capture output.""" + stdout_capture = StringIO() + original_stdout = sys.stdout + + try: + sys.stdout = stdout_capture + exec(code, namespace) + stdout = stdout_capture.getvalue() + if len(stdout) > max_output_chars: + stdout = stdout[:max_output_chars] + "\n... (output truncated)" + return { + "success": True, + "stdout": stdout, + "stderr": "", + } + except Exception: + return { + "success": False, + "stdout": stdout_capture.getvalue(), + "stderr": traceback.format_exc(), + } + finally: + sys.stdout = original_stdout + + +def send_response(result: dict[str, Any]) -> None: + """Send length-prefixed JSON response.""" + response = json.dumps(result) + sys.stdout.write(f"{len(response)}\n") + sys.stdout.write(response) + sys.stdout.flush() + + +async def main() -> None: + """Main entry point for container execution. + + Runs a loop reading length-prefixed JSON messages and executing code. + """ + import concurrent.futures + import os + from pathlib import Path + + from haiku.rag.agents.rlm.dependencies import RLMContext + from haiku.rag.client import HaikuRAG + from haiku.rag.config import get_config + + config = get_config() + db_path = Path(os.environ.get("HAIKU_DB_PATH", "/data/db.lancedb")) + filter_expr = os.environ.get("HAIKU_FILTER") + context = RLMContext(filter=filter_expr) + max_output_chars = config.rlm.max_output_chars + + loop = asyncio.get_running_loop() + + async with HaikuRAG(db_path, config=config, read_only=True) as client: + namespace = build_namespace(client, config, context, loop) + + with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: + while True: + # Read length-prefixed message + length_line = sys.stdin.readline() + if not length_line: + break + + try: + length = int(length_line.strip()) + message = sys.stdin.read(length) + request = json.loads(message) + code = request.get("code", "") + + result = await loop.run_in_executor( + executor, execute_code, code, namespace, max_output_chars + ) + send_response(result) + + except (ValueError, json.JSONDecodeError) as e: + send_response( + { + "success": False, + "stdout": "", + "stderr": f"Invalid request: {e}", + } + ) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/haiku_rag_slim/haiku/rag/agents/rlm/sandbox.py b/haiku_rag_slim/haiku/rag/agents/rlm/sandbox.py deleted file mode 100644 index 8691988e..00000000 --- a/haiku_rag_slim/haiku/rag/agents/rlm/sandbox.py +++ /dev/null @@ -1,414 +0,0 @@ -import ast -import asyncio -import concurrent.futures -import sys -import traceback -from io import StringIO -from typing import TYPE_CHECKING, Any - -from haiku.rag.agents.rlm.dependencies import RLMContext -from haiku.rag.config.models import RLMConfig -from haiku.rag.store.repositories.document import _escape_sql_string - -if TYPE_CHECKING: - from haiku.rag.client import HaikuRAG - - -class REPLResult: - """Result of executing code in the REPL environment.""" - - def __init__( - self, stdout: str, stderr: str, success: bool, locals_: dict | None = None - ): - self.stdout = stdout - self.stderr = stderr - self.success = success - self.locals = locals_ or {} - - def __repr__(self) -> str: - return f"REPLResult(success={self.success}, stdout={self.stdout!r}, stderr={self.stderr!r})" - - -class REPLEnvironment: - """Sandboxed Python execution environment with haiku.rag access.""" - - SAFE_BUILTINS: dict[str, Any] = { - "True": True, - "False": False, - "None": None, - "__build_class__": __builtins__["__build_class__"] - if isinstance(__builtins__, dict) - else getattr(__builtins__, "__build_class__"), - "abs": abs, - "all": all, - "any": any, - "ascii": ascii, - "bin": bin, - "bool": bool, - "bytearray": bytearray, - "bytes": bytes, - "callable": callable, - "chr": chr, - "complex": complex, - "dict": dict, - "divmod": divmod, - "enumerate": enumerate, - "filter": filter, - "float": float, - "format": format, - "frozenset": frozenset, - "hash": hash, - "hex": hex, - "id": id, - "int": int, - "isinstance": isinstance, - "issubclass": issubclass, - "iter": iter, - "len": len, - "list": list, - "map": map, - "max": max, - "min": min, - "next": next, - "object": object, - "oct": oct, - "ord": ord, - "pow": pow, - "print": print, - "range": range, - "repr": repr, - "reversed": reversed, - "round": round, - "set": set, - "slice": slice, - "sorted": sorted, - "str": str, - "sum": sum, - "tuple": tuple, - "type": ( - lambda obj: type(obj) - ), # Single-arg only, blocks type(name, bases, dict) - "zip": zip, - "Exception": Exception, - "ValueError": ValueError, - "TypeError": TypeError, - "KeyError": KeyError, - "IndexError": IndexError, - "AttributeError": AttributeError, - "RuntimeError": RuntimeError, - "StopIteration": StopIteration, - "ZeroDivisionError": ZeroDivisionError, - "AssertionError": AssertionError, - } - - ALLOWED_IMPORTS = { - "json", - "re", - "collections", - "math", - "statistics", - "itertools", - "functools", - "datetime", - "typing", - } - - def __init__( - self, - client: "HaikuRAG", - config: RLMConfig, - context: RLMContext, - event_loop: asyncio.AbstractEventLoop | None = None, - ): - self.client = client - self.config = config - self.context = context - self._event_loop = event_loop - self._setup_namespace() - - def _run_async_from_thread(self, coro): - """Run async coroutine from a worker thread using run_coroutine_threadsafe.""" - if self._event_loop is None: - raise RuntimeError("Event loop not set. Cannot call async functions.") - future = asyncio.run_coroutine_threadsafe(coro, self._event_loop) - return future.result(timeout=self.config.code_timeout) - - def _setup_namespace(self) -> None: - """Build execution namespace with haiku.rag functions.""" - self.globals: dict[str, Any] = { - "__builtins__": dict(self.SAFE_BUILTINS), - "__name__": "__sandbox__", - "search": self._make_search(), - "list_documents": self._make_list_documents(), - "get_document": self._make_get_document(), - "get_docling_document": self._make_get_docling_document(), - "llm": self._make_llm(), - } - self.locals: dict[str, Any] = {} - - if self.context.documents: - self.globals["documents"] = [ - {"id": d.id, "title": d.title, "uri": d.uri, "content": d.content} - for d in self.context.documents - ] - - def _make_search(self): - """Create sync search function that bridges to async client.""" - - def search(query: str, limit: int = 10) -> list[dict]: - async def _search(): - return await self.client.search( - query, limit=limit, filter=self.context.filter - ) - - results = self._run_async_from_thread(_search()) - self.context.search_results.extend(results) - return [ - { - "chunk_id": r.chunk_id, - "content": r.content, - "document_id": r.document_id, - "document_title": r.document_title, - "document_uri": r.document_uri, - "score": r.score, - "page_numbers": r.page_numbers, - "headings": r.headings, - } - for r in results - ] - - return search - - def _make_list_documents(self): - """Create sync list_documents function.""" - - def list_documents(limit: int = 10, offset: int = 0) -> list[dict]: - async def _list(): - return await self.client.list_documents( - limit=limit, offset=offset, filter=self.context.filter - ) - - docs = self._run_async_from_thread(_list()) - return [ - { - "id": d.id, - "title": d.title, - "uri": d.uri, - "created_at": str(d.created_at), - } - for d in docs - ] - - return list_documents - - def _make_get_document(self): - """Create sync get_document function that returns text content.""" - - def get_document(id_or_title: str) -> str | None: - async def _get(): - doc = await self.client.get_document_by_id(id_or_title) - if doc: - return doc.content - safe_input = _escape_sql_string(id_or_title) - docs = await self.client.list_documents( - filter=f"title = '{safe_input}'" - ) - if docs and docs[0].id: - full_doc = await self.client.get_document_by_id(docs[0].id) - return full_doc.content if full_doc else None - docs = await self.client.list_documents(filter=f"uri = '{safe_input}'") - if docs and docs[0].id: - full_doc = await self.client.get_document_by_id(docs[0].id) - return full_doc.content if full_doc else None - return None - - return self._run_async_from_thread(_get()) - - return get_document - - def _make_get_docling_document(self): - """Create sync get_docling_document function that returns DoclingDocument.""" - - def get_docling_document(id_or_title: str): - async def _get(): - doc = await self.client.get_document_by_id(id_or_title) - if doc: - return doc.get_docling_document() - safe_input = _escape_sql_string(id_or_title) - docs = await self.client.list_documents( - filter=f"title = '{safe_input}'" - ) - if docs and docs[0].id: - full_doc = await self.client.get_document_by_id(docs[0].id) - return full_doc.get_docling_document() if full_doc else None - docs = await self.client.list_documents(filter=f"uri = '{safe_input}'") - if docs and docs[0].id: - full_doc = await self.client.get_document_by_id(docs[0].id) - return full_doc.get_docling_document() if full_doc else None - return None - - return self._run_async_from_thread(_get()) - - return get_docling_document - - def _make_llm(self): - """Create sync llm function for plain LLM calls without RAG.""" - - def llm(prompt: str) -> str: - async def _llm(): - from pydantic_ai import Agent - - from haiku.rag.utils import get_model - - model = get_model(self.config.model) - agent: Agent[None, str] = Agent(model, output_type=str) - result = await agent.run(prompt) - return result.output - - return self._run_async_from_thread(_llm()) - - return llm - - def _safe_import( - self, - name: str, - globals: dict | None = None, - locals: dict | None = None, - fromlist: tuple = (), - level: int = 0, - ): - """Import hook that only allows safe modules.""" - base_module = name.split(".")[0] - if base_module not in self.ALLOWED_IMPORTS: - raise ImportError(f"Import of '{name}' is not allowed in sandbox") - - import importlib - - module = importlib.import_module(name) - if fromlist: - for attr in fromlist: - if not hasattr(module, attr): - raise ImportError(f"cannot import name '{attr}' from '{name}'") - return module - return module - - def _validate_code(self, code: str) -> None: - """Validate code AST for security issues.""" - tree = ast.parse(code) - - for node in ast.walk(tree): - if isinstance(node, ast.Attribute): - if node.attr.startswith("_") and node.attr not in ( - "__init__", - "__str__", - "__repr__", - "__class__", - "__name__", - "__doc__", - "__dict__", - ): - raise SecurityError( - f"Access to private/dunder attribute '{node.attr}' is not allowed" - ) - # Block dictionary key access to dunder/private strings - # This prevents type.__dict__['__subclasses__'] attacks - if isinstance(node, ast.Subscript): - if isinstance(node.slice, ast.Constant): - if isinstance( - node.slice.value, str - ) and node.slice.value.startswith("_"): - raise SecurityError( - f"Dictionary access to '{node.slice.value}' is not allowed" - ) - - def _execute_sync(self, code: str) -> REPLResult: - """Internal synchronous execution - must be called from executor thread.""" - stdout_capture = StringIO() - stderr_capture = StringIO() - - original_stdout = sys.stdout - original_stderr = sys.stderr - - try: - self._validate_code(code) - except SyntaxError as e: - return REPLResult( - stdout="", - stderr=f"SyntaxError: {e}", - success=False, - ) - except SecurityError as e: - return REPLResult( - stdout="", - stderr=str(e), - success=False, - ) - - exec_globals = dict(self.globals) - exec_globals["__builtins__"] = dict(self.SAFE_BUILTINS) - exec_globals["__builtins__"]["__import__"] = self._safe_import - - try: - sys.stdout = stdout_capture - sys.stderr = stderr_capture - - exec(code, exec_globals, self.locals) - - for key, value in self.locals.items(): - if not key.startswith("_"): - self.globals[key] = value - - stdout = stdout_capture.getvalue() - if len(stdout) > self.config.max_output_chars: - stdout = ( - stdout[: self.config.max_output_chars] + "\n... (output truncated)" - ) - - return REPLResult( - stdout=stdout, - stderr=stderr_capture.getvalue(), - success=True, - locals_=dict(self.locals), - ) - - except Exception: - tb = traceback.format_exc() - return REPLResult( - stdout=stdout_capture.getvalue(), - stderr=tb, - success=False, - ) - - finally: - sys.stdout = original_stdout - sys.stderr = original_stderr - - def execute(self, code: str) -> REPLResult: - """Execute code in sandbox synchronously. - - This method runs code directly in the current thread. - For async contexts, use execute_async() instead. - """ - return self._execute_sync(code) - - async def execute_async(self, code: str) -> REPLResult: - """Execute code in sandbox from async context. - - Runs the synchronous code in a thread executor, allowing - sandbox functions to call back to async client methods. - """ - loop = asyncio.get_running_loop() - self._event_loop = loop - - with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: - result = await asyncio.wait_for( - loop.run_in_executor(executor, self._execute_sync, code), - timeout=self.config.code_timeout, - ) - return result - - -class SecurityError(Exception): - """Raised when sandbox security is violated.""" - - pass diff --git a/haiku_rag_slim/haiku/rag/client.py b/haiku_rag_slim/haiku/rag/client.py index 8fb3e7d5..37f31bfb 100644 --- a/haiku_rag_slim/haiku/rag/client.py +++ b/haiku_rag_slim/haiku/rag/client.py @@ -1316,7 +1316,12 @@ class HaikuRAG: Returns: The answer as a string. """ - from haiku.rag.agents.rlm import RLMContext, RLMDeps, create_rlm_agent + from haiku.rag.agents.rlm import ( + DockerSandbox, + RLMContext, + RLMDeps, + create_rlm_agent, + ) context = RLMContext(filter=filter) @@ -1333,16 +1338,23 @@ class HaikuRAG: loaded_docs.append(doc) context.documents = loaded_docs if loaded_docs else None - deps = RLMDeps( + async with DockerSandbox( client=self, - config=self._config, + config=self._config.rlm, context=context, - ) + image=self._config.rlm.docker_image, + ) as sandbox: + deps = RLMDeps( + client=self, + config=self._config, + sandbox=sandbox, + context=context, + ) - agent = create_rlm_agent(self._config) - result = await agent.run(question, deps=deps) + agent = create_rlm_agent(self._config) + result = await agent.run(question, deps=deps) - return result.output.answer + return result.output.answer async def visualize_chunk(self, chunk: Chunk) -> list: """Render page images with bounding box highlights for a chunk. diff --git a/haiku_rag_slim/haiku/rag/config/models.py b/haiku_rag_slim/haiku/rag/config/models.py index 7d2c1828..544680c2 100644 --- a/haiku_rag_slim/haiku/rag/config/models.py +++ b/haiku_rag_slim/haiku/rag/config/models.py @@ -105,6 +105,8 @@ class RLMConfig(BaseModel): code_timeout: float = 60.0 max_output_chars: int = 50_000 max_tool_calls: int = 20 + docker_image: str = "ghcr.io/ggozad/haiku.rag-slim:latest" + docker_memory_limit: str = "512m" class PictureDescriptionConfig(BaseModel): diff --git a/haiku_rag_slim/pyproject.toml b/haiku_rag_slim/pyproject.toml index fda1e988..b98f1b9c 100644 --- a/haiku_rag_slim/pyproject.toml +++ b/haiku_rag_slim/pyproject.toml @@ -22,6 +22,7 @@ classifiers = [ ] dependencies = [ + "docker>=7.1.0", "docling-core==2.60.1", "httpx>=0.28.1", "jsonpatch>=1.33", diff --git a/tests/agents/rlm/conftest.py b/tests/agents/rlm/conftest.py index 7efb0a86..880d4cd5 100644 --- a/tests/agents/rlm/conftest.py +++ b/tests/agents/rlm/conftest.py @@ -1,10 +1,40 @@ +import os +import subprocess +from pathlib import Path + import pytest from haiku.rag.agents.rlm.dependencies import RLMContext -from haiku.rag.agents.rlm.sandbox import REPLEnvironment +from haiku.rag.agents.rlm.docker_sandbox import DockerSandbox from haiku.rag.client import HaikuRAG from haiku.rag.config.models import RLMConfig +TEST_DOCKER_IMAGE = os.environ.get("HAIKU_TEST_DOCKER_IMAGE", "haiku-rag-slim:test") + + +@pytest.fixture(scope="session") +def test_docker_image(): + """Build and return the Docker image for testing.""" + if os.environ.get("CI"): + return TEST_DOCKER_IMAGE + + project_root = Path(__file__).parent.parent.parent.parent + dockerfile = project_root / "docker" / "Dockerfile.slim" + + if not dockerfile.exists(): + pytest.skip(f"Dockerfile.slim not found at {dockerfile}") + + result = subprocess.run( + ["docker", "build", "-t", TEST_DOCKER_IMAGE, "-f", str(dockerfile), "."], + cwd=project_root, + capture_output=True, + text=True, + ) + if result.returncode != 0: + pytest.fail(f"Failed to build Docker image:\n{result.stderr}") + + return TEST_DOCKER_IMAGE + @pytest.fixture async def empty_client(temp_db_path): @@ -14,8 +44,11 @@ async def empty_client(temp_db_path): @pytest.fixture -async def repl_env_empty(empty_client): - """Create a REPL environment without documents.""" - config = RLMConfig() +async def docker_sandbox(empty_client, test_docker_image): + """Create a Docker sandbox for testing.""" + config = RLMConfig(docker_image=test_docker_image) context = RLMContext() - return REPLEnvironment(client=empty_client, config=config, context=context) + async with DockerSandbox( + client=empty_client, config=config, context=context, image=test_docker_image + ) as sandbox: + yield sandbox diff --git a/tests/agents/rlm/test_agent.py b/tests/agents/rlm/test_agent.py index 9bc78b59..e4d63344 100644 --- a/tests/agents/rlm/test_agent.py +++ b/tests/agents/rlm/test_agent.py @@ -4,7 +4,7 @@ import pytest from pydantic_ai import Agent from haiku.rag.agents.rlm.agent import create_rlm_agent -from haiku.rag.agents.rlm.dependencies import RLMContext, RLMDeps +from haiku.rag.agents.rlm.dependencies import RLMDeps from haiku.rag.agents.rlm.models import CodeExecution, RLMResult from haiku.rag.config import Config @@ -27,56 +27,8 @@ class TestCreateRLMAgent: assert "execute_code" in tool_names -class TestExecuteCodeTool: - @pytest.mark.asyncio - async def test_execute_code_returns_structured_result(self, empty_client): - """Test that execute_code tool produces structured CodeExecution output.""" - from haiku.rag.agents.rlm.agent import _get_or_create_repl - - context = RLMContext() - deps = RLMDeps( - client=empty_client, - config=Config, - context=context, - ) - - class MockCtx: - def __init__(self, deps): - self.deps = deps - - ctx = MockCtx(deps) - repl = _get_or_create_repl(ctx) - - result = await repl.execute_async("print(1 + 1)") - assert result.success - assert "2" in result.stdout - - @pytest.mark.asyncio - async def test_execute_code_tracks_executions_in_context(self, empty_client): - """Test that code executions are tracked as CodeExecution objects in RLMContext.""" - from haiku.rag.agents.rlm.agent import _get_or_create_repl - - context = RLMContext() - deps = RLMDeps( - client=empty_client, - config=Config, - context=context, - ) - - class MockCtx: - def __init__(self, deps): - self.deps = deps - - ctx = MockCtx(deps) - repl = _get_or_create_repl(ctx) - - assert len(context.code_executions) == 0 - - result = await repl.execute_async("x = 42") - assert result.success - - @pytest.mark.asyncio - async def test_code_execution_has_correct_fields(self, empty_client): +class TestCodeExecutionModel: + def test_code_execution_has_correct_fields(self): """Test that CodeExecution has all expected fields.""" execution = CodeExecution( code="print('hello')", @@ -89,29 +41,6 @@ class TestExecuteCodeTool: assert execution.stderr == "" assert execution.success is True - @pytest.mark.asyncio - async def test_code_execution_captures_errors(self, empty_client): - """Test that failed executions are properly captured.""" - from haiku.rag.agents.rlm.agent import _get_or_create_repl - - context = RLMContext() - deps = RLMDeps( - client=empty_client, - config=Config, - context=context, - ) - - class MockCtx: - def __init__(self, deps): - self.deps = deps - - ctx = MockCtx(deps) - repl = _get_or_create_repl(ctx) - - result = await repl.execute_async("1/0") - assert result.success is False - assert "ZeroDivisionError" in result.stderr - class TestClientRLMIntegration: """Integration tests for client.rlm() method.""" diff --git a/tests/agents/rlm/test_sandbox.py b/tests/agents/rlm/test_sandbox.py index 88eb9a07..733c3235 100644 --- a/tests/agents/rlm/test_sandbox.py +++ b/tests/agents/rlm/test_sandbox.py @@ -2,747 +2,246 @@ from pathlib import Path import pytest +from haiku.rag.agents.rlm.dependencies import RLMContext +from haiku.rag.agents.rlm.docker_sandbox import DockerSandbox, SandboxResult +from haiku.rag.client import HaikuRAG +from haiku.rag.config.models import RLMConfig + @pytest.fixture(scope="module") def vcr_cassette_dir(): return str(Path(__file__).parent.parent.parent / "cassettes" / "test_sandbox") -class TestSafeBuiltins: - """Test that safe builtins are available.""" +def is_docker_available() -> bool: + """Check if Docker daemon is available.""" + try: + import docker + client = docker.from_env() + client.ping() + return True + except Exception: + return False + + +docker_required = pytest.mark.skipif( + not is_docker_available(), + reason="Docker daemon not available", +) + + +@pytest.mark.integration +class TestDockerSandboxBasics: + """Test basic Docker sandbox functionality.""" + + @docker_required @pytest.mark.asyncio - async def test_print_available(self, repl_env_empty): - result = await repl_env_empty.execute_async("print('hello')") + async def test_execute_simple_code(self, docker_sandbox): + """Test executing simple code in the sandbox.""" + result = await docker_sandbox.execute("print('hello world')") + assert isinstance(result, SandboxResult) assert result.success - assert "hello" in result.stdout + assert "hello world" in result.stdout + assert result.stderr == "" + +@pytest.mark.integration +class TestDockerSandboxErrors: + """Test error handling in Docker sandbox.""" + + @docker_required @pytest.mark.asyncio - async def test_len_available(self, repl_env_empty): - result = await repl_env_empty.execute_async("print(len([1, 2, 3]))") - assert result.success - assert "3" in result.stdout - - @pytest.mark.asyncio - async def test_range_available(self, repl_env_empty): - result = await repl_env_empty.execute_async("print(list(range(3)))") - assert result.success - assert "[0, 1, 2]" in result.stdout - - @pytest.mark.asyncio - async def test_enumerate_available(self, repl_env_empty): - result = await repl_env_empty.execute_async( - "print(list(enumerate(['a', 'b'])))" - ) - assert result.success - assert "[(0, 'a'), (1, 'b')]" in result.stdout - - @pytest.mark.asyncio - async def test_sorted_available(self, repl_env_empty): - result = await repl_env_empty.execute_async("print(sorted([3, 1, 2]))") - assert result.success - assert "[1, 2, 3]" in result.stdout - - @pytest.mark.asyncio - async def test_sum_available(self, repl_env_empty): - result = await repl_env_empty.execute_async("print(sum([1, 2, 3]))") - assert result.success - assert "6" in result.stdout - - @pytest.mark.asyncio - async def test_min_max_available(self, repl_env_empty): - result = await repl_env_empty.execute_async( - "print(min([3, 1, 2]), max([3, 1, 2]))" - ) - assert result.success - assert "1 3" in result.stdout - - @pytest.mark.asyncio - async def test_all_any_available(self, repl_env_empty): - result = await repl_env_empty.execute_async( - "print(all([True, True]), any([False, True]))" - ) - assert result.success - assert "True True" in result.stdout - - @pytest.mark.asyncio - async def test_dict_list_set_tuple_available(self, repl_env_empty): - result = await repl_env_empty.execute_async( - "print(dict(a=1), list((1,2)), set([1,2,1]), tuple([1,2]))" - ) - assert result.success - assert "{'a': 1}" in result.stdout - - @pytest.mark.asyncio - async def test_str_int_float_bool_available(self, repl_env_empty): - result = await repl_env_empty.execute_async( - "print(str(1), int('2'), float('3.0'), bool(1))" - ) - assert result.success - assert "1 2 3.0 True" in result.stdout - - @pytest.mark.asyncio - async def test_zip_map_filter_available(self, repl_env_empty): - result = await repl_env_empty.execute_async( - "print(list(zip([1,2], ['a','b'])), " - "list(map(str, [1,2])), " - "list(filter(lambda x: x > 1, [1,2,3])))" - ) - assert result.success - assert "[(1, 'a'), (2, 'b')]" in result.stdout - - @pytest.mark.asyncio - async def test_isinstance_type_available(self, repl_env_empty): - result = await repl_env_empty.execute_async( - "print(isinstance(1, int), type([]))" - ) - assert result.success - assert "True" in result.stdout - - -class TestDangerousBuiltinsBlocked: - """Test that dangerous builtins are blocked.""" - - @pytest.mark.asyncio - async def test_eval_blocked(self, repl_env_empty): - result = await repl_env_empty.execute_async("eval('1+1')") + async def test_syntax_error(self, docker_sandbox): + """Test that syntax errors are reported.""" + result = await docker_sandbox.execute("def foo(") assert not result.success - assert "eval" in result.stderr.lower() or "not defined" in result.stderr.lower() + assert "SyntaxError" in result.stderr + @docker_required @pytest.mark.asyncio - async def test_exec_blocked(self, repl_env_empty): - result = await repl_env_empty.execute_async("exec('x = 1')") + async def test_runtime_error(self, docker_sandbox): + """Test that runtime errors are reported.""" + result = await docker_sandbox.execute("x = 1/0") assert not result.success - assert "exec" in result.stderr.lower() or "not defined" in result.stderr.lower() + assert "ZeroDivisionError" in result.stderr + @docker_required @pytest.mark.asyncio - async def test_compile_blocked(self, repl_env_empty): - result = await repl_env_empty.execute_async( - "compile('1+1', '', 'eval')" - ) + async def test_name_error(self, docker_sandbox): + """Test that name errors are reported.""" + result = await docker_sandbox.execute("print(undefined_variable)") assert not result.success - assert ( - "compile" in result.stderr.lower() or "not defined" in result.stderr.lower() - ) + assert "NameError" in result.stderr + @docker_required @pytest.mark.asyncio - async def test_open_blocked(self, repl_env_empty): - result = await repl_env_empty.execute_async("open('/etc/passwd')") - assert not result.success - assert "open" in result.stderr.lower() or "not defined" in result.stderr.lower() + async def test_missing_image(self, temp_db_path): + """Test error when Docker image is not found.""" + async with HaikuRAG(temp_db_path, create=True) as client: + config = RLMConfig(docker_image="nonexistent-image:v999.999.999") + context = RLMContext() + async with DockerSandbox( + client=client, config=config, context=context, image=config.docker_image + ) as sandbox: + result = await sandbox.execute("print('hello')") + assert not result.success + assert ( + "not found" in result.stderr.lower() + or "error" in result.stderr.lower() + ) + +@pytest.mark.integration +class TestDockerSandboxHaikuRAG: + """Test haiku.rag functions in Docker sandbox.""" + + @docker_required @pytest.mark.asyncio - async def test_input_blocked(self, repl_env_empty): - result = await repl_env_empty.execute_async("input('Enter: ')") - assert not result.success - assert ( - "input" in result.stderr.lower() or "not defined" in result.stderr.lower() - ) - - @pytest.mark.asyncio - async def test___import___blocked(self, repl_env_empty): - result = await repl_env_empty.execute_async("__import__('os')") - assert not result.success - - @pytest.mark.asyncio - async def test_globals_blocked(self, repl_env_empty): - result = await repl_env_empty.execute_async("globals()") - assert not result.success - - @pytest.mark.asyncio - async def test_locals_blocked(self, repl_env_empty): - result = await repl_env_empty.execute_async("locals()") - assert not result.success - - @pytest.mark.asyncio - async def test_breakpoint_blocked(self, repl_env_empty): - result = await repl_env_empty.execute_async("breakpoint()") - assert not result.success - - @pytest.mark.asyncio - async def test_getattr_setattr_blocked(self, repl_env_empty): - result = await repl_env_empty.execute_async("getattr(object, '__class__')") - assert not result.success - - @pytest.mark.asyncio - async def test_delattr_blocked(self, repl_env_empty): - result = await repl_env_empty.execute_async("delattr(object, 'x')") - assert not result.success - - -class TestAllowedImports: - """Test that allowed imports work.""" - - @pytest.mark.asyncio - async def test_json_import(self, repl_env_empty): - result = await repl_env_empty.execute_async( - "import json\nprint(json.dumps({'a': 1}))" - ) - assert result.success - assert '{"a": 1}' in result.stdout - - @pytest.mark.asyncio - async def test_re_import(self, repl_env_empty): - result = await repl_env_empty.execute_async( - "import re\nprint(re.match(r'\\d+', '123').group())" - ) - assert result.success - assert "123" in result.stdout - - @pytest.mark.asyncio - async def test_math_import(self, repl_env_empty): - result = await repl_env_empty.execute_async("import math\nprint(math.sqrt(4))") - assert result.success - assert "2.0" in result.stdout - - @pytest.mark.asyncio - async def test_statistics_import(self, repl_env_empty): - result = await repl_env_empty.execute_async( - "import statistics\nprint(statistics.mean([1, 2, 3]))" - ) - assert result.success - assert "2" in result.stdout - - @pytest.mark.asyncio - async def test_collections_import(self, repl_env_empty): - result = await repl_env_empty.execute_async( - "from collections import Counter\nprint(Counter(['a', 'b', 'a']))" - ) - assert result.success - assert "'a': 2" in result.stdout - - @pytest.mark.asyncio - async def test_itertools_import(self, repl_env_empty): - result = await repl_env_empty.execute_async( - "from itertools import chain\nprint(list(chain([1], [2])))" - ) - assert result.success - assert "[1, 2]" in result.stdout - - @pytest.mark.asyncio - async def test_functools_import(self, repl_env_empty): - result = await repl_env_empty.execute_async( - "from functools import reduce\nprint(reduce(lambda a, b: a+b, [1,2,3]))" - ) - assert result.success - assert "6" in result.stdout - - @pytest.mark.asyncio - async def test_datetime_import(self, repl_env_empty): - result = await repl_env_empty.execute_async( - "from datetime import date\nprint(date(2025, 1, 1))" - ) - assert result.success - assert "2025-01-01" in result.stdout - - -class TestDangerousImportsBlocked: - """Test that dangerous imports are blocked.""" - - @pytest.mark.asyncio - async def test_os_blocked(self, repl_env_empty): - result = await repl_env_empty.execute_async("import os") - assert not result.success - assert ( - "not allowed" in result.stderr.lower() or "error" in result.stderr.lower() - ) - - @pytest.mark.asyncio - async def test_sys_blocked(self, repl_env_empty): - result = await repl_env_empty.execute_async("import sys") - assert not result.success - - @pytest.mark.asyncio - async def test_subprocess_blocked(self, repl_env_empty): - result = await repl_env_empty.execute_async("import subprocess") - assert not result.success - - @pytest.mark.asyncio - async def test_shutil_blocked(self, repl_env_empty): - result = await repl_env_empty.execute_async("import shutil") - assert not result.success - - @pytest.mark.asyncio - async def test_socket_blocked(self, repl_env_empty): - result = await repl_env_empty.execute_async("import socket") - assert not result.success - - @pytest.mark.asyncio - async def test_requests_blocked(self, repl_env_empty): - result = await repl_env_empty.execute_async("import requests") - assert not result.success - - @pytest.mark.asyncio - async def test_builtins_module_blocked(self, repl_env_empty): - result = await repl_env_empty.execute_async("import builtins") - assert not result.success - - -class TestHaikuRAGBridgeFunctions: - """Test haiku.rag bridge functions in sandbox.""" - - @pytest.mark.asyncio - async def test_search(self, repl_env_empty): - """Test search function calls client with correct args.""" - from unittest.mock import AsyncMock - - from haiku.rag.store.models import SearchResult - - mock_results = [ - SearchResult( - chunk_id="chunk-1", - document_id="doc-1", - document_title="Test Doc", - document_uri="test://doc", - content="Test content about foxes", - score=0.9, - page_numbers=[1], - headings=["Heading"], - ) - ] - repl_env_empty.client.search = AsyncMock(return_value=mock_results) - - result = await repl_env_empty.execute_async( - "results = search('fox', limit=5)\n" - "print(len(results), results[0]['chunk_id'], 'fox' in results[0]['content'].lower())" - ) - assert result.success - assert "1 chunk-1 True" in result.stdout - repl_env_empty.client.search.assert_called_once_with( - "fox", limit=5, filter=None - ) - - @pytest.mark.asyncio - async def test_list_documents(self, repl_env_empty): - """Test list_documents returns list structure.""" - result = await repl_env_empty.execute_async( + async def test_list_documents_empty(self, docker_sandbox): + """Test list_documents returns empty list for empty database.""" + result = await docker_sandbox.execute( "docs = list_documents()\nprint(type(docs).__name__, len(docs))" ) assert result.success assert "list 0" in result.stdout - @pytest.mark.asyncio - async def test_get_document(self, repl_env_empty): - """Test get_document calls client correctly.""" - from unittest.mock import AsyncMock - - from haiku.rag.store.models import Document - - mock_doc = Document( - id="doc-1", - uri="test://doc", - title="Test Doc", - content="The quick brown fox", - ) - repl_env_empty.client.get_document_by_id = AsyncMock(return_value=mock_doc) - - result = await repl_env_empty.execute_async( - "doc = get_document('doc-1')\nprint('fox' in doc.lower())" - ) - assert result.success - assert "True" in result.stdout - - @pytest.mark.asyncio - async def test_get_document_missing(self, repl_env_empty): - """Test get_document returns None for missing document.""" - result = await repl_env_empty.execute_async( - "doc = get_document('Nonexistent')\nprint(doc is None)" - ) - assert result.success - assert "True" in result.stdout - - @pytest.mark.asyncio - async def test_llm(self, repl_env_empty): - """Test llm function is available in sandbox.""" - result = await repl_env_empty.execute_async("print(callable(llm))") - assert result.success - assert "True" in result.stdout - - -class TestSandboxExecution: - """Test general sandbox execution behavior.""" - - @pytest.mark.asyncio - async def test_variable_persistence(self, repl_env_empty): - """Variables persist across executions.""" - await repl_env_empty.execute_async("x = 42") - result = await repl_env_empty.execute_async("print(x)") - assert result.success - assert "42" in result.stdout - - @pytest.mark.asyncio - async def test_function_definition(self, repl_env_empty): - """Can define and call functions.""" - result = await repl_env_empty.execute_async( - "def add(a, b):\n return a + b\nprint(add(1, 2))" - ) - assert result.success - assert "3" in result.stdout - - @pytest.mark.asyncio - async def test_class_definition(self, repl_env_empty): - """Can define and use classes.""" - result = await repl_env_empty.execute_async( - "class Point:\n" - " def __init__(self, x, y):\n" - " self.x = x\n" - " self.y = y\n" - "p = Point(1, 2)\n" - "print(p.x, p.y)" - ) - assert result.success - assert "1 2" in result.stdout - - @pytest.mark.asyncio - async def test_list_comprehension(self, repl_env_empty): - """List comprehensions work.""" - result = await repl_env_empty.execute_async("print([x**2 for x in range(5)])") - assert result.success - assert "[0, 1, 4, 9, 16]" in result.stdout - - @pytest.mark.asyncio - async def test_dict_comprehension(self, repl_env_empty): - """Dict comprehensions work.""" - result = await repl_env_empty.execute_async( - "print({x: x**2 for x in range(3)})" - ) - assert result.success - assert "{0: 0, 1: 1, 2: 4}" in result.stdout - - @pytest.mark.asyncio - async def test_exception_handling(self, repl_env_empty): - """Can catch and handle exceptions.""" - result = await repl_env_empty.execute_async( - "try:\n x = 1/0\nexcept ZeroDivisionError:\n print('caught')" - ) - assert result.success - assert "caught" in result.stdout - - @pytest.mark.asyncio - async def test_uncaught_exception_reports_error(self, repl_env_empty): - """Uncaught exceptions are reported.""" - result = await repl_env_empty.execute_async("x = 1/0") - assert not result.success - assert "ZeroDivisionError" in result.stderr - - @pytest.mark.asyncio - async def test_syntax_error_reports_error(self, repl_env_empty): - """Syntax errors are reported.""" - result = await repl_env_empty.execute_async("def foo(") - assert not result.success - assert "SyntaxError" in result.stderr - - @pytest.mark.asyncio - async def test_output_truncation(self, repl_env_empty): - """Output is truncated if too long.""" - repl_env_empty.config.max_output_chars = 100 - result = await repl_env_empty.execute_async("print('x' * 1000)") - assert result.success - assert ( - len(result.stdout) <= 100 + 50 - ) # Allow some margin for truncation message - - -class TestContextFilter: - """Test that context filter is applied to all searches.""" - - @pytest.mark.asyncio - async def test_context_filter_applied_to_search(self, temp_db_path): - """Search applies context filter automatically.""" - from unittest.mock import AsyncMock - - from haiku.rag.agents.rlm.dependencies import RLMContext - from haiku.rag.agents.rlm.sandbox import REPLEnvironment - from haiku.rag.client import HaikuRAG - from haiku.rag.config.models import RLMConfig - - async with HaikuRAG(temp_db_path, create=True) as client: - context = RLMContext(filter="uri LIKE '%medical%'") - repl = REPLEnvironment(client=client, config=RLMConfig(), context=context) - client.search = AsyncMock(return_value=[]) - - await repl.execute_async("search('test query')") - - client.search.assert_called_once_with( - "test query", limit=10, filter="uri LIKE '%medical%'" - ) - - @pytest.mark.asyncio - async def test_context_filter_applied_to_list_documents(self, temp_db_path): - """list_documents applies context filter automatically.""" - from unittest.mock import AsyncMock - - from haiku.rag.agents.rlm.dependencies import RLMContext - from haiku.rag.agents.rlm.sandbox import REPLEnvironment - from haiku.rag.client import HaikuRAG - from haiku.rag.config.models import RLMConfig - - async with HaikuRAG(temp_db_path, create=True) as client: - context = RLMContext(filter="title = 'Report'") - repl = REPLEnvironment(client=client, config=RLMConfig(), context=context) - client.list_documents = AsyncMock(return_value=[]) - - await repl.execute_async("list_documents()") - - client.list_documents.assert_called_once_with( - limit=10, offset=0, filter="title = 'Report'" - ) - - -class TestPreloadedDocuments: - """Test pre-loaded documents context variable.""" - - @pytest.mark.asyncio - async def test_documents_variable_available_when_preloaded(self, temp_db_path): - """documents variable is available when context.documents is set.""" - from haiku.rag.agents.rlm.dependencies import RLMContext - from haiku.rag.agents.rlm.sandbox import REPLEnvironment - from haiku.rag.client import HaikuRAG - from haiku.rag.config.models import RLMConfig - from haiku.rag.store.models import Document - - async with HaikuRAG(temp_db_path, create=True) as client: - preloaded = [ - Document( - id="doc-1", - title="First Doc", - uri="test://first", - content="Content of first document about cats.", - ), - Document( - id="doc-2", - title="Second Doc", - uri="test://second", - content="Content of second document about dogs.", - ), - ] - context = RLMContext(documents=preloaded) - repl = REPLEnvironment(client=client, config=RLMConfig(), context=context) - - result = await repl.execute_async( - "print(len(documents))\n" - "print([d['title'] for d in documents])\n" - "print('cats' in documents[0]['content'])" - ) - assert result.success - assert "2" in result.stdout - assert "First Doc" in result.stdout - assert "Second Doc" in result.stdout - assert "True" in result.stdout - - @pytest.mark.asyncio - async def test_documents_variable_not_available_without_preload(self, temp_db_path): - """documents variable is not available when context.documents is None.""" - from haiku.rag.agents.rlm.dependencies import RLMContext - from haiku.rag.agents.rlm.sandbox import REPLEnvironment - from haiku.rag.client import HaikuRAG - from haiku.rag.config.models import RLMConfig - - async with HaikuRAG(temp_db_path, create=True) as client: - context = RLMContext() - repl = REPLEnvironment(client=client, config=RLMConfig(), context=context) - - result = await repl.execute_async("print(documents)") - assert not result.success - assert "NameError" in result.stderr - - @pytest.mark.asyncio - async def test_documents_has_expected_fields(self, temp_db_path): - """documents variable contains expected dict fields.""" - from haiku.rag.agents.rlm.dependencies import RLMContext - from haiku.rag.agents.rlm.sandbox import REPLEnvironment - from haiku.rag.client import HaikuRAG - from haiku.rag.config.models import RLMConfig - from haiku.rag.store.models import Document - - async with HaikuRAG(temp_db_path, create=True) as client: - preloaded = [ - Document( - id="doc-1", - title="Test Doc", - uri="test://doc", - content="Test content", - ), - ] - context = RLMContext(documents=preloaded) - repl = REPLEnvironment(client=client, config=RLMConfig(), context=context) - - result = await repl.execute_async( - "d = documents[0]\n" - "print(sorted(d.keys()))\n" - "print(d['id'], d['title'], d['uri'])" - ) - assert result.success - assert "['content', 'id', 'title', 'uri']" in result.stdout - assert "doc-1" in result.stdout - assert "Test Doc" in result.stdout - assert "test://doc" in result.stdout - - -class TestSandboxEscapeVectors: - """Test that known sandbox escape techniques are blocked. - - Each test contains actual exploit code that would work without the fix. - """ - - @pytest.mark.asyncio - async def test_type_dict_subclasses_escape_blocked(self, repl_env_empty): - """Cannot escape via type.__dict__['__subclasses__']. - - Without fix: This would enumerate all loaded classes and find - subprocess.Popen to execute arbitrary shell commands. - """ - result = await repl_env_empty.execute_async(""" -# EXPLOIT: Access __subclasses__ via dict to bypass AST check -subclasses_method = type.__dict__['__subclasses__'] -all_classes = subclasses_method(object) -print(f"Found {len(all_classes)} classes") -""") - assert not result.success - assert "not allowed" in result.stderr.lower() - - @pytest.mark.asyncio - async def test_popen_shell_execution_blocked(self, repl_env_empty): - """Cannot execute shell commands via Popen. - - Without fix: This would execute 'whoami' and return the username. - """ - result = await repl_env_empty.execute_async(""" -# EXPLOIT: Find subprocess.Popen and execute shell commands -subclasses_method = type.__dict__['__subclasses__'] -all_classes = subclasses_method(object) -popen = [c for c in all_classes if c.__name__ == 'Popen'][0] -proc = popen('whoami', shell=True, stdout=-1) -print(proc.stdout.read()) -""") - assert not result.success - - @pytest.mark.asyncio - async def test_socket_creation_blocked(self, repl_env_empty): - """Cannot create network sockets for data exfiltration. - - Without fix: This would create a socket that could connect to external servers. - """ - result = await repl_env_empty.execute_async(""" -# EXPLOIT: Find socket class and create network connection -subclasses_method = type.__dict__['__subclasses__'] -all_classes = subclasses_method(object) -socket_cls = [c for c in all_classes if c.__name__ == 'socket'][0] -s = socket_cls(2, 1) # AF_INET, SOCK_STREAM -print(f"Created socket: {s}") -""") - assert not result.success - - @pytest.mark.asyncio - async def test_type_three_arg_class_creation_blocked(self, repl_env_empty): - """Cannot use type() with 3 arguments to create classes dynamically.""" - result = await repl_env_empty.execute_async( - "EvilClass = type('EvilClass', (object,), {'x': 1})" - ) - assert not result.success - - @pytest.mark.asyncio - async def test_dict_key_dunder_access_blocked(self, repl_env_empty): - """Cannot access dunder methods via dictionary key access.""" - result = await repl_env_empty.execute_async( - "method = str.__dict__['__add__']\nprint(method)" - ) - assert not result.success - assert "not allowed" in result.stderr.lower() - - @pytest.mark.asyncio - async def test_dict_key_private_access_blocked(self, repl_env_empty): - """Cannot access private attributes via dictionary key access.""" - result = await repl_env_empty.execute_async( - "method = object.__dict__['_private']\nprint(method)" - ) - assert not result.success - assert "not allowed" in result.stderr.lower() - + @docker_required @pytest.mark.asyncio @pytest.mark.vcr() - async def test_sql_injection_in_get_document_blocked(self, temp_db_path): - """SQL injection in get_document cannot bypass context filter. - - Without fix: Injecting quotes would leak documents that should be - protected by the context filter. - """ - from haiku.rag.agents.rlm.dependencies import RLMContext - from haiku.rag.agents.rlm.sandbox import REPLEnvironment - from haiku.rag.client import HaikuRAG - from haiku.rag.config.models import RLMConfig - + async def test_list_documents_with_data(self, temp_db_path, test_docker_image): + """Test list_documents returns documents when populated.""" async with HaikuRAG(temp_db_path, create=True) as client: - # Create documents: one secret, one public await client.create_document( - content="TOP SECRET: Launch codes 1234", - uri="secret://classified", - title="Classified Intel", - ) - await client.create_document( - content="Public weather report", - uri="public://weather", - title="Weather", + content="Test content", + uri="test://doc1", + title="Test Document", ) - # Sandbox restricted to public:// only + config = RLMConfig(docker_image=test_docker_image) + context = RLMContext() + async with DockerSandbox( + client=client, config=config, context=context, image=test_docker_image + ) as sandbox: + result = await sandbox.execute( + "docs = list_documents()\nprint(len(docs))\nprint(docs[0]['title'])" + ) + assert result.success + assert "1" in result.stdout + assert "Test Document" in result.stdout + + @docker_required + @pytest.mark.asyncio + @pytest.mark.vcr() + async def test_search_with_data(self, temp_db_path, test_docker_image): + """Test search function works.""" + async with HaikuRAG(temp_db_path, create=True) as client: + await client.create_document( + content="The quick brown fox jumps over the lazy dog.", + uri="test://animals", + title="Animals", + ) + + config = RLMConfig(docker_image=test_docker_image) + context = RLMContext() + async with DockerSandbox( + client=client, config=config, context=context, image=test_docker_image + ) as sandbox: + result = await sandbox.execute( + "results = search('fox', limit=5)\n" + "print(len(results))\n" + "if results:\n" + " print('fox' in results[0]['content'].lower())" + ) + assert result.success + # Search should return at least one result + assert "True" in result.stdout or "1" in result.stdout + + @docker_required + @pytest.mark.asyncio + @pytest.mark.vcr() + async def test_get_document(self, temp_db_path, test_docker_image): + """Test get_document function.""" + async with HaikuRAG(temp_db_path, create=True) as client: + doc = await client.create_document( + content="Content about foxes and dogs.", + uri="test://doc", + title="Fox Document", + ) + + config = RLMConfig(docker_image=test_docker_image) + context = RLMContext() + async with DockerSandbox( + client=client, config=config, context=context, image=test_docker_image + ) as sandbox: + result = await sandbox.execute( + f"content = get_document('{doc.id}')\n" + "print('foxes' in content.lower() if content else 'None')" + ) + assert result.success + assert "True" in result.stdout + + @docker_required + @pytest.mark.asyncio + async def test_get_document_not_found(self, docker_sandbox): + """Test get_document returns None for missing document.""" + result = await docker_sandbox.execute( + "content = get_document('nonexistent-id')\nprint(content is None)" + ) + assert result.success + assert "True" in result.stdout + + +@pytest.mark.integration +class TestDockerSandboxContextFilter: + """Test context filter is applied.""" + + @docker_required + @pytest.mark.asyncio + @pytest.mark.vcr() + async def test_filter_applied_to_list_documents( + self, temp_db_path, test_docker_image + ): + """Test that context filter is passed to list_documents.""" + async with HaikuRAG(temp_db_path, create=True) as client: + await client.create_document( + content="Public content", + uri="public://doc1", + title="Public Doc", + ) + await client.create_document( + content="Private content", + uri="private://doc2", + title="Private Doc", + ) + + config = RLMConfig(docker_image=test_docker_image) context = RLMContext(filter="uri LIKE 'public://%'") - repl = REPLEnvironment(client=client, config=RLMConfig(), context=context) - - # EXPLOIT: SQL injection to access secret document - result = await repl.execute_async(""" -# Injection payload breaks out of quotes and adds OR clause -content = get_document("x' OR uri LIKE 'secret://%") -if content: - print(f"LEAKED: {content}") -else: - print("NO LEAK") -""") - assert result.success - assert "TOP SECRET" not in result.stdout - assert "Launch codes" not in result.stdout + async with DockerSandbox( + client=client, config=config, context=context, image=test_docker_image + ) as sandbox: + result = await sandbox.execute( + "docs = list_documents()\n" + "print(len(docs))\n" + "if docs:\n" + " print(docs[0]['title'])" + ) + assert result.success + assert "1" in result.stdout + assert "Public Doc" in result.stdout + assert "Private Doc" not in result.stdout -class TestSecurityEscapes: - """Test that common security escape attempts are blocked.""" +@pytest.mark.integration +class TestDockerSandboxPreloadedDocuments: + """Test pre-loaded documents context variable.""" + @docker_required @pytest.mark.asyncio - async def test_eval_via_builtins_dict(self, repl_env_empty): - """Cannot access eval through __builtins__.""" - result = await repl_env_empty.execute_async("__builtins__['eval']('1+1')") - assert not result.success - - @pytest.mark.asyncio - async def test_import_via_builtins(self, repl_env_empty): - """Cannot import os through builtins trickery.""" - result = await repl_env_empty.execute_async("__builtins__.__import__('os')") - assert not result.success - - @pytest.mark.asyncio - async def test_class_bases_escape(self, repl_env_empty): - """Cannot escape through __class__.__bases__.""" - result = await repl_env_empty.execute_async( - "().__class__.__bases__[0].__subclasses__()" - ) - assert not result.success - - @pytest.mark.asyncio - async def test_code_object_escape(self, repl_env_empty): - """Cannot create code objects.""" - result = await repl_env_empty.execute_async( - "def f(): pass\n" - "type(f.__code__)(0, 0, 0, 0, 0, 0, b'', (), (), (), '', '', 0, b'')" - ) - assert not result.success - - @pytest.mark.asyncio - async def test_import_system_escape(self, repl_env_empty): - """Cannot escape through importlib.""" - result = await repl_env_empty.execute_async("import importlib") - assert not result.success - - @pytest.mark.asyncio - async def test_pickle_escape(self, repl_env_empty): - """Cannot use pickle for code execution.""" - result = await repl_env_empty.execute_async("import pickle") + async def test_documents_variable_not_available_without_preload( + self, docker_sandbox + ): + """documents variable is not available when context.documents is None.""" + result = await docker_sandbox.execute("print(documents)") assert not result.success + assert "NameError" in result.stderr diff --git a/tests/cassettes/test_sandbox/TestDockerSandboxContextFilter.test_filter_applied_to_list_documents.yaml b/tests/cassettes/test_sandbox/TestDockerSandboxContextFilter.test_filter_applied_to_list_documents.yaml new file mode 100644 index 00000000..32d20513 --- /dev/null +++ b/tests/cassettes/test_sandbox/TestDockerSandboxContextFilter.test_filter_applied_to_list_documents.yaml @@ -0,0 +1,82 @@ +interactions: +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '84' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + encoding_format: base64 + input: + - Public content + model: qwen3-embedding:4b + uri: http://localhost:11434/v1/embeddings + response: + headers: + content-type: + - application/json + transfer-encoding: + - chunked + parsed_body: + data: + - embedding: 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 + index: 0 + object: embedding + model: qwen3-embedding:4b + object: list + usage: + prompt_tokens: 3 + total_tokens: 3 + status: + code: 200 + message: OK +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '85' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + encoding_format: base64 + input: + - Private content + model: qwen3-embedding:4b + uri: http://localhost:11434/v1/embeddings + response: + headers: + content-type: + - application/json + transfer-encoding: + - chunked + parsed_body: + data: + - embedding: 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 + index: 0 + object: embedding + model: qwen3-embedding:4b + object: list + usage: + prompt_tokens: 3 + total_tokens: 3 + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/test_sandbox/TestDockerSandboxHaikuRAG.test_get_document.yaml b/tests/cassettes/test_sandbox/TestDockerSandboxHaikuRAG.test_get_document.yaml new file mode 100644 index 00000000..b1ded61e --- /dev/null +++ b/tests/cassettes/test_sandbox/TestDockerSandboxHaikuRAG.test_get_document.yaml @@ -0,0 +1,42 @@ +interactions: +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '99' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + encoding_format: base64 + input: + - Content about foxes and dogs. + model: qwen3-embedding:4b + uri: http://localhost:11434/v1/embeddings + response: + headers: + content-type: + - application/json + transfer-encoding: + - chunked + parsed_body: + data: + - embedding: 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 + index: 0 + object: embedding + model: qwen3-embedding:4b + object: list + usage: + prompt_tokens: 8 + total_tokens: 8 + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/test_sandbox/TestDockerSandboxHaikuRAG.test_list_documents_with_data.yaml b/tests/cassettes/test_sandbox/TestDockerSandboxHaikuRAG.test_list_documents_with_data.yaml new file mode 100644 index 00000000..b6252ae6 --- /dev/null +++ b/tests/cassettes/test_sandbox/TestDockerSandboxHaikuRAG.test_list_documents_with_data.yaml @@ -0,0 +1,42 @@ +interactions: +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '82' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + encoding_format: base64 + input: + - Test content + model: qwen3-embedding:4b + uri: http://localhost:11434/v1/embeddings + response: + headers: + content-type: + - application/json + transfer-encoding: + - chunked + parsed_body: + data: + - embedding: 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 + index: 0 + object: embedding + model: qwen3-embedding:4b + object: list + usage: + prompt_tokens: 3 + total_tokens: 3 + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/test_sandbox/TestDockerSandboxHaikuRAG.test_search_with_data.yaml b/tests/cassettes/test_sandbox/TestDockerSandboxHaikuRAG.test_search_with_data.yaml new file mode 100644 index 00000000..b12b11cd --- /dev/null +++ b/tests/cassettes/test_sandbox/TestDockerSandboxHaikuRAG.test_search_with_data.yaml @@ -0,0 +1,42 @@ +interactions: +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '114' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + encoding_format: base64 + input: + - The quick brown fox jumps over the lazy dog. + model: qwen3-embedding:4b + uri: http://localhost:11434/v1/embeddings + response: + headers: + content-type: + - application/json + transfer-encoding: + - chunked + parsed_body: + data: + - embedding: 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 + index: 0 + object: embedding + model: qwen3-embedding:4b + object: list + usage: + prompt_tokens: 11 + total_tokens: 11 + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/test_sandbox/TestSandboxEscapeVectors.test_sql_injection_in_get_document_blocked.yaml b/tests/cassettes/test_sandbox/TestSandboxEscapeVectors.test_sql_injection_in_get_document_blocked.yaml deleted file mode 100644 index a2a27516..00000000 --- a/tests/cassettes/test_sandbox/TestSandboxEscapeVectors.test_sql_injection_in_get_document_blocked.yaml +++ /dev/null @@ -1,82 +0,0 @@ -interactions: -- request: - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate, zstd - connection: - - keep-alive - content-length: - - '99' - content-type: - - application/json - host: - - localhost:11434 - method: POST - parsed_body: - encoding_format: base64 - input: - - 'TOP SECRET: Launch codes 1234' - model: qwen3-embedding:4b - uri: http://localhost:11434/v1/embeddings - response: - headers: - content-type: - - application/json - transfer-encoding: - - chunked - parsed_body: - data: - - embedding: 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 - index: 0 - object: embedding - model: qwen3-embedding:4b - object: list - usage: - prompt_tokens: 11 - total_tokens: 11 - status: - code: 200 - message: OK -- request: - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate, zstd - connection: - - keep-alive - content-length: - - '91' - content-type: - - application/json - host: - - localhost:11434 - method: POST - parsed_body: - encoding_format: base64 - input: - - Public weather report - model: qwen3-embedding:4b - uri: http://localhost:11434/v1/embeddings - response: - headers: - content-type: - - application/json - transfer-encoding: - - chunked - parsed_body: - data: - - embedding: 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 - index: 0 - object: embedding - model: qwen3-embedding:4b - object: list - usage: - prompt_tokens: 4 - total_tokens: 4 - status: - code: 200 - message: OK -version: 1 diff --git a/uv.lock b/uv.lock index d8f88265..bf87e61c 100644 --- a/uv.lock +++ b/uv.lock @@ -739,6 +739,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, ] +[[package]] +name = "docker" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "requests" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/9b/4a2ea29aeba62471211598dac5d96825bb49348fa07e906ea930394a83ce/docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c", size = 117834, upload-time = "2024-05-23T11:13:57.216Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774, upload-time = "2024-05-23T11:13:55.01Z" }, +] + [[package]] name = "docling" version = "2.69.1" @@ -1366,6 +1380,7 @@ name = "haiku-rag-slim" version = "0.28.0" source = { editable = "haiku_rag_slim" } dependencies = [ + { name = "docker" }, { name = "docling-core" }, { name = "httpx" }, { name = "jsonpatch" }, @@ -1427,6 +1442,7 @@ zeroentropy = [ [package.metadata] requires-dist = [ { name = "cohere", marker = "extra == 'cohere'", specifier = ">=5.20.1" }, + { name = "docker", specifier = ">=7.1.0" }, { name = "docling", marker = "extra == 'docling'", specifier = "==2.69.1" }, { name = "docling-core", specifier = "==2.60.1" }, { name = "httpx", specifier = ">=0.28.1" },