电子表格沙盒任务环境

一个安全的隔离环境,AI代理可以在LibreOffice中执行电子表格任务,并通过MCP协议进行交互,支持自动性能评估。

  • by arnavgarg233
  • Python
  • 本地部署
  • 更新于
在 GitHub 查看 arnavgarg233/rl-sandbox-mcp

使用说明

基于MCP的沙盒任务环境

一个安全、隔离的RL环境,AI代理可以在LibreOffice中执行电子表格任务,并自动评估性能。代理通过 模型上下文协议(MCP) API,支持基于GUI(计算机使用)和编程(odfpy)交互模式。

视频演示: 织机-MCP沙盒演示

演示和评分验证

演示仪表板(scripts/demo_ui.py)运行这两个代理,并在每次MCP工具调用时实时显示电子表格更新。

pip install streamlit openai
docker compose up -d
streamlit run scripts/demo_ui.py

脚本代理(scripts/demo_agent.py)

一个按顺序调用MCP工具的确定性Python代理: list_files → get_sheet_info → read_range → 计算插值储量→ write_cell + write_range → task/complete。每次得分100%。

LLM代理(scripts/llm_agent.py)

一个GPT-4o代理,可以自主发现并使用MCP工具来解决相同的任务。GPT-4o接收所有17个可用工具的列表,并决定调用哪个工具。

我们如何知道评分是有效的——GPT-4o的迭代故事:

当GPT-4o被赋予完全自主权时,它试图通过以下方式编写原始odfpy代码 execute_python 和一贯的失败-odfpy的API是不明显的,模型不断产生有缺陷的细胞操作代码(错误的属性名称,错误的类型转换)。评分员抓住了每一个错误:GPT-4o的首次尝试得分 67%(214/315个细胞),评分报告显示了哪些细胞是错误的,以及为什么。

在观察到这些故障后,我们指导GPT-4o使用 execute_python 与熊猫(pd.read_excel / df.to_excel 使用odf引擎),它对此非常熟悉。这使比分达到 100%(315/315个细胞).

这表明了两件事:

  1. 分级系统有效 --它捕获不正确的输出并报告精确的单元格级别不匹配
  2. 沙箱很灵活 --代理可以直接使用MCP工具,运行任意Python,或结合这两种方法

最终LLM代理流程:

  1. list_files → get_sheet_info → read_range (通过MCP发现和读取数据)
  2. write_cell (通过MCP设置标题)
  3. execute_python 使用pandas脚本(计算插值+写入结果)
  4. TASK_COMPLETE → 平地机运行→ PASS

架构概述

┌─────────────────────────────────────────────────────────┐
│                    Host Machine                         │
│                                                         │
│  ┌──────────────┐    ┌──────────────────────────────┐   │
│  │   sandbox_    │    │     Docker Container         │   │
│  │   manager.py  │───▶│  ┌────────────────────────┐  │   │
│  │              │    │  │     MCP Server (:8080)  │  │   │
│  │  • build     │    │  │                        │  │   │
│  │  • run       │    │  │  17 Tools Exposed:     │  │   │
│  │  • grade     │    │  │  • 9 Programmatic      │  │   │
│  │  • destroy   │    │  │  • 8 Computer Use      │  │   │
│  └──────────────┘    │  └────────┬───────────────┘  │   │
│                      │           │                   │   │
│  ┌──────────────┐    │  ┌────────▼───────────────┐  │   │
│  │  shared/     │◀──▶│  │  /workspace            │  │   │
│  │  (volume     │    │  │  • cash_flows.ods      │  │   │
│  │   mount)     │    │  │  • (agent output)      │  │   │
│  └──────────────┘    │  └────────────────────────┘  │   │
│                      │                               │   │
│  ┌──────────────┐    │  ┌────────────────────────┐  │   │
│  │  grader.py   │    │  │  LibreOffice Calc      │  │   │
│  │  • compare   │    │  │  Xvfb (virtual display)│  │   │
│  │  • score     │    │  │  Python + pandas/odfpy │  │   │
│  └──────────────┘    │  └────────────────────────┘  │   │
│                      │                               │   │
│                      │  🔒 Network-restricted:        │   │
│                      │  │  iptables blocks outbound   │   │
│                      │  │  DNS disabled, caps dropped  │   │
│                      │  🔒 2GB RAM / 2 CPU / 256 PIDs│   │
│                      └──────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘

沙盒生命周期:

  1. 从镜像构建容器(Ubuntu 22.04+LibreOffice+Python)
  2. 将特定任务的起始文件复制到共享工作区
  3. 代理通过MCP连接并处理任务
  4. 代理发出“完成”信号时提取输出文件
  5. 运行分级脚本,将输出与oracle进行比较
  6. 销毁容器(下次尝试时彻底销毁)

先决条件

  • Docker 桌面版 (macOS/Windows)或Docker引擎(Linux)
  • Python 3.10+ (用于在本地运行沙盒管理器、分级和测试)
pip install -e ".[dev]"

这将安装所有运行时依赖项(odfpy, openpyxl, pydantic, starlette, uvicorn, mcp)以及开发依赖关系(pytest, httpx).由于测试套件导入,因此需要MCP SDK mcp_server.py,使用 mcp.server.Server 和 mcp.server.sse.SseServerTransport.

快速开始

1.生成示例任务文件

python3 tasks/banking_reserve/generate_task_files.py

这将创建:

  • tasks/banking_reserve/starting_files/cash_flows.ods --带有稀疏速率点的100行电子表格
  • tasks/banking_reserve/oracle/expected_reserves.ods --正确答案加上插值储备

2.构建Docker镜像

python3 scripts/sandbox_manager.py build

3.端到端运行任务

python3 scripts/sandbox_manager.py run --task banking_reserve

这将:

  • 将起始文件复制到 shared/
  • 启动具有网络限制(iptables+DNS阻止)的容器
  • 等待代理完成(或300秒后超时)
  • 提取输出文件
  • 运行评分脚本
  • 打印分数
  • 销毁容器

4.或者使用docker compose手动运行

docker compose up -d          # start the container
curl http://localhost:8080/health   # verify it's running
curl http://localhost:8080/tools    # see all 17 MCP tools
docker compose down           # stop and remove

MCP协议实现

服务器建立在官方 MCP Python SDK (mcp>=1.0.0).核心MCP实现使用 mcp.server.Server 用于工具/资源注册和 mcp.server.sse.SseServerTransport 用于符合标准的SSE传输——与Claude Desktop、Cursor和其他MCP客户端本机使用的JSON-RPC协议相同。

工具通过注册 @mcp_sdk_server.list_tools() 和 @mcp_sdk_server.call_tool() 装饰师和资源通过 @mcp_sdk_server.list_resources()。两种传输方式共享相同的内容 TOOL_HANDLERS 调度表,因此无论客户端如何连接,行为都是相同的。

传输1:MCP SDK(SSE)——符合标准

任何MCP客户端都可以直接连接:

端点 方法 描述
/mcp/sse GET SSE连接端点(MCP JSON-RPC协议)
/mcp/messages POST 消息端点(MCP JSON-RPC协议)

传输2:REST/HTTP-轻量级JSON API

对于脚本, curl,以及简单的代理集成:

端点 方法 描述
/health GET 健康检查
/tools GET 列出所有可用的MCP工具
/tools/call POST 执行MCP工具
/resources GET 将工作区文件列为MCP资源
/task/start POST 开始超时任务会话
/task/status GET 检查当前任务状态
/task/complete POST 将任务标记为已完成

两种传输共享相同的工具处理程序和工作区状态。

MCP工具(共17个)

模式2:程序化(odfpy/openpyxl)——9个工具

工具 说明
read_cell 从电子表格中的特定单元格读取值
write_cell 将值写入电子表格中的特定单元格
read_range 读取矩形范围的单元格
write_range 从单元格开始写一个值块
set_formula 在特定单元格中设置公式(例如。 =SUM(A1:A10))
list_sheets 列出电子表格文件中的所有工作表
get_sheet_info 获取工作表的元数据(行/列计数、标题)
list_files 列出工作区目录中的所有文件
execute_python 在沙盒中执行任意Python代码

模式1:计算机使用(GUI)——8个工具

工具 说明
take_screenshot 将当前屏幕状态捕获为base64编码的PNG(1280x720)
click 点击特定屏幕坐标
double_click 双击特定屏幕坐标
type_text 使用键盘输入键入文本
key_press 按下一个键或组合(例如。 Return, ctrl+s)
mouse_move 将鼠标光标移动到特定坐标
drag 单击并从一个位置拖动到另一个位置
get_cursor_position 获取当前鼠标光标位置

示例:调用MCP工具

# Read a cell
curl -X POST http://localhost:8080/tools/call \
  -H "Content-Type: application/json" \
  -d '{"tool_name":"read_cell","arguments":{"file_path":"cash_flows.ods","sheet_name":"Sheet1","cell_reference":"A1"}}'

# Response:
# {"success":true,"result":{"cell_reference":"A1","value":"Period","value_type":"string"},"error":null}

# Take a screenshot (Mode 1)
curl -X POST http://localhost:8080/tools/call \
  -H "Content-Type: application/json" \
  -d '{"tool_name":"take_screenshot","arguments":{}}'

# Response:
# {"success":true,"result":{"image_base64":"iVBOR...","width":1280,"height":720,"timestamp":1773001940.1},"error":null}

任务结构

每个任务都是一个目录 tasks/ 包含:

tasks/<task_name>/
├── task.json              # Problem statement, instructions, grading config
├── generate_task_files.py # Script to generate starting + oracle files
├── grade.py               # Task-specific grading script
├── starting_files/        # Files the agent receives
│   └── cash_flows.ods
└── oracle/                # Expected correct output
    └── expected_reserves.ods

评分

评分员将代理输出与oracle电子表格进行比较:

  • 具有可配置容差的逐单元格数字比较(默认值:0.01)
  • 非数字单元格的字符串匹配
  • 处理丢失的纸张和空单元格
  • 返回0.0到1.0的分数,并包含详细的不匹配报告

预期演示输出

构建

$ python3 scripts/sandbox_manager.py build
[BUILD] Building sandbox Docker image...
  → docker compose build
 Image mcp-sandbox Built
[BUILD] Done.

全生命周期自动检查

$ python3 scripts/sandbox_manager.py run --task banking_reserve --demo
============================================================
TASK: Banking Reserve Calculation
============================================================
  Copied .../starting_files/cash_flows.ods → .../shared/cash_flows.ods
[START] MCP server is up: {'status': 'ok', 'workspace': '/workspace'}

[DEMO] Running automated MCP tool checks...
  [PASS] Health endpoint responds
  [PASS] Tools endpoint returns 17 tools
  [PASS] Mode 2: read_cell tool available
  [PASS] Mode 2: write_cell tool available
  [PASS] Mode 2: set_formula tool available
  [PASS] Mode 1: take_screenshot tool available
  [PASS] Mode 1: click tool available
  [PASS] Mode 1: type_text tool available
  [PASS] Workspace has files: ['cash_flows.ods']
  [PASS] read_cell A1 = 'Period'
  [PASS] read_range returns 3 rows
  [PASS] Sheet has 101 rows (expected 101)
  [PASS] write_cell D1 header
  [PASS] take_screenshot (1280px wide, 159808 chars)
  [PASS] click(400, 300)
  [PASS] task/start
  [PASS] task/status shows running
[DEMO] Results: 17/17 checks passed

GRADING RESULT: PASS
Score: 100.00%
Matched: 404/404 cells

[CLEAN] Removing sandbox container...

演示代理通过MCP解决任务

$ docker compose up -d && sleep 5 && python3 scripts/demo_agent.py
=== Demo Agent: Banking Reserve Calculation ===
[1] Starting task session...
[2] Listing workspace files...
    Found: cash_flows.ods
[3] Reading sheet info...
    101 rows, 3 cols, Headers: ['Period', 'Cash_Flow_Amount', 'Rate_Point']
[5] Analyzing data...
    100 periods, 11 known rate points
[8] Writing 100 reserve values...
[10] Signaling task completion...
=== Agent finished! ===

# Grade the agent's output:
GRADING RESULT: PASS
Score: 100.00%
Matched: 404/404 cells

错误处理

MCP服务器对所有故障情况返回结构化错误响应:

{"success": false, "result": null, "error": "File not found: /workspace/nonexistent.ods"}
{"success": false, "result": null, "error": "Sheet 'BadSheet' not found"}
{"success": false, "result": null, "error": "Invalid cell reference: ZZZ"}
{"success": false, "result": null, "error": "Task has timed out"}
{"success": false, "result": null, "error": "Unknown tool: fake_tool"}
{"success": false, "result": null, "error": "Code execution timed out after 30 seconds"}

