Skip to content

Repository files navigation

AI Sandbox

AI Sandbox

A self-hosted code-execution sandbox for LLM agents — built from scratch on Docker, not rented from E2B.

Upload a CSV → ask in plain English → an LLM agent writes Python, runs it in an isolated container, and answers with real charts.


React 19 · TypeScript · Express · OpenRouter · Docker · Agentic tool-calling


Most "AI data analyst" demos either (a) let the model hallucinate numbers from the CSV text, or (b) outsource execution to a managed sandbox like E2B. This project does neither: it runs untrusted, model-generated code in a Docker sandbox built and operated in this repo, and feeds the actual runtime output back into the agent loop.


See it in action

The agent receives a question and a CSV, writes Python, executes it in the sandbox, and returns prose plus the figures it actually rendered. The charts below are real output produced by the project's own pipeline from the included sample dataset:

Monthly revenue by product Profit by region
Monthly revenue by product Profit by region
The sample data the agent reasoned over (click to expand)
date product region units_sold revenue cost
2024-01-05 Shoes North 120 60000 36000
2024-01-12 Bags North 80 40000 16000
2024-01-18 Shoes South 95 47500 28500
2024-01-25 Bags South 60 30000 12000
2024-02-03 Shoes North 140 70000 42000
2024-02-10 Bags North 90 45000 18000

📸 Live UI: drop a screenshot/GIF of the chat interface here once captured — ![AI Sandbox UI](docs/assets/ui.png) (A 10–15s screen recording of "upload CSV → ask → chart appears" is the single highest-impact visual you can add.)


Why build the sandbox instead of using E2B?

E2B and similar managed sandboxes are excellent products. Building this layer myself was a deliberate choice, and the tradeoffs are the interesting part of the project:

  • Own the execution boundary. Code execution is where every security, cost, and latency decision actually lives. Treating it as a black box means you never learn where the boundary is — building it surfaces every decision (process isolation, filesystem mounts, output capture, lifecycle) explicitly.
  • No per-execution vendor cost or rate limit. Each run is a local docker run --rm; the only ceiling is the host.
  • Full control of the runtime image. The Python environment, libraries, and output contract (/output/*.png) are defined in this repo and reproducible anywhere Docker runs.
  • It's the part of the system worth being able to defend in an interview. Anyone can call an API. The systems thinking is in the sandbox.

Architecture

flowchart LR
    subgraph Client["🖥️  React UI (Vite + TS)"]
        UI["Chat · CSV upload · chart viewer"]
    end

    subgraph Server["⚙️  Express backend (TS)"]
        API["POST /chat"]
        LOOP["Agentic tool loop<br/>(max 10 iterations)"]
    end

    subgraph LLM["🤖  OpenRouter"]
        MODEL["gemini-2.5-flash-lite<br/>tool calling"]
    end

    subgraph Box["📦  Docker sandbox"]
        RUN["docker run --rm<br/>py_compile gate → python<br/>stdout · stderr · PNGs"]
    end

    UI -- "question + csvContent" --> API
    API --> LOOP
    LOOP -- "messages + tools" --> MODEL
    MODEL -- "run_code(code)" --> LOOP
    LOOP -- "execute" --> RUN
    RUN -- "stdout / charts" --> LOOP
    LOOP -- "answer + base64 charts" --> UI
Loading

Request flow

sequenceDiagram
    participant U as User
    participant B as Backend
    participant M as LLM
    participant S as Docker sandbox

    U->>B: question + CSV
    B->>M: system prompt forces tool use + CSV
    loop until answer (≤ 10 iterations)
        M->>B: run_code(python)
        B->>S: docker run --rm (compile → execute)
        S-->>B: stdout / stderr / PNGs
        B->>M: tool result
    end
    M-->>B: final answer
    B-->>U: answer + rendered charts
Loading

The agent loop

The backend runs a bounded tool-calling loop (runAgent, max 10 iterations):

  1. The LLM is forced via system prompt to call the run_code tool before answering — it cannot answer from memory when execution is available.
  2. The model emits Python (e.g. a pandas aggregation + a matplotlib chart saved to /output/).
  3. The backend executes it in a fresh container and captures stdout, stderr, and PNGs.
  4. The tool result is appended to the message history and handed back to the model.
  5. The model iterates (fix an error, refine) or, once it has executed code and has a final answer, returns prose + the collected charts.

The core idea: the model's beliefs are grounded in real execution output, not in its reading of the CSV.

The sandbox

Each run_code call (runInSandbox):

  • Writes the model's code to a unique scratch dir (/tmp/sandbox_<ts>/script.py).
  • Launches an ephemeral container (docker run --rm — destroyed on exit).
  • Bind-mounts the script and a dedicated /output directory for artifacts.
  • Runs a compile gate first (python -m py_compile) so syntax errors are caught cleanly before execution.
  • Harvests every .png from /output, base64-encodes it, and returns it with exitCode, stdout, and stderr.

Tech stack

Layer Stack
Frontend React 19, TypeScript, Vite
Backend Node 20, Express, TypeScript (ESM)
Agent / LLM OpenRouter SDK (google/gemini-2.5-flash-lite), tool calling
Sandbox Docker (Python 3.11 + pandas + matplotlib)
Packaging Multi-stage Dockerfiles, Docker Compose

Getting started

Prerequisites: Node 20+ & pnpm, a running Docker daemon, and an OpenRouter API key.

# 1. Build the sandbox image (backend executes code inside `my-python-app`)
docker build -t my-python-app .

# 2. Add your key to .env in the repo root
echo "open_router_key=sk-or-..." > .env

# 3. Backend
cd backend && pnpm install && pnpm dev      # http://localhost:3000

# 4. Frontend (new terminal)
cd frontend && pnpm install && pnpm dev      # Vite dev server

Open the frontend, drop in a CSV (sample sales.csv included), and ask something like "Which product has the highest profit margin, and show me the monthly revenue trend."

API

POST /chat
{
  "question":   "Which region is most profitable?",
  "csvContent": "date,product,region,units_sold,revenue,cost\n...",
  "messages":   []
}

{ ok: true, answer: string, images: [{ fileName, base64 }] } · GET /health{ status: "ok" }


Design decisions worth calling out

  • Execution-grounded answers. The system prompt hard-requires a tool call before any answer — eliminating the most common failure mode of CSV chatbots (summarizing numbers the model never computed).
  • Compile-before-run gate. Syntax errors return a clean signal so the model self-corrects instead of the container dying opaquely.
  • Bounded agent loop. A hard 10-iteration cap with graceful fallback to the last good stdout — the loop can't run away.
  • Artifact contract. A fixed /output convention keeps the sandbox↔backend interface simple and language-agnostic.
  • Stateless containers. --rm means no state leaks between executions; every run starts clean.

Roadmap

The honest hardening path from "works on my machine" to "I'd run this in production":

  • Sandbox hardening--network none, --memory / --cpus limits, --read-only root FS, non-root user, per-execution wall-clock timeout.
  • Streaming — stream tool output and tokens to the UI instead of awaiting the full loop.
  • Persistence — store conversations and uploaded files (Conversation/fileId are already modelled in the frontend types).
  • Multi-language sandboxes — the /output contract generalizes beyond Python.
  • Benchmarks — measured cold-start, warm-execution latency, and cost-per-query. (To be published once measured — none are claimed here.)

Status

Working prototype: single-image Python sandbox, single LLM provider, in-memory conversation state. Built to understand the code-execution layer of agentic systems end to end — from the browser, through the agent loop, down to the container boundary.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages