沙盒代码执行代理

一个基于Docker隔离的多语言沙盒代码执行代理,支持Python、Node.js和R,提供持久化状态、文件传输和LLM集成功能。

  • by eduresser
  • Python
  • 本地部署
  • 更新于
在 GitHub 查看 eduresser/sandbox-agent

使用说明

沙盒代理

基于Docker的沙盒代码执行的LangGraph代理。每个会话都在一个隔离的、强化的Docker容器中运行,该容器具有一个持久内核——Python的IPython, vm.createContext Node.js和专用的R环境。支持3个运行时、与提供者无关的LLM配置和视觉(多模式模型的自动检测)。可用作交互式CLI、MCP服务器(Cursor、Claude Desktop)、REST API(艾格拉),以及React前端。

特性

  • Docker隔离 --每个会话都在自己的容器中运行,没有暴露的端口,也没有主机卷
  • 硬化容器 --非根用户(UID 65532)、PID限制、内存+交换限制、仅tmpfs可写目录、, no-new-privileges
  • 碰撞检测 --检测到OOM杀伤、分叉炸弹、分段故障,并明确报告给特工
  • 持久状态 --变量在代码执行之间存活(如Jupyter单元格)
  • PostgreSQL检查点 --对话历史记录在重新启动后仍然存在(与Aegra共享)
  • 异步支持 --Promise(Node.js)和协程(Python)会自动等待
  • 多运行时 --Python、Node.js和R
  • 丰富的显示输出 --捕获matplotlib/ggplot图形、Plotly图表、IPython音频、HTML小部件等;自动将图像发送到多模式LLM
  • 提供者不可知 --通过以下方式与OpenAI、Anthropic、Google Gemini、Ollama或任何兼容提供商合作 langchain init_chat_model
  • 运行时软件包安装 — pip install / npm install / install.packages() 在会话创建时或通过终端
  • 6工具 — create_session, execute_code, execute_terminal, import_files, export_files, stop_session
  • MCP服务器 --通过模型上下文协议(stdio传输)公开相同的工具
  • REST API -完整的LangGraph平台API,通过 艾格拉 使用OpenAPI文档、流媒体、线程管理
  • 输入验证 --Pydantic模式在执行前验证所有工具输入,失败时返回结构化错误
  • React前端 --SPA聊天、工具可视化、文件上传/下载、设置对话框(React 19+Vite+Tailwind CSS)
  • 文件上传 -将文件上载到API以导入沙箱会话(POST /threads/{id}/files/upload)
  • 文件导出 --注册文件以供下载(无主机副本);通过API下载或在交叉会话导入中使用
  • 文件导入 --从主机路径、内联内容或其他会话(在同一对话中导出的文件)导入
  • 跨会话传输 --从会话A导出,导入到会话B {session_id, path}
  • 会话垃圾回收 --空闲超时、最大生存期、线程驱逐、孤立容器清理
  • 自动清理 --当代理退出时,所有容器都会停止并被移除

先决条件

  • Python 3.11+
  • Docker引擎
  • LLM提供商的API密钥(CHAT_MODEL_API_KEY)
  • PostgreSQL(适用于API/CLI模式-checkpointer+Aegra)
  • Node.js 18+和npm(用于React前端)

设置

# Docker — installs (if needed), configures permissions, and builds all 3 images
sudo ./setup-docker.sh

# Install Python dependencies (open a new terminal so the docker group is active)
uv sync

# Install frontend dependencies
cd frontend && npm install && cd ..

# Configure environment
cp .env.example .env
# Edit .env with your CHAT_MODEL_API_KEY, POSTGRES_PASSWORD, and other settings

# Docker images are also built automatically on first use if not already present

PostgreSQL(CLI、API和UI都需要)

PostgreSQL在使用时通过Docker Compose自动启动 localhostCLI检测PostgreSQL是否可访问并自动启动:

# Manual start (if needed)
docker compose up postgres -d

或者通过以下方式指向现有的PostgreSQL实例 POSTGRES_* env变量 .env.

用法

所有命令都使用统一 sandbox-agent 入口点:

uv run sandbox-agent cli       # Interactive CLI (default)
uv run sandbox-agent mcp       # MCP server (Cursor, Claude Desktop)
uv run sandbox-agent api       # REST API (Aegra, no reload)
uv run sandbox-agent api dev   # REST API with hot reload
uv run sandbox-agent ui        # React UI (auto-starts API if needed)

命令行界面

uv run sandbox-agent cli
# or simply
uv run sandbox-agent

CLI作为Aegra REST API之上的瘦客户端运行。要求API正在运行(uv run sandbox-agent api).特征:

  • 富有的 带有语法高亮显示的工具I/O的面板(每个运行时词法分析器)
  • Markdown渲染流媒体代理输出
  • 跨重启的持久线程(~/.local/state/sandbox-agent/cli-thread.json)
  • /new 开始新对话的命令
  • 通过将模型/提供商/密钥设置传递给API configurable

MCP 服务器

运行MCP服务器(stdio传输)以与Cursor、Claude Desktop或任何兼容MCP的客户端集成:

uv run sandbox-agent mcp

光标或克劳德桌面

添加以下MCP配置:

{
  "mcpServers": {
    "sandbox-agent": {
      "command": "uv",
      "args": ["--directory", "/path/to/sandbox-agent", "run", "sandbox-agent", "mcp"]
    }
  }
}

MCP服务器公开了与CLI代理相同的6个工具,具有相同的行为。它在中维护一个持久的thread_id ~/.local/state/sandbox-agent/mcp-thread.json 为了确保导出URL的一致性。

这 import_files 该工具直接接受文件内容(如文本或base64) file_content/encoding 密钥)、主机路径(通过 source/destination),或跨会话引用(session_id+path).这 export_files 该工具通过注册文件进行下载 GET /threads/{thread_id}/files/download?session_id=...&path=....

REST API(Aegra)

将代理作为REST API运行,通过 艾格拉 (自托管LangGraph平台替代方案):

uv run sandbox-agent api       # Production mode (no reload, auto-starts PostgreSQL)
uv run sandbox-agent api dev   # Development mode (hot reload via aegra dev)

如果无法在本地主机上访问PostgreSQL,则生产命令会通过Docker Compose自动启动PostgreSQL。服务器运行在 http://localhost:8000 OpenAPI文档位于 /docs。使用LangGraph SDK或curl创建助手、线程和流运行。与Agent Chat UI、LangGraph Studio和CopilotKit兼容。

自定义端点:

  • GET /threads/{thread_id}/files/download?session_id=...&path=... --流式传输从容器导出的文件
  • POST /threads/{thread_id}/files/upload --上传文件以供导入沙盒会话
  • DELETE /threads/{thread_id} --还清理该线程的Docker会话和存储(通过中间件)
  • GET /settings --返回在后端合并的持久化前端设置 .env 默认值
  • PUT /settings --将前端设置持久化到PostgreSQL(加密)

反应前端

通过Aegra API与代理商聊天的web UI(React 19+Vite+Tailwind CSS):

# Install frontend dependencies (if not done during setup)
cd frontend && npm install && cd ..

# Start the UI (auto-starts API + PostgreSQL if needed)
uv run sandbox-agent ui

前端运行在 http://localhost:5173 (带有API代理的Vite dev服务器 :8000).特征:

  • 通过侧边栏进行话题管理(创建、恢复、删除对话)
  • 使用可扩展的工具块流式传输响应(每次运行时突出显示语法)
  • 文件上传和下载支持
  • 思维块可视化
  • 设置对话框(模型、提供商、API密钥、基本URL、视觉切换)
  • 通过服务器端API进行持久设置(GET/PUT /settings),带后端 .env 默认为回退

程序化

from sandbox_agent.sandbox import SandboxManager

manager = SandboxManager()

info = manager.create_session(
    runtime="python",
    dependencies={"pandas": "2.2.3", "matplotlib": ""},
)
sid = info.session_id

r1 = manager.execute_code(sid, """
import pandas as pd
df = pd.DataFrame({'x': [1, 2, 3], 'y': [4, 5, 6]})
print(df.describe())
""")
print(r1.stdout)

# Variables persist between calls
r2 = manager.execute_code(sid, "df.shape")
print(r2.result)

# Export files from the sandbox (registers for download, no host copy)
manager.execute_code(sid, "df.to_csv('/workspace/output.csv', index=False)")
export = manager.export_files(sid, [{"source": "output.csv"}])
print(export.files[0].session_id, export.files[0].path)

manager.stop_session(sid)

导出文件

export_files 注册文件以供下载和跨会话导入(无主机副本)。文件可通过API获得(GET /threads/{thread_id}/files/download?session_id=...&path=...)以及 import_files 在其他会议中:

# Export a single file
result = manager.export_files(sid, [{"source": "report.pdf"}])

# Export an entire directory
result = manager.export_files(sid, [{"source": "results/"}])

# Export multiple files at once
result = manager.export_files(sid, [
    {"source": "data.csv"},
    {"source": "chart.png"},
    {"source": "/workspace/logs/"},
])

for f in result.files:
    print(f"{f.session_id}:{f.path} ({'OK' if f.success else f.error})")

跨会话文件传输

使用 export_files + import_files 在会话之间移动文件(甚至在不同的运行时之间):

# Session A (Python): produce data
sid_a = manager.create_session(runtime="python", dependencies={"pandas": ""}).session_id
manager.execute_code(sid_a, """
import pandas as pd
df = pd.DataFrame({'x': [1,2,3], 'y': [4,5,6]})
df.to_csv('/workspace/data.csv', index=False)
""")
export = manager.export_files(sid_a, [{"source": "data.csv"}])
path = export.files[0].path  # /workspace/data.csv

# Session B (R): consume the same data
sid_b = manager.create_session(runtime="r", dependencies={"readr": ""}).session_id
manager.import_files(sid_b, [{"session_id": sid_a, "path": path, "destination": "data.csv"}])
manager.execute_code(sid_b, 'df <- readr::read_csv("/workspace/data.csv"); summary(df)')

导入文件

import_files 将文件从主机或其他会话复制到沙盒中:

# Import from host
result = manager.import_files(sid, [
    {"source": "/home/user/data.csv", "destination": "data.csv"},
    {"source": "/home/user/project/", "destination": "project/"},
])

# Import from another session (file must have been exported first)
result = manager.import_files(sid, [
    {"session_id": "abc123", "path": "/workspace/out.csv", "destination": "out.csv"},
])

其他运行时也以同样的方式工作——pass runtime="node" 或 runtime="r" 到 create_session.

异步代码

Node.js --如果最后一个表达式返回Promise,内核将在收集输出之前等待它。顶层 await 还支持(需要时回退到异步IIFE包装器)。

const axios = require('axios');
async function fetchData() {
    const resp = await axios.get('https://api.example.com/data');
    console.log(resp.data);
}
fetchData(); // Promise is awaited automatically

python --IPython autoawait 处理顶级 await如果一个单元返回一个未等待的协程,内核会检测到它并用 asyncio.run().

import aiohttp

async def fetch_data():
    async with aiohttp.ClientSession() as session:
        resp = await session.get('https://api.example.com/data')
        print(await resp.text())

fetch_data()  # coroutine is detected and executed automatically

容器安全

每个容器都创建了以下保护:

保护 设置 效果
内存限制 2048m (无交换) 溢出时OOM终止,主机不受影响
PID限制 512 叉式炸弹被控制并杀死
CPU配额 2 核心 防止主机CPU不足
可写目录 tmpfs(/workspace, /tmp, /home/sandbox) tmpfs-dirs从不接触主机磁盘
tmpfs大小 200m 每次装载 容器磁盘使用限制
用户 sandbox (UID 65532) 容器内没有根
特权 no-new-privileges 无法通过setuid/setgid升级
网络 可配置(默认启用) 每个会话都可以禁用

当容器崩溃时,代理会收到清除 CONTAINER_DIED 错误原因(OOM终止、SIGKILL、segfault等)和重新创建会话的提示。

会话生命周期

会话通过垃圾回收自动管理:

行为 默认 设置
空闲超时 30分钟 SESSION_IDLE_TTL_SECONDS
最大寿命 2小时 SESSION_MAX_LIFETIME_SECONDS
GC间隔 60秒 SESSION_GC_INTERVAL_SECONDS
最大活动线程数 10 SESSION_MAX_ACTIVE_THREADS
最大会话数(全局) 5 CONTAINER_MAX_SESSIONS
每个线程的最大会话数 3 CONTAINER_MAX_SESSIONS_PER_THREAD
孤儿清理年龄 5分钟 CONTAINER_ORPHAN_MIN_AGE_SECONDS

在启动时,管理器会删除超过最低年龄的孤立容器。出口时,所有容器都会被停止并通过以下方式移除 atexit 和信号处理器(SIGTERM/SIGINT).

配置

所有设置都可以通过环境变量或 .env默认值显示如下 settings.py:

# ── LLM (provider-agnostic) ──
CHAT_MODEL=gpt-4o                    # Model name
CHAT_MODEL_PROVIDER=openai           # Provider: openai, anthropic, google_genai, ollama
CHAT_MODEL_API_KEY=sk-...            # API key (required)
CHAT_MODEL_BASE_URL=                 # Custom API base URL (optional)
CHAT_MODEL_SUPPORTS_VISION=          # Override vision detection (true/false, empty = auto)

# ── Container Limits ──
CONTAINER_MEMORY_LIMIT=2048m         # Docker memory limit (no swap)
CONTAINER_CPU_QUOTA=200000           # CPU quota (100000 = 1 core)
CONTAINER_PIDS_LIMIT=512             # Max PIDs per container
CONTAINER_TMPFS_SIZE=200m            # tmpfs size for writable dirs
CONTAINER_EXECUTION_TIMEOUT_SECONDS=30  # Default code execution timeout
CONTAINER_MAX_SESSIONS=5             # Max concurrent sessions (global)
CONTAINER_MAX_SESSIONS_PER_THREAD=3  # Max sessions per conversation
CONTAINER_EXECUTE_AS_ROOT=False      # Run terminal commands as root
CONTAINER_NETWORK_ENABLED=True       # Enable container networking (disable per session)
CONTAINER_ORPHAN_MIN_AGE_SECONDS=300 # Min age before orphan cleanup (5 min)

# ── Session Lifecycle / GC ──
SESSION_IDLE_TTL_SECONDS=1800        # Idle timeout (30 min)
SESSION_MAX_LIFETIME_SECONDS=7200    # Hard lifetime cap (2 hours)
SESSION_GC_INTERVAL_SECONDS=60       # GC check interval
SESSION_MAX_ACTIVE_THREADS=10        # Max active threads before eviction

# ── Output Truncation (characters) ──
MAX_STDOUT_CHARS=50000
MAX_STDERR_CHARS=120000
MAX_RESULT_CHARS=30000
MAX_TRACEBACK_CHARS=8000

# ── Encryption ──
ENCRYPTION_KEY=                      # Fernet key for settings encryption (optional)

# ── Storage ──
STORAGE_DIR=./storage                # Base dir for uploads
IMPORT_ALLOWED_DIRS=                 # Comma-separated host dirs allowed for import (empty = all)

# ── API ──
API_BASE_URL=http://127.0.0.1:8000   # API URL (for export download URLs)

# ── Agent ──
MAX_ITERATIONS=25                    # Max LangGraph iterations (recursion limit)

# ── PostgreSQL (checkpointer + Aegra) — all required, no defaults ──
POSTGRES_USER=sandbox_agent
POSTGRES_PASSWORD=sandbox_agent_secret
POSTGRES_DB=sandbox_agent
POSTGRES_HOST=localhost
POSTGRES_PORT=5432

运行时

运行时 基础映像 内核 IPC 预安装
python python:3.12-slim IPython shell UNIX套接字 IPython+系统库
Node.js node:22-slim vm.createContext UNIX套接字 裸运行时
R rocker/r-ver:4 专用R环境 TCP :8765 jsonline、base64enc、tidyverse、data.table、readxl、haven、httr2、DBI、RSQLite、rmarkdown、kniter、devtools、glmnet、randomForest

R容器使用编译的C客户端二进制文件进行IPC,而Python和Node.js使用本机客户端。

建筑

flowchart TB
    CLI["CLI · Rich REPL"]
    MCP["MCP Server · FastMCP (stdio)"]
    UI["React · Frontend"]

    CLI --> API["Aegra REST API
    (LangGraph Platform)"]
    UI --> API
    API --> Agent["LangGraph ReAct Agent"]
    Agent --> Tools["LangChain Tools"]
    MCP --> Core["Core Tool Functions"]

    Tools --> Core
    Core --> SM["SandboxManager
    Docker SDK"]

    SM -->|"docker exec -i + JSON pipe"| Docker

    subgraph Docker ["Docker Containers
    isolated, hardened"]
        direction LR
        PY["Python
        IPython · UNIX socket"]
        JS["Node.js
        vm.createContext · UNIX socket"]
        R["R
        R env · TCP :8765"]
    end

    subgraph Storage ["Persistence"]
        PG["PostgreSQL
        checkpoints, exports"]
    end

    API --> PG
    SM --> PG

在每个容器内,都有一个持久的 内核 (PID 1)保持执行状态 客户端 通过UNIX套接字(Python/Node.js)或TCP(R)连接到它 docker exec 呼叫:

flowchart TB
    SM["SandboxManager"] -->|"docker exec -i"| Client["Client (ephemeral)"]

    subgraph container ["Container"]
        Client -->|"UNIX socket / TCP"| Kernel["Kernel (PID 1, persistent)"]
        Kernel --- State["State
        variables, imports, data"]
    end

测试

# Unit tests (no Docker required)
uv run pytest tests/test_cli.py tests/test_http_app.py -v

# Integration tests (requires Docker)
uv run pytest tests/test_manager.py tests/test_tools.py tests/test_export_files.py tests/test_mcp.py -v

# LangGraph debug trace (requires Docker + LLM API key)
uv run pytest tests/test_langgraph_debug.py -v -s

# API integration tests (requires Docker + running API: uv run sandbox-agent api dev)
uv run pytest tests/test_api.py -v -s

# Full suite
uv run pytest tests/ -v

生产部署

A制作 Dockerfile 和 docker-compose.yml 包括:

# Start PostgreSQL + API
docker compose up -d

# Or build and run manually
docker build -t sandbox-agent-api .
docker run -p 8000:8000 --env-file .env sandbox-agent-api

生产图像使用 aegra serve 具有非根 app 用户。

许可证

麻省理工学院 --爱德华多·拉蒙·雷瑟