所有工具调用在执行前都会通过Pydantic模型进行验证。格式错误的请求返回HTTP 400。未知工具返回HTTP 404。超时任务返回HTTP 408。内部错误(文件路径错误、工作表名称错误、单元格引用错误)返回HTTP 500并显示描述性错误消息。

沙盒安全

容器通过多层进行网络限制和权限限制。它控制了对两个已挂载目录的访问(shared/ 用于工作区文件和 mcp_server/ 对于服务器代码)——这允许文件交换,同时保持主机文件系统的其余部分不可访问。

层 实现 工作原理
防火墙(iptables) entrypoint.sh 设置 OUTPUT 在删除特权之前,将链规则作为根规则 阻止所有出站连接;仅回复入站请求(ESTABLISHED/RELATED)是允许的。MCP服务器可以响应API调用,但沙箱中的任何进程都不能启动与互联网的连接。
DNS阻止 --dns 0.0.0.0 在 docker-compose.yml 和 sandbox_manager.py 深度防御:即使绕过iptables,主机名解析也会失败。
能力下降 cap_drop: ALL +选择性 cap_add: NET_ADMIN, SETUID, SETGID 删除所有Linux功能。三个附加的背部被使用 仅在容器启动期间: NET_ADMIN 对于iptables规则, SETUID/SETGID 为了 su 从根到根 sandbox 用户。一旦入口点完成其特权设置并执行 sandbox,这些功能不再可由无特权用户执行。
特权升级预防 security_opt: no-new-privileges:true 防止任何进程通过以下方式获得新权限 execve() (例如suid二进制文件)。一旦入口点下降到 sandbox,没有回到根的路。
非根执行 入口点从iptables的根开始,然后 exec su sandbox 对于所有服务 MCP服务器、LibreOffice和Xvfb都以无特权模式运行 sandbox 用户。
资源限制 mem_limit: 2g, cpus: 2.0, pids_limit: 256 防止资源枯竭和分叉炸弹。
仅本地主机端口 127.0.0.1:8080:8080 MCP服务器只能从主机访问,不能从网络访问
期限 可配置超时,到期后自动拒绝工具 mcp_server.py (_check_timeout())
干净的石板 每次任务尝试后,容器都会被销毁 sandbox_manager.py (clean_container())
代码执行超时 execute_python 30秒后死亡 mcp_server.py (subprocess.run(timeout=30))

为什么不 --network none?

真正的网络禁用(network_mode: none)这将是最强大的隔离,但它也会阻止MCP服务器通过HTTP访问——主机需要一个TCP路径 localhost:8080 以便代理调用工具。另一种选择是基于文件的IPC(代理将请求写入共享卷,服务器轮询并写入响应),它以网络攻击面换取架构复杂性。这里使用的iptables+DNS+能力下降方法是一种务实的中间立场:容器可以 接收 连接,但不能 发起 any,这是对沙盒任务运行器很重要的威胁模型。

项目结构

rl-sandbox-mcp/
├── README.md
├── pyproject.toml
├── docker-compose.yml
├── sandbox/
│   ├── Dockerfile
│   ├── requirements.txt
│   └── entrypoint.sh
├── mcp_server/
│   ├── models.py               # Pydantic models (strongly typed)
│   ├── spreadsheet_engine.py   # ODS + XLSX read/write engines
│   ├── mcp_server.py           # HTTP MCP server (17 tools)
│   ├── computer_use.py         # GUI interaction tools
│   └── grader.py               # Automated grading engine
├── tasks/
│   ├── banking_reserve/        # Medium: interpolation + finance
│   │   ├── task.json
│   │   ├── generate_task_files.py
│   │   ├── grade.py
│   │   ├── starting_files/
│   │   └── oracle/
│   └── sales_revenue/          # Easy: arithmetic (Qty × Price)
│       ├── task.json
│       ├── generate_task_files.py
│       ├── grade.py
│       ├── starting_files/
│       └── oracle/
├── scripts/
│   ├── sandbox_manager.py      # Full lifecycle automation
│   ├── demo_agent.py           # Scripted agent that solves a task via MCP
│   ├── llm_agent.py            # GPT-4o agent (autonomous MCP tool use)
│   ├── demo_ui.py              # Streamlit visual dashboard for demos
│   └── run_demo.sh             # Shell script for end-to-end demo
├── tests/
│   └── test_suite.py           # Unit + integration tests
└── shared/                     # Volume mount (agent workspace)