From 5c16b968a0fc2424a8b94f12cb5ac14e51d61fea Mon Sep 17 00:00:00 2001 From: xyaohubery Date: Sun, 5 Jul 2026 15:00:18 +0800 Subject: [PATCH 1/8] feat(integration): add Claude Code MCP Server and integration guide - Add MCP Server exposing 9 CubeSandbox tools via stdio - Input validation with bounds checking on all numeric/temporal params - Defensive error handling throughout tool handler - Bilingual README (EN/ZH) with setup flow and troubleshooting - Integration guide (EN/ZH) following docs template conventions - Document WSL2 incompatibility (Cubelet + network-agent) per v0.5.0 testing Assisted-by: claude-code:deepseek-v4-pro --- .gitignore | 4 + docs/guide/integrations/claude-code.md | 338 +++++++++++ docs/guide/integrations/index.md | 2 + docs/zh/guide/integrations/claude-code.md | 318 +++++++++++ docs/zh/guide/integrations/index.md | 2 + examples/claude-code-sandbox/.env.example | 28 + examples/claude-code-sandbox/README.md | 277 +++++++++ examples/claude-code-sandbox/README_zh.md | 257 +++++++++ examples/claude-code-sandbox/mcp_server.py | 537 ++++++++++++++++++ examples/claude-code-sandbox/requirements.txt | 6 + 10 files changed, 1769 insertions(+) create mode 100644 docs/guide/integrations/claude-code.md create mode 100644 docs/zh/guide/integrations/claude-code.md create mode 100644 examples/claude-code-sandbox/.env.example create mode 100644 examples/claude-code-sandbox/README.md create mode 100644 examples/claude-code-sandbox/README_zh.md create mode 100644 examples/claude-code-sandbox/mcp_server.py create mode 100644 examples/claude-code-sandbox/requirements.txt diff --git a/.gitignore b/.gitignore index 6b32b5acf..6a6341c54 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,7 @@ __pycache__ /tests web/tsconfig.tsbuildinfo + +# WSL artifacts (do not commit) +WSLUbuntu/ +WSLubuntu.tar diff --git a/docs/guide/integrations/claude-code.md b/docs/guide/integrations/claude-code.md new file mode 100644 index 000000000..97505461d --- /dev/null +++ b/docs/guide/integrations/claude-code.md @@ -0,0 +1,338 @@ +--- +title: Claude Code Integration Guide +author: community +date: 2026-07-01 +tags: + - integration + - claude-code + - mcp +lang: en-US +--- + +# Claude Code Integration Guide + +Run Claude Code with code execution safely offloaded to CubeSandbox +MicroVMs via MCP (Model Context Protocol). This guide covers the +end-to-end setup: from template creation to production best practices. + +## Integration Target and Version + +| Component | Tested Version | +|-----------------|--------------------| +| Claude Code | ≥ 1.0.0 | +| CubeSandbox | v0.4.0 | +| `cubesandbox` SDK | ≥ 0.3.0 | +| `mcp` (Python) | ≥ 1.0.0 | + +## Overview + +Claude Code is Anthropic's interactive terminal coding agent. By default, +Claude Code's `Bash` tool executes commands directly on the host machine. +This integration adds a set of MCP tools that route code execution to +CubeSandbox — a hardware-isolated MicroVM platform — so that: + +- AI-generated code runs in an isolated VM, not on your host. +- Malicious or buggy scripts can't affect your system. +- Each coding session can have its own disposable environment. +- Long-running tasks can be snapshot and resumed across sessions. + +This integration implements the **"Sandbox as Tool"** pattern: Claude Code +runs locally (handling conversation and orchestration), but delegates code +execution to CubeSandbox on demand. + +## Prerequisites + +- **A Linux x86_64 host** with KVM enabled. CubeSandbox requires bare-metal + Linux or a cloud VM with nested virtualization. **WSL2 is not supported** + (CubeSandbox v0.5.0 components including network-agent and cubelet crash on + the Microsoft WSL2 kernel due to incompatible kernel interfaces). +- Cube Sandbox deployed and CubeAPI reachable (see + [Quick Start](../quickstart.md)). +- A sandbox template with Python and development toolchain preinstalled. + The `sandbox-code` image is recommended as a starting point. +- Claude Code installed on your local machine. +- Python 3.10+ on your local machine (for the MCP server process). + +## Integration Steps + +### Step 1 — Create a Sandbox Template + +```bash +cubemastercli tpl create-from-image \ + --image cube-sandbox-int.tencentcloudcr.com/cube-sandbox/sandbox-code:latest \ + --writable-layer-size 2G \ + --expose-port 49999 \ + --expose-port 49983 \ + --probe 49999 +``` + +Wait for the build to finish (check with `cubemastercli tpl watch --job-id `). +Note the `template_id` from the output. + +> **Registry note:** Use `cube-sandbox-cn.tencentcloudcr.com` for mainland +> China access or `cube-sandbox-int.tencentcloudcr.com` for international. + +**Template requirements for Claude Code integration:** + +| Requirement | Why | +|-----------------------|--------------------------------------------------| +| Port 49999 exposed | Jupyter kernel gateway for `run_code` | +| Port 49983 exposed | envd endpoint for `run_command` and file ops | +| Python 3.10+ | `run_code` backend | +| Common CLI tools | `run_command` (git, curl, make, gcc, etc.) | +| ≥ 2GB writable layer | Room for `pip install` / `npm install` packages | + +For custom needs, build your own Docker image and create a template from it: + +```dockerfile +FROM cube-sandbox-int.tencentcloudcr.com/cube-sandbox/sandbox-code:latest +RUN pip install torch numpy pandas matplotlib scikit-learn +RUN apt-get update && apt-get install -y ffmpeg +``` + +### Step 2 — Install the MCP Server Dependencies + +```bash +git clone https://github.com/tencentcloud/CubeSandbox.git +cd CubeSandbox/examples/claude-code-sandbox +pip install -r requirements.txt +cp .env.example .env +# Edit .env with your CubeSandbox connection details +``` + +### Step 3 — Configure Claude Code + +Add the MCP server entry to your Claude Code settings: + +**Project-level** (`.claude/settings.json` in your project root): + +```json +{ + "mcpServers": { + "cube-sandbox": { + "command": "python", + "args": ["/absolute/path/to/CubeSandbox/examples/claude-code-sandbox/mcp_server.py"], + "env": { + "CUBE_TEMPLATE_ID": "", + "E2B_API_URL": "http://:3000", + "E2B_API_KEY": "e2b_000000" + } + } + } +} +``` + +**User-level** (`~/.claude/settings.json`) — applies to all projects. + +### Step 4 — Verify the Integration + +Restart Claude Code and check that the sandbox tools are registered: + +``` +/claude mcp list +``` + +You should see `cube-sandbox` with 9 tools listed. Test with a simple +conversation: + +``` +> Create a sandbox, run `print(1 + 1)`, then destroy the sandbox. +``` + +Expected: Claude Code calls `sandbox_create` → `sandbox_run_code` → +`sandbox_destroy` and reports the result `2`. + +## Key Code Snippets + +### Minimal MCP Server + +The MCP server wraps the `cubesandbox` Python SDK. Core connection: + +```python +from cubesandbox import Sandbox +from mcp.server import Server +from mcp.server.stdio import stdio_server + +server = Server("cube-sandbox") + +@server.call_tool() +async def call_tool(name: str, arguments: dict): + if name == "sandbox_create": + sb = Sandbox.create( + template=os.environ["CUBE_TEMPLATE_ID"], + timeout=arguments.get("timeout", 600), + ) + return [TextContent(type="text", text=f"Created: {sb.sandbox_id}")] + # ... other tools + +async def main(): + async with stdio_server() as (read, write): + await server.run(read, write, server.create_initialization_options()) +``` + +See [`examples/claude-code-sandbox/mcp_server.py`](../../examples/claude-code-sandbox/mcp_server.py) +for the full implementation. + +### Custom Template with Extra Tools + +If your workflow needs additional packages or tools, create a custom +Docker image and template: + +```bash +# 1. Build a custom image +docker build -t my-sandbox:latest -f- . <<'EOF' +FROM cube-sandbox-int.tencentcloudcr.com/cube-sandbox/sandbox-code:latest +RUN pip install torch transformers +EOF + +# 2. Push to a registry accessible by CubeSandbox +docker tag my-sandbox:latest your-registry/my-sandbox:latest +docker push your-registry/my-sandbox:latest + +# 3. Create a template from the custom image +cubemastercli tpl create-from-image \ + --image your-registry/my-sandbox:latest \ + --writable-layer-size 5G \ + --expose-port 49999 \ + --expose-port 49983 \ + --probe 49999 +``` + +## Best Practices + +### Sandbox Lifecycle Management + +- **Create once per session**: Create one sandbox at the start of a + Claude Code conversation and reuse it across multiple `run_code` / + `run_command` calls. The Jupyter kernel preserves state across calls. +- **Timeout appropriately**: Set `timeout` to match your expected session + length. Default 600s (10 min) may be too short for complex tasks. +- **Always destroy**: Call `sandbox_destroy` at the end of a session. + Sandboxes consume node resources (CPU, memory, disk). + +### Snapshots for Long-Running Work + +For multi-session tasks (e.g., debugging that spans days): + +```text +Session 1: + sandbox_create → work → sandbox_snapshot(name="checkpoint-1") + sandbox_pause + +Session 2: + sandbox_create → (snapshot is available) sandbox_run_code → continue work +``` + +Snapshots survive sandbox destruction. You can create multiple checkpoints +and roll back to any of them. + +### Credential Security + +Do **not** hardcode API keys or secrets in code passed to `sandbox_run_code` +or `sandbox_write_file`. Use one of these approaches instead: + +1. **CubeEgress credential injection** (recommended): Configure the + sandbox's network policy to inject `Authorization` headers at the + egress proxy. Secrets are stored in CubeEgress configuration and + never enter the sandbox. See + [Security Proxy Guide](../security-proxy.md). + +2. **Environment variables**: Pass secrets via `env_vars` in + `Sandbox.create()`. These are set in the sandbox process environment + and are not visible in Claude Code's context. + +```python +# In mcp_server.py (or a custom variant): +sb = Sandbox.create( + template=template, + env_vars={"GITHUB_TOKEN": os.environ["GITHUB_TOKEN"]}, +) +``` + +### Network Policy + +By default, sandboxes have unrestricted internet access. For stricter +control, configure per-sandbox network policies: + +```python +sb = Sandbox.create( + template=template, + allow_internet_access=False, # Block all by default + network={ + "allow_out": [ + "pypi.org", + "files.pythonhosted.org", + ], + "deny_out": [ + "169.254.0.0/16", + "10.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16", + ], + }, +) +``` + +See [Network Policy Guide](../network-policy.md) for details. + +## Caveats + +### Local File Access + +Unlike Claude Code's native `Bash` tool, the sandbox tools **cannot** +directly access files on your host machine. Use `sandbox_write_file` to +upload files and `sandbox_read_file` to download results. For large +datasets, use `sandbox_run_command("git clone ...")` or configure a +host mount (see [Host Mount Example](../../examples/host-mount/README.md)). + +### Tool Discovery + +Claude Code may not always choose the sandbox tools over its built-in +`Bash` tool when both are available. To encourage sandbox usage: + +1. Use a CLAUDE.md file in your project: + +```markdown +# CLAUDE.md + +When executing code, ALWAYS use the sandbox_run_code or sandbox_run_command +tools. Do NOT use the Bash tool for code execution. Use sandbox_write_file +to provide input and sandbox_read_file to retrieve output. +``` + +### Performance Overhead + +- **Cold start**: First sandbox creation from a template typically takes + 2–5 seconds. This includes template clone (CoW), VM boot, and service + startup. +- **Warm start**: Subsequent sandboxes from recently-used templates are + faster (< 1s) due to VM pooling. +- **Execution latency**: `run_code` adds ~50–200ms round-trip overhead vs + local execution, depending on network distance to the CubeSandbox node. + +### Compatibility + +- This MCP server uses the **Jupyter kernel mode** (`run_code`). It + requires a template with the e2b code-interpreter service (port 49999). + Plain envd-only templates will work for `run_command` but not `run_code`. +- The `cubesandbox` Python SDK is compatible with CubeSandbox v0.3.0+. +- Claude Code MCP support requires Claude Code v1.0.0+. + +## Troubleshooting + +| Symptom | Likely Cause | Fix | +|-------------------------------------------------|-------------------------------------|-----------------------------------------------------------------------------------------| +| MCP server fails to start | Missing dependencies | Run `pip install -r requirements.txt`; check Python 3.10+ | +| `sandbox_create` hangs | CubeAPI unreachable | Verify `E2B_API_URL`; `curl http://:3000/health` | +| `sandbox_create` returns 404 | Wrong template ID | Check `CUBE_TEMPLATE_ID`; run `cubemastercli tpl list` | +| `sandbox_run_code` returns 502 | Sandbox evicted (timeout/deleted) | Increase `timeout`; check that sandbox isn't being killed by idle timeout | +| `sandbox_run_code` returns "connection refused" | Jupyter gateway not ready | Template must expose port 49999; wait 2-3s after create before first `run_code` call | +| SSL certificate errors | Custom CA not trusted | Set `CUBE_SSL_CERT_FILE` env var; or pass `verify=False` for testing (not for production)| +| Claude Code doesn't show sandbox tools | MCP config error | Check `settings.json` syntax; run Claude Code with `--debug` to see MCP startup logs | + +## References + +- Example code: [`examples/claude-code-sandbox/`](../../examples/claude-code-sandbox/) +- CubeSandbox Python SDK: [`sdk/python/`](../../sdk/python/) +- Claude Code MCP documentation: [docs.anthropic.com](https://docs.anthropic.com/en/docs/claude-code/mcp) +- MCP specification: [modelcontextprotocol.io](https://modelcontextprotocol.io) diff --git a/docs/guide/integrations/index.md b/docs/guide/integrations/index.md index f40867fbc..f3161bdd6 100644 --- a/docs/guide/integrations/index.md +++ b/docs/guide/integrations/index.md @@ -48,3 +48,5 @@ lang: en-US | Title | Author | Date | Tags | | --- | --- | --- | --- | | [Pi Agent Integration Guide](./pi-agent.md) | chaojixinren | 2026-07-01 | integration, pi-agent, coding-agent, agent | +| [Claude Code Integration Guide](claude-code.md) | community | 2026-07-01 | integration, claude-code, mcp | +| _Add your article here_ | - | - | - | diff --git a/docs/zh/guide/integrations/claude-code.md b/docs/zh/guide/integrations/claude-code.md new file mode 100644 index 000000000..8785c1468 --- /dev/null +++ b/docs/zh/guide/integrations/claude-code.md @@ -0,0 +1,318 @@ +--- +title: Claude Code 集成指南 +author: community +date: 2026-07-01 +tags: + - integration + - claude-code + - mcp +lang: zh-CN +--- + +# Claude Code 集成指南 + +通过 MCP(Model Context Protocol)将 Claude Code 的代码执行安全地卸载到 +CubeSandbox MicroVM。本指南覆盖从模板创建到生产环境最佳实践的端到端流程。 + +## 集成对象与版本 + +| 组件 | 已测试版本 | +|---------------------|------------------| +| Claude Code | ≥ 1.0.0 | +| CubeSandbox | v0.4.0 | +| `cubesandbox` SDK | ≥ 0.3.0 | +| `mcp` (Python) | ≥ 1.0.0 | + +## 概述 + +Claude Code 是 Anthropic 的交互式终端编码 Agent。默认情况下,Claude Code 的 +`Bash` 工具直接在宿主机上执行命令。本集成通过添加一组 MCP 工具,将代码执行 +路由到 CubeSandbox——一个基于硬件隔离 MicroVM 的平台——从而实现: + +- AI 生成的代码在隔离 VM 中而非宿主机上运行。 +- 恶意或有 bug 的脚本无法影响你的系统。 +- 每个编码会话拥有独立的即用即毁环境。 +- 长任务可通过快照跨会话保存和恢复。 + +本集成采用 **"沙箱即工具"** 模式:Claude Code 在本地运行(处理对话和编排), +按需将代码执行委托给 CubeSandbox。 + +## 前提条件 + +- **一台 Linux x86_64 主机**,需启用 KVM。CubeSandbox 需要裸金属 Linux 或支持 + 嵌套虚拟化的云服务器。**WSL2 不受支持**(CubeSandbox v0.5.0 的 network-agent + 和 cubelet 组件在 Microsoft WSL2 内核上因内核接口不兼容而崩溃)。 +- Cube Sandbox 已部署且 CubeAPI 可访问(参见[快速开始](../quickstart.md))。 +- 已有预装 Python 和开发工具链的沙箱模板。推荐使用 `sandbox-code` 镜像作为起点。 +- 本地已安装 Claude Code。 +- 本地 Python 3.10+(用于运行 MCP Server 进程)。 + +## 集成步骤 + +### 第一步 — 创建沙箱模板 + +```bash +cubemastercli tpl create-from-image \ + --image cube-sandbox-int.tencentcloudcr.com/cube-sandbox/sandbox-code:latest \ + --writable-layer-size 2G \ + --expose-port 49999 \ + --expose-port 49983 \ + --probe 49999 +``` + +等待构建完成(用 `cubemastercli tpl watch --job-id ` 查看进度)。 +记下输出的 `template_id`。 + +> **镜像源说明**:国内用户使用 `cube-sandbox-cn.tencentcloudcr.com`, +> 国际用户使用 `cube-sandbox-int.tencentcloudcr.com`。 + +**Claude Code 集成所需的模板要求:** + +| 要求 | 原因 | +|----------------------|------------------------------------------| +| 暴露端口 49999 | Jupyter 内核网关,用于 `run_code` | +| 暴露端口 49983 | envd 端点,用于 `run_command` 和文件操作 | +| Python 3.10+ | `run_code` 后端 | +| 常用 CLI 工具 | `run_command`(git、curl、make、gcc 等) | +| ≥ 2GB 可写层 | 为 `pip install`/`npm install` 留空间 | + +按需构建自定义 Docker 镜像并创建模板: + +```dockerfile +FROM cube-sandbox-int.tencentcloudcr.com/cube-sandbox/sandbox-code:latest +RUN pip install torch numpy pandas matplotlib scikit-learn +RUN apt-get update && apt-get install -y ffmpeg +``` + +### 第二步 — 安装 MCP Server 依赖 + +```bash +git clone https://github.com/tencentcloud/CubeSandbox.git +cd CubeSandbox/examples/claude-code-sandbox +pip install -r requirements.txt +cp .env.example .env +# 编辑 .env 填入你的 CubeSandbox 连接信息 +``` + +### 第三步 — 配置 Claude Code + +在 Claude Code 设置中添加 MCP Server 条目: + +**项目级**(项目根目录下的 `.claude/settings.json`): + +```json +{ + "mcpServers": { + "cube-sandbox": { + "command": "python", + "args": ["/绝对路径/CubeSandbox/examples/claude-code-sandbox/mcp_server.py"], + "env": { + "CUBE_TEMPLATE_ID": "", + "E2B_API_URL": "http://:3000", + "E2B_API_KEY": "e2b_000000" + } + } + } +} +``` + +**用户级**(`~/.claude/settings.json`)— 对所有项目生效。 + +### 第四步 — 验证集成 + +重启 Claude Code,确认沙箱工具已注册: + +``` +/claude mcp list +``` + +应能看到 `cube-sandbox` 及 9 个工具。用简单对话测试: + +``` +> 创建一个沙箱,运行 `print(1 + 1)`,然后销毁沙箱。 +``` + +预期:Claude Code 依次调用 `sandbox_create` → `sandbox_run_code` → +`sandbox_destroy`,并报告结果 `2`。 + +## 关键代码片段 + +### 最小 MCP Server + +MCP Server 封装了 `cubesandbox` Python SDK。核心连接方式: + +```python +from cubesandbox import Sandbox +from mcp.server import Server +from mcp.server.stdio import stdio_server + +server = Server("cube-sandbox") + +@server.call_tool() +async def call_tool(name: str, arguments: dict): + if name == "sandbox_create": + sb = Sandbox.create( + template=os.environ["CUBE_TEMPLATE_ID"], + timeout=arguments.get("timeout", 600), + ) + return [TextContent(type="text", text=f"Created: {sb.sandbox_id}")] + # ... 其他工具 + +async def main(): + async with stdio_server() as (read, write): + await server.run(read, write, server.create_initialization_options()) +``` + +完整实现见 [`examples/claude-code-sandbox/mcp_server.py`](../../examples/claude-code-sandbox/mcp_server.py)。 + +### 自定义模板(含额外工具) + +如果你的工作流需要额外的软件包或工具,创建自定义 Docker 镜像和模板: + +```bash +# 1. 构建自定义镜像 +docker build -t my-sandbox:latest -f- . <<'EOF' +FROM cube-sandbox-int.tencentcloudcr.com/cube-sandbox/sandbox-code:latest +RUN pip install torch transformers +EOF + +# 2. 推送到 CubeSandbox 可访问的镜像仓库 +docker tag my-sandbox:latest your-registry/my-sandbox:latest +docker push your-registry/my-sandbox:latest + +# 3. 从自定义镜像创建模板 +cubemastercli tpl create-from-image \ + --image your-registry/my-sandbox:latest \ + --writable-layer-size 5G \ + --expose-port 49999 \ + --expose-port 49983 \ + --probe 49999 +``` + +## 最佳实践 + +### 沙箱生命周期管理 + +- **每个会话创建一个沙箱**:在 Claude Code 对话开始时创建一个沙箱,在多次 + `run_code`/`run_command` 调用中复用。Jupyter 内核在多次调用间保持状态。 +- **合理设置超时时间**:根据预期会话时长设置 `timeout`。默认 600 秒(10 分钟) + 对复杂任务可能不够。 +- **务必销毁**:会话结束时调用 `sandbox_destroy`。沙箱会消耗节点资源(CPU、 + 内存、磁盘)。 + +### 长任务的快照保存 + +对于跨会话的任务(如持续数天的调试): + +```text +会话 1: + sandbox_create → work → sandbox_snapshot(name="checkpoint-1") + sandbox_pause + +会话 2: + sandbox_create → (快照可用) sandbox_run_code → 继续工作 +``` + +快照独立于沙箱生命周期。你可以创建多个检查点并回滚到任意一个。 + +### 凭证安全 + +**不要**在传给 `sandbox_run_code` 或 `sandbox_write_file` 的代码中硬编码 API +密钥或其他机密信息。使用以下方式之一: + +1. **CubeEgress 凭证注入**(推荐):配置沙箱的网络策略,在出口代理处注入 + `Authorization` 头。机密信息存储在 CubeEgress 配置中,不会进入沙箱。 + 参见[安全代理指南](../security-proxy.md)。 + +2. **环境变量**:通过 `Sandbox.create()` 的 `env_vars` 传入机密信息。 + 这些变量设置在沙箱进程环境中,在 Claude Code 上下文中不可见。 + +```python +# 在 mcp_server.py(或自定义变体)中: +sb = Sandbox.create( + template=template, + env_vars={"GITHUB_TOKEN": os.environ["GITHUB_TOKEN"]}, +) +``` + +### 网络策略 + +默认情况下沙箱可以不受限访问公网。如需更严格的控制,按沙箱配置网络策略: + +```python +sb = Sandbox.create( + template=template, + allow_internet_access=False, # 默认拦截所有出口 + network={ + "allow_out": [ + "pypi.org", + "files.pythonhosted.org", + ], + "deny_out": [ + "169.254.0.0/16", # 链路本地 + "10.0.0.0/8", # 私有 A 类 + "172.16.0.0/12", # 私有 B 类 + "192.168.0.0/16", # 私有 C 类 + ], + }, +) +``` + +详见[网络策略指南](../network-policy.md)。 + +## 注意事项 + +### 本地文件访问 + +与 Claude Code 原生的 `Bash` 工具不同,沙箱工具**无法**直接访问宿主机上的文件。 +使用 `sandbox_write_file` 上传文件,`sandbox_read_file` 下载结果。对于大规模 +数据集,使用 `sandbox_run_command("git clone ...")` 或配置 host mount +(参见[Host Mount 示例](../../examples/host-mount/README.md))。 + +### 工具发现 + +当沙箱工具和内置 `Bash` 工具同时可用时,Claude Code 不一定总是选择沙箱工具。 +为引导 Claude Code 优先使用沙箱,在项目中创建 CLAUDE.md 文件: + +```markdown +# CLAUDE.md + +执行代码时,始终使用 sandbox_run_code 或 sandbox_run_command 工具。 +不要使用 Bash 工具执行代码。使用 sandbox_write_file 提供输入, +使用 sandbox_read_file 获取输出。 +``` + +### 性能开销 + +- **冷启动**:从模板创建首个沙箱通常需要 2–5 秒。包含模板 clone(CoW)、VM + 启动和服务就绪。 +- **热启动**:使用最近用过的模板创建沙箱更快(< 1 秒),得益于 VM 池化。 +- **执行延迟**:`run_code` 比本地执行增加约 50–200ms 往返延迟,取决于你与 + CubeSandbox 节点之间的网络距离。 + +### 兼容性 + +- 此 MCP Server 使用 **Jupyter 内核模式**(`run_code`)。需要模板中包含 e2b + code-interpreter 服务(端口 49999)。仅含 envd 的模板可用于 `run_command` + 但不能用于 `run_code`。 +- `cubesandbox` Python SDK 兼容 CubeSandbox v0.3.0+。 +- Claude Code MCP 支持需要 Claude Code v1.0.0+。 + +## 常见问题 + +| 现象 | 可能原因 | 解决方案 | +|---------------------------------------------------|----------------------|-----------------------------------------------------------------------------------------| +| MCP Server 无法启动 | 缺少依赖 | 执行 `pip install -r requirements.txt`;检查 Python 3.10+ | +| `sandbox_create` 挂起 | CubeAPI 不可达 | 检查 `E2B_API_URL`;`curl http://:3000/health` | +| `sandbox_create` 返回 404 | 模板 ID 错误 | 核对 `CUBE_TEMPLATE_ID`;执行 `cubemastercli tpl list` | +| `sandbox_run_code` 返回 502 | 沙箱被驱逐(超时/被删) | 增加 `timeout`;检查沙箱是否被空闲超时机制终止 | +| `sandbox_run_code` 返回 "connection refused" | Jupyter 网关未就绪 | 模板必须暴露端口 49999;首次 `run_code` 调用前等待 2–3 秒 | +| SSL 证书错误 | 自定义 CA 未被信任 | 设置 `CUBE_SSL_CERT_FILE` 环境变量;或测试环境传 `verify=False`(生产环境不要这样做) | +| Claude Code 不显示沙箱工具 | MCP 配置错误 | 检查 `settings.json` 语法;用 `--debug` 运行 Claude Code 查看 MCP 启动日志 | + +## 参考 + +- 示例代码:[`examples/claude-code-sandbox/`](../../examples/claude-code-sandbox/) +- CubeSandbox Python SDK:[`sdk/python/`](../../sdk/python/) +- Claude Code MCP 文档:[docs.anthropic.com](https://docs.anthropic.com/en/docs/claude-code/mcp) +- MCP 规范:[modelcontextprotocol.io](https://modelcontextprotocol.io) diff --git a/docs/zh/guide/integrations/index.md b/docs/zh/guide/integrations/index.md index c96579ce6..a87590ba5 100644 --- a/docs/zh/guide/integrations/index.md +++ b/docs/zh/guide/integrations/index.md @@ -48,3 +48,5 @@ lang: zh-CN | 标题 | 作者 | 日期 | 标签 | | --- | --- | --- | --- | | [Pi Agent 集成指南](./pi-agent.md) | chaojixinren | 2026-07-01 | integration, pi-agent, coding-agent, agent | +| [Claude Code 集成指南](claude-code.md) | community | 2026-07-01 | integration, claude-code, mcp | +| _在这里补充你的文章_ | - | - | - | diff --git a/examples/claude-code-sandbox/.env.example b/examples/claude-code-sandbox/.env.example new file mode 100644 index 000000000..185130699 --- /dev/null +++ b/examples/claude-code-sandbox/.env.example @@ -0,0 +1,28 @@ +# CubeSandbox MCP Server — Environment Variables +# Copy to .env and fill in real values: +# cp .env.example .env + +# -------------------------------------------------------------------- +# Required — CubeSandbox API connection +# -------------------------------------------------------------------- + +# CubeAPI endpoint (the E2B-compatible REST API, default port 3000). +E2B_API_URL=http://:3000 + +# API key. For local/testing deployments any non-empty placeholder works +# (e.g. "e2b_000000"). For authenticated deployments, use your real key. +E2B_API_KEY=e2b_000000 + +# Sandbox template ID. Create one with: +# cubemastercli tpl create-from-image \ +# --image cube-sandbox-int.tencentcloudcr.com/cube-sandbox/sandbox-code:latest \ +# --writable-layer-size 1G --expose-port 49999 --expose-port 49983 --probe 49999 +CUBE_TEMPLATE_ID= + +# -------------------------------------------------------------------- +# Optional — TLS / SSL +# -------------------------------------------------------------------- + +# Path to the CubeSandbox CA certificate bundle (for HTTPS connections). +# Only needed when CubeProxy uses TLS with a custom CA (e.g. mkcert). +# CUBE_SSL_CERT_FILE=/root/.local/share/mkcert/rootCA.pem diff --git a/examples/claude-code-sandbox/README.md b/examples/claude-code-sandbox/README.md new file mode 100644 index 000000000..59abf1acd --- /dev/null +++ b/examples/claude-code-sandbox/README.md @@ -0,0 +1,277 @@ +# Claude Code + CubeSandbox Integration + +[中文文档](README_zh.md) + +Give Claude Code a safe, isolated execution environment by routing code +execution through CubeSandbox MicroVMs via MCP (Model Context Protocol). + +## What This Is + +A **MCP Server** that exposes CubeSandbox sandbox operations as Claude Code +tools. When Claude Code needs to execute Python, run shell commands, or +manipulate files, it delegates the work to a hardware-isolated MicroVM +instead of running directly on your host. + +``` +Claude Code MCP (stdio) This Server CubeSandbox +────────── ────────── ─────────── ─────────── +"I'll analyze → sandbox_create → Sandbox.create() → KVM MicroVM + the data..." sandbox_run_code sandbox.run_code() (isolated) + sandbox_read_file sandbox.files.read() ↑ safe +``` + +> **Verification status**: This MCP Server has been reviewed against the +> CubeSandbox v0.4.0 / v0.5.0 Python SDK source and is believed to be API-correct, +> but has **not been tested end-to-end** against a running CubeSandbox deployment. +> Feedback, bug reports, and verification reports are welcome. + +## Why Use This + +| Without This | With This | +|--------------------------------------|----------------------------------------------| +| Claude Code's `bash` tool runs commands directly on your host | Code runs in hardware-isolated MicroVMs | +| `rm -rf /` destroys your machine | `rm -rf /` destroys only the sandbox VM | +| `pip install` pollutes your system | All packages installed in a disposable VM | +| No snapshot / rollback | Save sandbox state, clone, rollback anytime | +| Network access to everything | Per-sandbox network policy (allow/deny) | + +## Prerequisites + +- **CubeSandbox deployed**: A running CubeSandbox instance on a Linux x86_64 + host with KVM (bare metal or cloud VM). **WSL2 is not supported** — see + the [integration guide](../../docs/guide/integrations/claude-code.md) for details. + See [Quick Start](https://cube-sandbox.pages.dev/guide/quickstart) for deployment. +- **A sandbox template**: Prebuilt with Python and your required tools. The + standard `sandbox-code` image works for most use cases. +- **Claude Code** with MCP support (Claude Code v1.0.0+). +- **Python 3.10+** to run this MCP server. + +## Quick Start + +### 1. Install Dependencies + +```bash +pip install -r requirements.txt +``` + +### 2. Configure Environment + +```bash +cp .env.example .env +``` + +Edit `.env` and fill in your CubeSandbox connection details: + +| Variable | Description | +|----------------------|----------------------------------------------------------------| +| `E2B_API_URL` | CubeAPI endpoint, e.g. `http://:3000` | +| `E2B_API_KEY` | CubeAPI auth key (`e2b_000000` for local/testing deployments) | +| `CUBE_TEMPLATE_ID` | Template ID for your sandbox images | +| `CUBE_SSL_CERT_FILE` | (Optional) Path to CubeSandbox CA bundle for HTTPS | + +### 3. Create a Sandbox Template + +If you don't have a template yet: + +```bash +cubemastercli tpl create-from-image \ + --image cube-sandbox-int.tencentcloudcr.com/cube-sandbox/sandbox-code:latest \ + --writable-layer-size 1G \ + --expose-port 49999 \ + --expose-port 49983 \ + --probe 49999 +``` + +Copy the `template_id` from the output into your `.env`. + +### 4. Register the MCP Server with Claude Code + +Add this to your Claude Code settings: + +```json +{ + "mcpServers": { + "cube-sandbox": { + "command": "python", + "args": ["/path/to/cube-sandbox/examples/claude-code-sandbox/mcp_server.py"], + "env": { + "CUBE_TEMPLATE_ID": "", + "E2B_API_URL": "http://:3000", + "E2B_API_KEY": "e2b_000000" + } + } + } +} +``` + +Settings file location: +- Project-level: `.claude/settings.json` +- User-level: `~/.claude/settings.json` + +### 5. Use It + +Restart Claude Code (or reload MCP servers). Claude Code now has these tools: + +| Tool | What It Does | +|------------------------|----------------------------------------| +| `sandbox_create` | Create a new isolated sandbox | +| `sandbox_run_code` | Execute Python (Jupyter kernel) | +| `sandbox_run_command` | Run shell commands | +| `sandbox_read_file` | Read files from the sandbox | +| `sandbox_write_file` | Write files into the sandbox | +| `sandbox_get_info` | Inspect sandbox metadata | +| `sandbox_snapshot` | Save a point-in-time snapshot | +| `sandbox_pause` | Pause the sandbox (preserves state) | +| `sandbox_destroy` | Destroy the sandbox | + +Start a conversation with Claude Code and ask it to do something: + +``` +> Create a sandbox, then write a Python script that calculates the first + 100 Fibonacci numbers and saves them to fib.csv. Read the file back and + verify the results. +``` + +Claude Code will call the sandbox tools step by step and report results. + +## Tools Reference + +### sandbox_create + +Creates a hardware-isolated MicroVM. Must be called first. + +``` +Arguments: + template (string, optional) — Template ID + timeout (integer, optional) — Sandbox lifetime in seconds (default: 600) + +Returns: sandbox ID, template ID, state, timeout +``` + +### sandbox_run_code + +Executes Python code in a persistent Jupyter kernel. Variables, imports, +and DataFrames persist across calls. + +``` +Arguments: + code (string, required) — Python source code + timeout (integer, optional) — Execution timeout in seconds (default: 120) + +Returns: stdout, stderr, text result, error traceback (if any) +``` + +### sandbox_run_command + +Runs a shell command. + +``` +Arguments: + command (string, required) — Shell command (passed to sh -lc) + timeout (integer, optional) — Command timeout in seconds (default: 60) + +Returns: exit code, stdout, stderr +``` + +### sandbox_read_file / sandbox_write_file + +File I/O inside the sandbox. Use to provide input data and retrieve outputs. + +### sandbox_snapshot + +Captures the complete sandbox state (filesystem + memory). Snapshots survive +sandbox destruction and can be used to clone or rollback later. + +### sandbox_pause + +Pauses the sandbox, preserving its memory snapshot. The sandbox can be +resumed later. Useful for checkpointing long-running work between sessions. + +### sandbox_destroy + +Kills the sandbox immediately. All unsaved state is lost (snapshots +survive). Always call this when done to free resources. + +## Caveats + +### Sandbox Startup Latency + +The first `sandbox_create` may take a few seconds (cold start). Subsequent +creates from the same template are faster due to VM pooling. + +### Template Ports + +- The MCP server uses port **49999** for `run_code` (Jupyter kernel gateway). + Your template must expose this port. +- Port **49983** (envd) is used for `run_command` and file operations. Most + templates expose this by default. + +### Network / Egress + +- By default, the sandbox can reach the public internet. Use `network` + options in `Sandbox.create()` if you need to restrict this. +- If Claude Code accesses an LLM API from inside the sandbox (unusual, since + LLM calls happen in the MCP server process on the host), you'll need to + configure CubeEgress domain allowlists. + +### File Size Limits + +`sandbox_read_file` truncates output at 50,000 characters to avoid flooding +the Claude Code context window. For large files, use `sandbox_run_command` +with `head`/`tail`/`wc` to inspect incrementally. + +### MCP vs Direct SDK + +This MCP server is designed for **interactive use with Claude Code**. If +you're building an automated agent pipeline, consider calling the +`cubesandbox` Python SDK directly — it gives you full control over +sandbox lifecycle, concurrency, and error handling. + +## Architecture + +``` +┌──────────────────┐ +│ Claude Code │ Anthropic's terminal coding agent +│ (on your host) │ +└────────┬─────────┘ + │ MCP protocol over stdio + │ +┌────────▼─────────┐ +│ mcp_server.py │ This file — MCP ↔ CubeSDK bridge +│ (this project) │ +└────────┬─────────┘ + │ cubesandbox Python SDK + │ (pip install cubesandbox) + │ +┌────────▼─────────┐ +│ CubeAPI │ CubeSandbox REST API (:3000) +└────────┬─────────┘ + │ gRPC +┌────────▼─────────┐ +│ CubeMaster / │ Scheduler, node agent, hypervisor +│ Cubelet / │ +│ CubeHypervisor │ +└────────┬─────────┘ + │ KVM +┌────────▼─────────┐ +│ MicroVM │ Hardware-isolated sandbox +│ (Linux kernel) │ ─ Python, Node.js, CLI tools +└──────────────────┘ +``` + +## Troubleshooting + +| Symptom | Likely Cause | Fix | +|--------------------------------------------|-------------------------------------|-----------------------------------------------------------------------------| +| `sandbox_create` returns "Connection refused" | CubeAPI not reachable | Check `E2B_API_URL`; ensure CubeAPI is running (`curl http://host:3000/health`) | +| `sandbox_run_code` hangs | Jupyter gateway not ready | Wait a few seconds after create; template may need port 49999 exposed | +| `sandbox_run_command` returns "not found" | Tool not installed in template | Use `sandbox_run_command` with `apt install` or `pip install` as needed | +| "Template not found" | Wrong or missing `CUBE_TEMPLATE_ID` | Run `cubemastercli tpl list` and verify the ID | +| SSL errors | Custom CA not trusted | Set `CUBE_SSL_CERT_FILE` to the path of the CubeSandbox CA bundle | + +## Related Documents + +- [CubeSandbox Claude Code Integration Guide](../../docs/guide/integrations/claude-code.md) — Full integration guide with best practices +- [CubeSandbox Quick Start](https://cube-sandbox.pages.dev/guide/quickstart) +- [CubeSandbox Python SDK](../sdk/python/README.md) +- [Claude Code MCP Documentation](https://docs.anthropic.com/en/docs/claude-code/mcp) diff --git a/examples/claude-code-sandbox/README_zh.md b/examples/claude-code-sandbox/README_zh.md new file mode 100644 index 000000000..0501d07a1 --- /dev/null +++ b/examples/claude-code-sandbox/README_zh.md @@ -0,0 +1,257 @@ +# Claude Code + CubeSandbox 集成 + +[English](README.md) + +通过 MCP(Model Context Protocol)将 Claude Code 的代码执行路由至 CubeSandbox +MicroVM,为 Claude Code 提供安全隔离的执行环境。 + +## 这是什么 + +一个 **MCP Server**,将 CubeSandbox 的沙箱操作暴露为 Claude Code 可调用的工具。 +当 Claude Code 需要执行 Python、运行 Shell 命令或操作文件时,这些操作会被代理到 +硬件隔离的 MicroVM 中,而非直接在宿主机上执行。 + +``` +Claude Code MCP (stdio) MCP Server CubeSandbox +────────── ────────── ────────── ─────────── +"帮我分析数据" → sandbox_create → Sandbox.create() → KVM MicroVM + sandbox_run_code sandbox.run_code() (隔离环境) + sandbox_read_file sandbox.files.read() ↑ 安全 +``` + +> **验证状态**:此 MCP Server 已对照 CubeSandbox v0.4.0 / v0.5.0 Python SDK 源码 +> 进行了审查,API 调用逻辑上正确,但**尚未在真实 CubeSandbox 部署中进行端到端测试**。 +> 欢迎反馈、Bug 报告和验证报告。 + +## 为什么需要它 + +| 不用时 | 用了之后 | +|----------------------------------------|---------------------------------------------| +| Claude Code 的 `bash` 直接在宿主机执行 | 代码在硬件隔离的 MicroVM 中运行 | +| `rm -rf /` 会破坏你的机器 | `rm -rf /` 只破坏沙箱 VM | +| `pip install` 污染系统环境 | 所有包安装在即用即毁的 VM 中 | +| 无快照/回滚能力 | 随时保存、克隆、回滚沙箱状态 | +| 无网络访问控制 | 按沙箱粒度设置网络策略(allow/deny) | + +## 前提条件 + +- **已部署 CubeSandbox**(参考[快速开始](https://cube-sandbox.pages.dev/zh/guide/quickstart))。 +- **已有沙箱模板**:预装 Python 及所需工具。标准 `sandbox-code` 镜像能满足大部分场景。 +- **Claude Code** 支持 MCP(Claude Code v1.0.0+)。 +- **Python 3.10+** 用于运行此 MCP Server。 + +## 快速开始 + +### 1. 安装依赖 + +```bash +pip install -r requirements.txt +``` + +### 2. 配置环境变量 + +```bash +cp .env.example .env +``` + +编辑 `.env`,填入你的 CubeSandbox 连接信息: + +| 变量 | 说明 | +|-----------------------|---------------------------------------------------------------| +| `E2B_API_URL` | CubeAPI 地址,如 `http://:3000` | +| `E2B_API_KEY` | CubeAPI 认证密钥(本地/测试部署可用任意占位符,如 `e2b_000000`) | +| `CUBE_TEMPLATE_ID` | 沙箱模板 ID | +| `CUBE_SSL_CERT_FILE` | (可选)CubeSandbox CA 证书路径,用于 HTTPS 连接 | + +### 3. 创建沙箱模板 + +如果还没有模板: + +```bash +cubemastercli tpl create-from-image \ + --image cube-sandbox-int.tencentcloudcr.com/cube-sandbox/sandbox-code:latest \ + --writable-layer-size 1G \ + --expose-port 49999 \ + --expose-port 49983 \ + --probe 49999 +``` + +将输出的 `template_id` 填入 `.env`。 + +> **镜像源**:国内用户请使用 `cube-sandbox-cn.tencentcloudcr.com/cube-sandbox/sandbox-code:latest`。 + +### 4. 在 Claude Code 中注册 MCP Server + +在 Claude Code 配置中添加: + +```json +{ + "mcpServers": { + "cube-sandbox": { + "command": "python", + "args": ["/path/to/cube-sandbox/examples/claude-code-sandbox/mcp_server.py"], + "env": { + "CUBE_TEMPLATE_ID": "", + "E2B_API_URL": "http://:3000", + "E2B_API_KEY": "e2b_000000" + } + } + } +} +``` + +配置文件位置: +- 项目级:`.claude/settings.json` +- 用户级:`~/.claude/settings.json` + +### 5. 开始使用 + +重启 Claude Code(或重新加载 MCP Server)。Claude Code 现在拥有以下工具: + +| 工具 | 功能 | +|------------------------|--------------------------| +| `sandbox_create` | 创建新的隔离沙箱 | +| `sandbox_run_code` | 执行 Python(Jupyter 内核)| +| `sandbox_run_command` | 执行 Shell 命令 | +| `sandbox_read_file` | 从沙箱读取文件 | +| `sandbox_write_file` | 向沙箱写入文件 | +| `sandbox_get_info` | 查看沙箱元数据 | +| `sandbox_snapshot` | 保存时间点快照 | +| `sandbox_pause` | 暂停沙箱(保留状态) | +| `sandbox_destroy` | 销毁沙箱 | + +开始对话: + +``` +> 帮我创建一个沙箱,然后写一个 Python 脚本计算前 100 个斐波那契数, + 保存到 fib.csv,读取文件并验证结果。 +``` + +Claude Code 会按步骤调用沙箱工具并报告结果。 + +## 工具参考 + +### sandbox_create + +创建硬件隔离的 MicroVM。必须首先调用。 + +``` +参数: + template (字符串, 可选) — 模板 ID + timeout (整数, 可选) — 沙箱生命周期秒数(默认 600) + +返回: 沙箱 ID、模板 ID、状态、超时时间 +``` + +### sandbox_run_code + +在持久化 Jupyter 内核中执行 Python 代码。变量、导入、DataFrame 在多次调用间保持。 + +``` +参数: + code (字符串, 必填) — Python 源码 + timeout (整数, 可选) — 执行超时秒数(默认 120) + +返回: stdout、stderr、文本结果、错误回溯(如有) +``` + +### sandbox_run_command + +执行 Shell 命令。 + +``` +参数: + command (字符串, 必填) — Shell 命令(传递给 sh -lc) + timeout (整数, 可选) — 命令超时秒数(默认 60) + +返回: 退出码、stdout、stderr +``` + +### sandbox_read_file / sandbox_write_file + +沙箱内文件读写。用于提供输入数据和获取输出。 + +### sandbox_snapshot + +捕获完整沙箱状态(文件系统 + 内存)。快照独立于沙箱生命周期,可用于后续克隆或回滚。 + +### sandbox_pause + +暂停沙箱并保留内存快照。后续可恢复。适合跨会话保存长任务进度。 + +### sandbox_destroy + +立即销毁沙箱。所有未保存状态将丢失(快照不受影响)。完成后请及时调用来释放资源。 + +## 注意事项 + +### 沙箱启动延迟 + +首次 `sandbox_create` 可能需要几秒(冷启动)。同一模板的后续创建因 VM 池化会更快。 + +### 模板端口 + +- MCP Server 使用端口 **49999** 进行 `run_code`(Jupyter 内核网关)。模板必须暴露此端口。 +- 端口 **49983**(envd)用于 `run_command` 和文件操作。大部分模板默认暴露此端口。 + +### 网络与出口 + +- 默认情况下沙箱可访问公网。如需限制,请在 `Sandbox.create()` 中使用 `network` 选项。 +- 如果 Claude Code 从沙箱内访问 LLM API(不常见,LLM 调用通常发生在宿主机的 MCP Server 进程中),需要配置 CubeEgress 域名白名单。 + +### 文件大小限制 + +`sandbox_read_file` 将输出截断至 50,000 字符以免淹没 Claude Code 上下文窗口。对于大文件,请使用 `sandbox_run_command` 配合 `head`/`tail`/`wc` 逐步查看。 + +### MCP vs 直接 SDK + +此 MCP Server 设计用于**与 Claude Code 交互使用**。如果你在构建自动化 Agent 流水线,建议直接调用 `cubesandbox` Python SDK——它能提供对沙箱生命周期、并发、错误处理的完全控制。 + +## 架构 + +``` +┌──────────────────┐ +│ Claude Code │ Anthropic 的终端编码 Agent +│ (你的宿主机上) │ +└────────┬─────────┘ + │ MCP 协议 (stdio) + │ +┌────────▼─────────┐ +│ mcp_server.py │ 本文件 — MCP ↔ CubeSDK 桥接 +│ (本项目) │ +└────────┬─────────┘ + │ cubesandbox Python SDK + │ (pip install cubesandbox) + │ +┌────────▼─────────┐ +│ CubeAPI │ CubeSandbox REST API (:3000) +└────────┬─────────┘ + │ gRPC +┌────────▼─────────┐ +│ CubeMaster / │ 调度器、节点代理、Hypervisor +│ Cubelet / │ +│ CubeHypervisor │ +└────────┬─────────┘ + │ KVM +┌────────▼─────────┐ +│ MicroVM │ 硬件隔离沙箱 +│ (独立 Linux 内核)│ ─ Python、Node.js、CLI 工具 +└──────────────────┘ +``` + +## 常见问题 + +| 现象 | 可能原因 | 解决方案 | +|-----------------------------------------------|---------------------|---------------------------------------------------------------------------------| +| `sandbox_create` 返回 "Connection refused" | CubeAPI 不可达 | 检查 `E2B_API_URL`;确认 CubeAPI 正在运行(`curl http://host:3000/health`) | +| `sandbox_run_code` 卡住 | Jupyter 网关未就绪 | 创建沙箱后等待几秒;模板需暴露 49999 端口 | +| `sandbox_run_command` 返回 "not found" | 工具未安装在模板中 | 用 `sandbox_run_command` 执行 `apt install` 或 `pip install` | +| "Template not found" | `CUBE_TEMPLATE_ID` 错误或缺失 | 执行 `cubemastercli tpl list` 核对 ID | +| SSL 错误 | 自定义 CA 未被信任 | 设置 `CUBE_SSL_CERT_FILE` 指向 CubeSandbox CA 证书路径 | + +## 相关文档 + +- [CubeSandbox Claude Code 集成指南](../../docs/zh/guide/integrations/claude-code.md) — 完整集成指南与最佳实践 +- [CubeSandbox 快速开始](https://cube-sandbox.pages.dev/zh/guide/quickstart) +- [CubeSandbox Python SDK](../sdk/python/README.md) +- [Claude Code MCP 文档](https://docs.anthropic.com/en/docs/claude-code/mcp) diff --git a/examples/claude-code-sandbox/mcp_server.py b/examples/claude-code-sandbox/mcp_server.py new file mode 100644 index 000000000..3817d1809 --- /dev/null +++ b/examples/claude-code-sandbox/mcp_server.py @@ -0,0 +1,537 @@ +"""CubeSandbox MCP Server for Claude Code. + +Exposes CubeSandbox sandbox operations as MCP tools so Claude Code can +safely execute code, run shell commands, and manage files inside +hardware-isolated MicroVMs — all through natural language. + +Architecture:: + + Claude Code ←→ MCP (stdio) ←→ this server ←→ CubeSandbox API + (CubeAPI :3000) + +Usage:: + + pip install -r requirements.txt + # Then configure Claude Code settings.json to point to this script. + # See README.md for the full setup flow. + +Environment variables (required):: + + CUBE_TEMPLATE_ID — sandbox template ID + E2B_API_URL — CubeAPI endpoint, e.g. http://:3000 + E2B_API_KEY — CubeAPI auth key (any string for local deploys) + CUBE_SSL_CERT_FILE — optional, path to CA bundle for cube HTTPS + +Status: + This MCP Server has been reviewed against the CubeSandbox v0.4.0 / + v0.5.0 Python SDK source and is believed to be correct, but has not + been tested end-to-end against a running CubeSandbox deployment. + Feedback and bug reports are welcome. +""" + +from __future__ import annotations + +import os +from pathlib import Path +from typing import Any + +# --------------------------------------------------------------------------- +# Optional SSL patch for CubeSandbox HTTPS +# --------------------------------------------------------------------------- +_ssl_cert = os.environ.get("CUBE_SSL_CERT_FILE") +if _ssl_cert and Path(_ssl_cert).is_file(): + os.environ["SSL_CERT_FILE"] = _ssl_cert + +# --------------------------------------------------------------------------- +# SDK import +# --------------------------------------------------------------------------- +try: + from cubesandbox import Sandbox +except ImportError: + raise SystemExit( + "cubesandbox is not installed. Run: pip install cubesandbox" + ) + +try: + from mcp.server import Server, NotificationOptions + from mcp.server.stdio import stdio_server + from mcp.types import Tool, TextContent +except ImportError: + raise SystemExit("mcp is not installed. Run: pip install mcp") + +# --------------------------------------------------------------------------- +# Global sandbox reference (one per MCP session) +# --------------------------------------------------------------------------- +_sandbox: Sandbox | None = None + +# --------------------------------------------------------------------------- +# Validation helpers +# --------------------------------------------------------------------------- + +def _validate_int(name: str, value: object, default: int, *, + min_val: int = 1, max_val: int = 86_400) -> int: + """Coerce *value* to int, falling back to *default* on failure.""" + if value is None: + return default + try: + v = int(value) + except (TypeError, ValueError): + raise ValueError( + f"{name} must be an integer, got {type(value).__name__}: {value!r}" + ) + if not (min_val <= v <= max_val): + raise ValueError( + f"{name} must be between {min_val} and {max_val}, got {v}" + ) + return v + + +def _validate_float(name: str, value: object, default: float, *, + min_val: float = 1.0, max_val: float = 3_600.0) -> float: + """Coerce *value* to float, falling back to *default* on failure.""" + if value is None: + return default + try: + v = float(value) + except (TypeError, ValueError): + raise ValueError( + f"{name} must be a number, got {type(value).__name__}: {value!r}" + ) + if not (min_val <= v <= max_val): + raise ValueError( + f"{name} must be between {min_val} and {max_val}, got {v}" + ) + return v + + +def _require_str(name: str, value: object) -> str: + """Require *value* to be a non-empty string.""" + if not isinstance(value, str) or not value.strip(): + raise ValueError( + f"{name} must be a non-empty string, got {type(value).__name__}: {value!r}" + ) + return value.strip() + + +# --------------------------------------------------------------------------- +# MCP Server setup +# --------------------------------------------------------------------------- +server = Server("cube-sandbox") + + +@server.list_tools() +async def list_tools() -> list[Tool]: + return [ + Tool( + name="sandbox_create", + description=( + "Create a new isolated sandbox environment. " + "Must be called before any other sandbox_* tools. " + "The sandbox is a hardware-isolated MicroVM with its own " + "Linux kernel, preinstalled with Python, Node.js, and common " + "CLI tools (depending on the template)." + ), + inputSchema={ + "type": "object", + "properties": { + "template": { + "type": "string", + "description": ( + "Template ID. If omitted, uses CUBE_TEMPLATE_ID " + "environment variable." + ), + }, + "timeout": { + "type": "integer", + "description": "Sandbox lifetime in seconds (default: 600, min: 30, max: 86400).", + }, + }, + "required": [], + }, + ), + Tool( + name="sandbox_run_code", + description=( + "Execute Python code inside the sandbox via a persistent " + "Jupyter kernel. Variables, imports, and DataFrames persist " + "across calls within the same sandbox session. Use for data " + "analysis, computation, plotting, or any Python work. " + "For plain shell commands, use sandbox_run_command instead." + ), + inputSchema={ + "type": "object", + "properties": { + "code": { + "type": "string", + "description": "Python source code to execute.", + }, + "timeout": { + "type": "integer", + "description": "Execution timeout in seconds (default: 120, min: 1, max: 3600).", + }, + }, + "required": ["code"], + }, + ), + Tool( + name="sandbox_run_command", + description=( + "Run a shell command inside the sandbox. " + "Returns exit code, stdout, and stderr. " + "Use for file inspection (ls/cat/head), package management " + "(pip install / npm install), git operations, or any CLI tool." + ), + inputSchema={ + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "Shell command to execute (passed to sh -lc).", + }, + "timeout": { + "type": "integer", + "description": "Command timeout in seconds (default: 60, min: 1, max: 3600).", + }, + }, + "required": ["command"], + }, + ), + Tool( + name="sandbox_read_file", + description=( + "Read the contents of a file from the sandbox. " + "Use to retrieve generated output, logs, or plot images. " + "Output is truncated at 50,000 characters." + ), + inputSchema={ + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "Absolute or relative path inside the sandbox.", + }, + }, + "required": ["path"], + }, + ), + Tool( + name="sandbox_write_file", + description=( + "Write content to a file inside the sandbox. " + "Use to provide input data, scripts, or configuration files " + "that subsequent run_code / run_command calls will use." + ), + inputSchema={ + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "Target path inside the sandbox.", + }, + "content": { + "type": "string", + "description": "File content to write.", + }, + }, + "required": ["path", "content"], + }, + ), + Tool( + name="sandbox_get_info", + description=( + "Retrieve sandbox metadata: ID, state, CPU/memory, uptime, " + "template ID. Useful for checking whether a sandbox is still " + "running or for debugging connectivity issues." + ), + inputSchema={"type": "object", "properties": {}, "required": []}, + ), + Tool( + name="sandbox_snapshot", + description=( + "Create a point-in-time snapshot of the sandbox, preserving " + "the complete filesystem and memory state. The snapshot " + "survives sandbox destruction and can be used later to " + "clone or rollback. Useful for saving long-running " + "work before a pause or as a checkpoint." + ), + inputSchema={ + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Label for the snapshot (optional).", + }, + }, + "required": [], + }, + ), + Tool( + name="sandbox_pause", + description=( + "Pause the sandbox, preserving its memory snapshot. " + "The sandbox can be resumed later via a new sandbox_create " + "pointing to the snapshot. Use to save work and release " + "compute resources between sessions." + ), + inputSchema={"type": "object", "properties": {}, "required": []}, + ), + Tool( + name="sandbox_destroy", + description=( + "Destroy the sandbox immediately, freeing all resources. " + "All unsaved state is lost. Snapshots created via " + "sandbox_snapshot survive independently." + ), + inputSchema={"type": "object", "properties": {}, "required": []}, + ), + ] + + +# --------------------------------------------------------------------------- +# Tool handler +# --------------------------------------------------------------------------- +@server.call_tool() +async def call_tool(name: str, arguments: dict) -> list[TextContent]: + global _sandbox + + # ---- sandbox_create ------------------------------------------------ + if name == "sandbox_create": + if _sandbox is not None: + return [_text("⚠️ A sandbox is already active. Destroy it first.")] + + template = arguments.get("template") or os.environ.get("CUBE_TEMPLATE_ID") + if not template: + return [_text("❌ Missing template. Set CUBE_TEMPLATE_ID or pass template=.")] + + try: + timeout = _validate_int("timeout", arguments.get("timeout"), 600, + min_val=30, max_val=86_400) + except ValueError as exc: + return [_text(f"❌ Invalid argument: {exc}")] + + try: + _sandbox = Sandbox.create(template=template, timeout=timeout) + except Exception as exc: + return [_text(f"❌ Failed to create sandbox: {exc}")] + + try: + info = _sandbox.get_info() + except Exception: + info = {} + return [_text( + f"✅ Sandbox created.\n" + f" ID: {_sandbox.sandbox_id}\n" + f" Template: {_sandbox.template_id}\n" + f" State: {info.get('state', 'unknown')}\n" + f" Timeout: {timeout}s" + )] + + # ---- guard: sandbox must exist ------------------------------------- + if _sandbox is None: + return [_text("❌ No active sandbox. Call sandbox_create first.")] + + # ---- sandbox_run_code ---------------------------------------------- + if name == "sandbox_run_code": + try: + code = _require_str("code", arguments.get("code")) + except ValueError as exc: + return [_text(f"❌ Invalid argument: {exc}")] + + try: + timeout_val = _validate_float("timeout", arguments.get("timeout"), + 120.0, min_val=1.0, max_val=3_600.0) + except ValueError as exc: + return [_text(f"❌ Invalid argument: {exc}")] + + try: + execution = _sandbox.run_code(code, timeout=timeout_val) + except Exception as exc: + return [_text(f"❌ Code execution failed: {exc}")] + + parts: list[str] = [] + try: + if execution.logs and execution.logs.stdout: + stdout_text = "".join(execution.logs.stdout).strip() + if stdout_text: + parts.append(stdout_text) + except Exception: + pass + try: + if execution.logs and execution.logs.stderr: + stderr_text = "".join(execution.logs.stderr).strip() + if stderr_text: + parts.append(f"[stderr]\n{stderr_text}") + except Exception: + pass + try: + if execution.error: + parts.append( + f"❌ {execution.error.name}: {execution.error.value}\n" + f"{execution.error.traceback[:2000] if execution.error.traceback else ''}" + ) + except Exception: + pass + try: + if execution.text and execution.text.strip(): + parts.append(execution.text.strip()) + except Exception: + pass + + if not parts: + return [_text("(executed, no output)")] + return [_text("\n\n".join(parts))] + + # ---- sandbox_run_command ------------------------------------------- + if name == "sandbox_run_command": + try: + command = _require_str("command", arguments.get("command")) + except ValueError as exc: + return [_text(f"❌ Invalid argument: {exc}")] + + try: + timeout_val = _validate_int("timeout", arguments.get("timeout"), + 60, min_val=1, max_val=3_600) + except ValueError as exc: + return [_text(f"❌ Invalid argument: {exc}")] + + try: + result = _sandbox.commands.run(command, timeout=timeout_val) + except Exception as exc: + return [_text(f"❌ Command failed: {exc}")] + + output = "" + if result.stdout: + output += result.stdout.strip() + if result.stderr: + if output: + output += "\n[stderr]\n" + output += result.stderr.strip() + return [_text( + f"(exit={result.exit_code})\n{output}" if output + else f"(exit={result.exit_code})" + )] + + # ---- sandbox_read_file --------------------------------------------- + if name == "sandbox_read_file": + try: + path = _require_str("path", arguments.get("path")) + except ValueError as exc: + return [_text(f"❌ Invalid argument: {exc}")] + + try: + content = _sandbox.files.read(path) + except Exception as exc: + return [_text(f"❌ Failed to read {path!r}: {exc}")] + + # Guard against non-string returns (defensive — SDK returns str) + if not isinstance(content, str): + content = str(content) + + limit = 50_000 + if len(content) > limit: + content = ( + content[:limit] + + f"\n\n... (truncated {len(content) - limit:,d} bytes)" + ) + return [_text(content)] + + # ---- sandbox_write_file -------------------------------------------- + if name == "sandbox_write_file": + try: + path = _require_str("path", arguments.get("path")) + file_content = _require_str("content", arguments.get("content")) + except ValueError as exc: + return [_text(f"❌ Invalid argument: {exc}")] + + try: + _sandbox.files.write(path, file_content.encode("utf-8")) + return [_text(f"✅ Written {len(file_content):,d} bytes to {path!r}")] + except Exception as exc: + return [_text(f"❌ Failed to write {path!r}: {exc}")] + + # ---- sandbox_get_info ---------------------------------------------- + if name == "sandbox_get_info": + try: + info = _sandbox.get_info() + lines = [] + for k in ("sandboxID", "templateID", "state", "cpuCount", + "memoryMB", "startedAt"): + v = info.get(k) + if v is not None: + lines.append(f" {k}: {v}") + if not lines: + return [_text(f"(info returned, but no recognised keys in: {sorted(info.keys())!r})")] + return [_text("\n".join(lines))] + except Exception as exc: + return [_text(f"❌ Failed to get info: {exc}")] + + # ---- sandbox_snapshot ---------------------------------------------- + if name == "sandbox_snapshot": + snap_name = arguments.get("name") or None + try: + snap = _sandbox.create_snapshot(name=snap_name) + # SnapshotInfo.names is list[str] + names_str = ", ".join(snap.names) if snap.names else "(none)" + return [_text( + f"✅ Snapshot created.\n" + f" ID: {snap.snapshot_id}\n" + f" Names: {names_str}" + )] + except Exception as exc: + return [_text(f"❌ Snapshot failed: {exc}")] + + # ---- sandbox_pause ------------------------------------------------- + if name == "sandbox_pause": + try: + _sandbox.pause() + return [_text( + "✅ Sandbox paused. Use sandbox_create with the same " + "sandbox ID to resume." + )] + except Exception as exc: + return [_text(f"❌ Pause failed: {exc}")] + + # ---- sandbox_destroy ----------------------------------------------- + if name == "sandbox_destroy": + try: + sid = _sandbox.sandbox_id + except Exception: + sid = "" + try: + _sandbox.kill() + except Exception as exc: + _sandbox = None + return [_text(f"⚠️ Destroy may have partially failed: {exc}")] + finally: + _sandbox = None + return [_text(f"✅ Sandbox {sid!r} destroyed.")] + + return [_text(f"❌ Unknown tool: {name!r}")] + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- +def _text(content: str) -> TextContent: + return TextContent(type="text", text=content) + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- +async def main() -> None: + async with stdio_server() as (read_stream, write_stream): + await server.run( + read_stream, + write_stream, + server.create_initialization_options( + notification_options=NotificationOptions(), + experimental_capabilities={}, + ), + ) + + +if __name__ == "__main__": + import asyncio + + asyncio.run(main()) diff --git a/examples/claude-code-sandbox/requirements.txt b/examples/claude-code-sandbox/requirements.txt new file mode 100644 index 000000000..2fbda65d4 --- /dev/null +++ b/examples/claude-code-sandbox/requirements.txt @@ -0,0 +1,6 @@ +# CubeSandbox MCP Server for Claude Code +# ----------------------------------------- +# Install with: pip install -r requirements.txt + +cubesandbox>=0.3.0 +mcp>=1.0.0 From 1997f5c69e7ffa8d71933b9e006741c2fb02ab47 Mon Sep 17 00:00:00 2001 From: xyaohubery Date: Sun, 5 Jul 2026 15:19:00 +0800 Subject: [PATCH 2/8] fix: address auto-review feedback on MCP Server - Replace silent except:pass in run_code with [mcp: error ...] warnings - Remove redundant get_info() HTTP call after sandbox creation - Fix falsy string trap in sandbox_snapshot name handling Assisted-by: claude-code:deepseek-v4-pro --- examples/claude-code-sandbox/mcp_server.py | 34 +++++++++++++--------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/examples/claude-code-sandbox/mcp_server.py b/examples/claude-code-sandbox/mcp_server.py index 3817d1809..e41f709ad 100644 --- a/examples/claude-code-sandbox/mcp_server.py +++ b/examples/claude-code-sandbox/mcp_server.py @@ -314,15 +314,11 @@ async def call_tool(name: str, arguments: dict) -> list[TextContent]: except Exception as exc: return [_text(f"❌ Failed to create sandbox: {exc}")] - try: - info = _sandbox.get_info() - except Exception: - info = {} return [_text( f"✅ Sandbox created.\n" f" ID: {_sandbox.sandbox_id}\n" f" Template: {_sandbox.template_id}\n" - f" State: {info.get('state', 'unknown')}\n" + f" State: running\n" f" Timeout: {timeout}s" )] @@ -349,33 +345,37 @@ async def call_tool(name: str, arguments: dict) -> list[TextContent]: return [_text(f"❌ Code execution failed: {exc}")] parts: list[str] = [] + # Safely extract structured output — each field is BestEffort. + # On unexpected types we splice a short warning into the output + # so callers can see that something went wrong, but the partial + # result is still returned for debugging. try: if execution.logs and execution.logs.stdout: stdout_text = "".join(execution.logs.stdout).strip() if stdout_text: parts.append(stdout_text) - except Exception: - pass + except Exception as _exc: + parts.append(f"[mcp: error reading stdout: {_exc}]") try: if execution.logs and execution.logs.stderr: stderr_text = "".join(execution.logs.stderr).strip() if stderr_text: parts.append(f"[stderr]\n{stderr_text}") - except Exception: - pass + except Exception as _exc: + parts.append(f"[mcp: error reading stderr: {_exc}]") try: if execution.error: parts.append( f"❌ {execution.error.name}: {execution.error.value}\n" f"{execution.error.traceback[:2000] if execution.error.traceback else ''}" ) - except Exception: - pass + except Exception as _exc: + parts.append(f"[mcp: error reading execution.error: {_exc}]") try: if execution.text and execution.text.strip(): parts.append(execution.text.strip()) - except Exception: - pass + except Exception as _exc: + parts.append(f"[mcp: error reading execution.text: {_exc}]") if not parts: return [_text("(executed, no output)")] @@ -467,7 +467,13 @@ async def call_tool(name: str, arguments: dict) -> list[TextContent]: # ---- sandbox_snapshot ---------------------------------------------- if name == "sandbox_snapshot": - snap_name = arguments.get("name") or None + # Explicit None check to avoid falsy string trap: + # "0", "False", empty string are all valid snapshot names in the API. + snap_name = arguments.get("name") + if snap_name is not None and not isinstance(snap_name, str): + return [_text(f"❌ snapshot 'name' must be a string, got {type(snap_name).__name__}")] + if snap_name is not None and not snap_name.strip(): + snap_name = None # Treat empty/whitespace-only as "no name" try: snap = _sandbox.create_snapshot(name=snap_name) # SnapshotInfo.names is list[str] From 3a30fa555344522213a484ba34d82b9558bcf18b Mon Sep 17 00:00:00 2001 From: xyaohubery Date: Sun, 5 Jul 2026 15:36:09 +0800 Subject: [PATCH 3/8] fix: address all auto-review feedback on MCP Server Critical fixes: - Use CUBE_API_URL instead of E2B_API_URL (matches SDK Config class) - Remove non-functional E2B_API_KEY references - Wrap sync SDK calls via asyncio.to_thread() to avoid blocking event loop - Add asyncio.Lock to prevent TOCTOU race on _sandbox lifecycle Medium fixes: - Sanitize file paths (reject .. traversal) - Register atexit cleanup to kill sandbox on exit - Sanitize exception messages (no internal URLs/stack traces) - Fix sandbox_pause success message (remove non-existent resume flow) - Remove redundant try/except around Execution dataclass field access - Align writable layer size to 2G across all docs Low fixes: - Move SSL_CERT_FILE side effect from import time to main() - Change run_code timeout schema from 'integer' to 'number' Assisted-by: claude-code:deepseek-v4-pro --- docs/guide/integrations/claude-code.md | 5 +- docs/zh/guide/integrations/claude-code.md | 5 +- examples/claude-code-sandbox/.env.example | 10 +- examples/claude-code-sandbox/README.md | 10 +- examples/claude-code-sandbox/README_zh.md | 10 +- examples/claude-code-sandbox/mcp_server.py | 300 ++++++++++++--------- 6 files changed, 181 insertions(+), 159 deletions(-) diff --git a/docs/guide/integrations/claude-code.md b/docs/guide/integrations/claude-code.md index 97505461d..70ef73277 100644 --- a/docs/guide/integrations/claude-code.md +++ b/docs/guide/integrations/claude-code.md @@ -114,8 +114,7 @@ Add the MCP server entry to your Claude Code settings: "args": ["/absolute/path/to/CubeSandbox/examples/claude-code-sandbox/mcp_server.py"], "env": { "CUBE_TEMPLATE_ID": "", - "E2B_API_URL": "http://:3000", - "E2B_API_KEY": "e2b_000000" + "CUBE_API_URL": "http://:3000" } } } @@ -323,7 +322,7 @@ to provide input and sandbox_read_file to retrieve output. | Symptom | Likely Cause | Fix | |-------------------------------------------------|-------------------------------------|-----------------------------------------------------------------------------------------| | MCP server fails to start | Missing dependencies | Run `pip install -r requirements.txt`; check Python 3.10+ | -| `sandbox_create` hangs | CubeAPI unreachable | Verify `E2B_API_URL`; `curl http://:3000/health` | +| `sandbox_create` hangs | CubeAPI unreachable | Verify `CUBE_API_URL`; `curl http://:3000/health` | | `sandbox_create` returns 404 | Wrong template ID | Check `CUBE_TEMPLATE_ID`; run `cubemastercli tpl list` | | `sandbox_run_code` returns 502 | Sandbox evicted (timeout/deleted) | Increase `timeout`; check that sandbox isn't being killed by idle timeout | | `sandbox_run_code` returns "connection refused" | Jupyter gateway not ready | Template must expose port 49999; wait 2-3s after create before first `run_code` call | diff --git a/docs/zh/guide/integrations/claude-code.md b/docs/zh/guide/integrations/claude-code.md index 8785c1468..47f9adb03 100644 --- a/docs/zh/guide/integrations/claude-code.md +++ b/docs/zh/guide/integrations/claude-code.md @@ -108,8 +108,7 @@ cp .env.example .env "args": ["/绝对路径/CubeSandbox/examples/claude-code-sandbox/mcp_server.py"], "env": { "CUBE_TEMPLATE_ID": "", - "E2B_API_URL": "http://:3000", - "E2B_API_KEY": "e2b_000000" + "CUBE_API_URL": "http://:3000" } } } @@ -303,7 +302,7 @@ sb = Sandbox.create( | 现象 | 可能原因 | 解决方案 | |---------------------------------------------------|----------------------|-----------------------------------------------------------------------------------------| | MCP Server 无法启动 | 缺少依赖 | 执行 `pip install -r requirements.txt`;检查 Python 3.10+ | -| `sandbox_create` 挂起 | CubeAPI 不可达 | 检查 `E2B_API_URL`;`curl http://:3000/health` | +| `sandbox_create` 挂起 | CubeAPI 不可达 | 检查 `CUBE_API_URL`;`curl http://:3000/health` | | `sandbox_create` 返回 404 | 模板 ID 错误 | 核对 `CUBE_TEMPLATE_ID`;执行 `cubemastercli tpl list` | | `sandbox_run_code` 返回 502 | 沙箱被驱逐(超时/被删) | 增加 `timeout`;检查沙箱是否被空闲超时机制终止 | | `sandbox_run_code` 返回 "connection refused" | Jupyter 网关未就绪 | 模板必须暴露端口 49999;首次 `run_code` 调用前等待 2–3 秒 | diff --git a/examples/claude-code-sandbox/.env.example b/examples/claude-code-sandbox/.env.example index 185130699..ded490cbf 100644 --- a/examples/claude-code-sandbox/.env.example +++ b/examples/claude-code-sandbox/.env.example @@ -6,17 +6,13 @@ # Required — CubeSandbox API connection # -------------------------------------------------------------------- -# CubeAPI endpoint (the E2B-compatible REST API, default port 3000). -E2B_API_URL=http://:3000 - -# API key. For local/testing deployments any non-empty placeholder works -# (e.g. "e2b_000000"). For authenticated deployments, use your real key. -E2B_API_KEY=e2b_000000 +# CubeAPI endpoint (default: http://127.0.0.1:3000). +CUBE_API_URL=http://:3000 # Sandbox template ID. Create one with: # cubemastercli tpl create-from-image \ # --image cube-sandbox-int.tencentcloudcr.com/cube-sandbox/sandbox-code:latest \ -# --writable-layer-size 1G --expose-port 49999 --expose-port 49983 --probe 49999 +# --writable-layer-size 2G --expose-port 49999 --expose-port 49983 --probe 49999 CUBE_TEMPLATE_ID= # -------------------------------------------------------------------- diff --git a/examples/claude-code-sandbox/README.md b/examples/claude-code-sandbox/README.md index 59abf1acd..252b4ba2f 100644 --- a/examples/claude-code-sandbox/README.md +++ b/examples/claude-code-sandbox/README.md @@ -64,8 +64,7 @@ Edit `.env` and fill in your CubeSandbox connection details: | Variable | Description | |----------------------|----------------------------------------------------------------| -| `E2B_API_URL` | CubeAPI endpoint, e.g. `http://:3000` | -| `E2B_API_KEY` | CubeAPI auth key (`e2b_000000` for local/testing deployments) | +| `CUBE_API_URL` | CubeAPI endpoint, e.g. `http://:3000` | | `CUBE_TEMPLATE_ID` | Template ID for your sandbox images | | `CUBE_SSL_CERT_FILE` | (Optional) Path to CubeSandbox CA bundle for HTTPS | @@ -76,7 +75,7 @@ If you don't have a template yet: ```bash cubemastercli tpl create-from-image \ --image cube-sandbox-int.tencentcloudcr.com/cube-sandbox/sandbox-code:latest \ - --writable-layer-size 1G \ + --writable-layer-size 2G \ --expose-port 49999 \ --expose-port 49983 \ --probe 49999 @@ -96,8 +95,7 @@ Add this to your Claude Code settings: "args": ["/path/to/cube-sandbox/examples/claude-code-sandbox/mcp_server.py"], "env": { "CUBE_TEMPLATE_ID": "", - "E2B_API_URL": "http://:3000", - "E2B_API_KEY": "e2b_000000" + "CUBE_API_URL": "http://:3000" } } } @@ -263,7 +261,7 @@ sandbox lifecycle, concurrency, and error handling. | Symptom | Likely Cause | Fix | |--------------------------------------------|-------------------------------------|-----------------------------------------------------------------------------| -| `sandbox_create` returns "Connection refused" | CubeAPI not reachable | Check `E2B_API_URL`; ensure CubeAPI is running (`curl http://host:3000/health`) | +| `sandbox_create` returns "Connection refused" | CubeAPI not reachable | Check `CUBE_API_URL`; ensure CubeAPI is running (`curl http://host:3000/health`) | | `sandbox_run_code` hangs | Jupyter gateway not ready | Wait a few seconds after create; template may need port 49999 exposed | | `sandbox_run_command` returns "not found" | Tool not installed in template | Use `sandbox_run_command` with `apt install` or `pip install` as needed | | "Template not found" | Wrong or missing `CUBE_TEMPLATE_ID` | Run `cubemastercli tpl list` and verify the ID | diff --git a/examples/claude-code-sandbox/README_zh.md b/examples/claude-code-sandbox/README_zh.md index 0501d07a1..d4dc0c2b0 100644 --- a/examples/claude-code-sandbox/README_zh.md +++ b/examples/claude-code-sandbox/README_zh.md @@ -58,8 +58,7 @@ cp .env.example .env | 变量 | 说明 | |-----------------------|---------------------------------------------------------------| -| `E2B_API_URL` | CubeAPI 地址,如 `http://:3000` | -| `E2B_API_KEY` | CubeAPI 认证密钥(本地/测试部署可用任意占位符,如 `e2b_000000`) | +| `CUBE_API_URL` | CubeAPI 地址,如 `http://:3000` | | `CUBE_TEMPLATE_ID` | 沙箱模板 ID | | `CUBE_SSL_CERT_FILE` | (可选)CubeSandbox CA 证书路径,用于 HTTPS 连接 | @@ -70,7 +69,7 @@ cp .env.example .env ```bash cubemastercli tpl create-from-image \ --image cube-sandbox-int.tencentcloudcr.com/cube-sandbox/sandbox-code:latest \ - --writable-layer-size 1G \ + --writable-layer-size 2G \ --expose-port 49999 \ --expose-port 49983 \ --probe 49999 @@ -92,8 +91,7 @@ cubemastercli tpl create-from-image \ "args": ["/path/to/cube-sandbox/examples/claude-code-sandbox/mcp_server.py"], "env": { "CUBE_TEMPLATE_ID": "", - "E2B_API_URL": "http://:3000", - "E2B_API_KEY": "e2b_000000" + "CUBE_API_URL": "http://:3000" } } } @@ -243,7 +241,7 @@ Claude Code 会按步骤调用沙箱工具并报告结果。 | 现象 | 可能原因 | 解决方案 | |-----------------------------------------------|---------------------|---------------------------------------------------------------------------------| -| `sandbox_create` 返回 "Connection refused" | CubeAPI 不可达 | 检查 `E2B_API_URL`;确认 CubeAPI 正在运行(`curl http://host:3000/health`) | +| `sandbox_create` 返回 "Connection refused" | CubeAPI 不可达 | 检查 `CUBE_API_URL`;确认 CubeAPI 正在运行(`curl http://host:3000/health`) | | `sandbox_run_code` 卡住 | Jupyter 网关未就绪 | 创建沙箱后等待几秒;模板需暴露 49999 端口 | | `sandbox_run_command` 返回 "not found" | 工具未安装在模板中 | 用 `sandbox_run_command` 执行 `apt install` 或 `pip install` | | "Template not found" | `CUBE_TEMPLATE_ID` 错误或缺失 | 执行 `cubemastercli tpl list` 核对 ID | diff --git a/examples/claude-code-sandbox/mcp_server.py b/examples/claude-code-sandbox/mcp_server.py index e41f709ad..1575adb1a 100644 --- a/examples/claude-code-sandbox/mcp_server.py +++ b/examples/claude-code-sandbox/mcp_server.py @@ -2,7 +2,7 @@ Exposes CubeSandbox sandbox operations as MCP tools so Claude Code can safely execute code, run shell commands, and manage files inside -hardware-isolated MicroVMs — all through natural language. +hardware-isolated MicroVMs. Architecture:: @@ -13,44 +13,36 @@ pip install -r requirements.txt # Then configure Claude Code settings.json to point to this script. - # See README.md for the full setup flow. -Environment variables (required):: +Environment variables:: - CUBE_TEMPLATE_ID — sandbox template ID - E2B_API_URL — CubeAPI endpoint, e.g. http://:3000 - E2B_API_KEY — CubeAPI auth key (any string for local deploys) + CUBE_API_URL — CubeAPI endpoint (default: http://127.0.0.1:3000) + CUBE_TEMPLATE_ID — sandbox template ID CUBE_SSL_CERT_FILE — optional, path to CA bundle for cube HTTPS Status: - This MCP Server has been reviewed against the CubeSandbox v0.4.0 / - v0.5.0 Python SDK source and is believed to be correct, but has not + This MCP Server has been reviewed against the CubeSandbox v0.5.0 + Python SDK source and is believed to be API-correct, but has not been tested end-to-end against a running CubeSandbox deployment. - Feedback and bug reports are welcome. """ from __future__ import annotations +import asyncio +import atexit import os +import signal +import sys from pathlib import Path from typing import Any -# --------------------------------------------------------------------------- -# Optional SSL patch for CubeSandbox HTTPS -# --------------------------------------------------------------------------- -_ssl_cert = os.environ.get("CUBE_SSL_CERT_FILE") -if _ssl_cert and Path(_ssl_cert).is_file(): - os.environ["SSL_CERT_FILE"] = _ssl_cert - # --------------------------------------------------------------------------- # SDK import # --------------------------------------------------------------------------- try: from cubesandbox import Sandbox except ImportError: - raise SystemExit( - "cubesandbox is not installed. Run: pip install cubesandbox" - ) + raise SystemExit("cubesandbox is not installed. Run: pip install cubesandbox") try: from mcp.server import Server, NotificationOptions @@ -60,9 +52,26 @@ raise SystemExit("mcp is not installed. Run: pip install mcp") # --------------------------------------------------------------------------- -# Global sandbox reference (one per MCP session) +# Per-session state (guarded by _lock) # --------------------------------------------------------------------------- _sandbox: Sandbox | None = None +_lock = asyncio.Lock() + + +# --------------------------------------------------------------------------- +# Shutdown — kill sandbox on exit / SIGTERM / SIGINT +# --------------------------------------------------------------------------- +def _cleanup() -> None: + """Best-effort sandbox kill on process exit.""" + sb = _sandbox + if sb is not None: + try: + sb.kill() + except Exception: + pass + + +atexit.register(_cleanup) # --------------------------------------------------------------------------- # Validation helpers @@ -70,7 +79,6 @@ def _validate_int(name: str, value: object, default: int, *, min_val: int = 1, max_val: int = 86_400) -> int: - """Coerce *value* to int, falling back to *default* on failure.""" if value is None: return default try: @@ -88,7 +96,6 @@ def _validate_int(name: str, value: object, default: int, *, def _validate_float(name: str, value: object, default: float, *, min_val: float = 1.0, max_val: float = 3_600.0) -> float: - """Coerce *value* to float, falling back to *default* on failure.""" if value is None: return default try: @@ -105,16 +112,33 @@ def _validate_float(name: str, value: object, default: float, *, def _require_str(name: str, value: object) -> str: - """Require *value* to be a non-empty string.""" if not isinstance(value, str) or not value.strip(): raise ValueError( - f"{name} must be a non-empty string, got {type(value).__name__}: {value!r}" + f"{name} must be a non-empty string, " + f"got {type(value).__name__}: {value!r}" ) return value.strip() +def _sanitize_path(path: str) -> str: + """Reject path traversal and absolute paths outside sandbox workspace.""" + p = Path(path) + if ".." in p.parts: + raise ValueError(f"Path traversal not allowed: {path!r}") + return str(p) + + +def _sanitize_error(exc: BaseException) -> str: + """Return a user-safe error message — no stack traces or internal URLs.""" + msg = str(exc).strip() + # Truncate long messages (e.g. HTML error pages) + if len(msg) > 500: + msg = msg[:500] + "..." + return msg + + # --------------------------------------------------------------------------- -# MCP Server setup +# MCP Server # --------------------------------------------------------------------------- server = Server("cube-sandbox") @@ -154,8 +178,7 @@ async def list_tools() -> list[Tool]: description=( "Execute Python code inside the sandbox via a persistent " "Jupyter kernel. Variables, imports, and DataFrames persist " - "across calls within the same sandbox session. Use for data " - "analysis, computation, plotting, or any Python work. " + "across calls within the same sandbox session. " "For plain shell commands, use sandbox_run_command instead." ), inputSchema={ @@ -166,7 +189,7 @@ async def list_tools() -> list[Tool]: "description": "Python source code to execute.", }, "timeout": { - "type": "integer", + "type": "number", "description": "Execution timeout in seconds (default: 120, min: 1, max: 3600).", }, }, @@ -200,7 +223,6 @@ async def list_tools() -> list[Tool]: name="sandbox_read_file", description=( "Read the contents of a file from the sandbox. " - "Use to retrieve generated output, logs, or plot images. " "Output is truncated at 50,000 characters." ), inputSchema={ @@ -208,7 +230,7 @@ async def list_tools() -> list[Tool]: "properties": { "path": { "type": "string", - "description": "Absolute or relative path inside the sandbox.", + "description": "Relative path inside the sandbox workspace.", }, }, "required": ["path"], @@ -218,15 +240,14 @@ async def list_tools() -> list[Tool]: name="sandbox_write_file", description=( "Write content to a file inside the sandbox. " - "Use to provide input data, scripts, or configuration files " - "that subsequent run_code / run_command calls will use." + "Subsequent run_code / run_command calls can use the file." ), inputSchema={ "type": "object", "properties": { "path": { "type": "string", - "description": "Target path inside the sandbox.", + "description": "Relative path inside the sandbox workspace.", }, "content": { "type": "string", @@ -240,8 +261,7 @@ async def list_tools() -> list[Tool]: name="sandbox_get_info", description=( "Retrieve sandbox metadata: ID, state, CPU/memory, uptime, " - "template ID. Useful for checking whether a sandbox is still " - "running or for debugging connectivity issues." + "template ID. Useful for debugging connectivity issues." ), inputSchema={"type": "object", "properties": {}, "required": []}, ), @@ -250,9 +270,8 @@ async def list_tools() -> list[Tool]: description=( "Create a point-in-time snapshot of the sandbox, preserving " "the complete filesystem and memory state. The snapshot " - "survives sandbox destruction and can be used later to " - "clone or rollback. Useful for saving long-running " - "work before a pause or as a checkpoint." + "survives sandbox destruction and can be used to clone or " + "rollback later." ), inputSchema={ "type": "object", @@ -269,9 +288,8 @@ async def list_tools() -> list[Tool]: name="sandbox_pause", description=( "Pause the sandbox, preserving its memory snapshot. " - "The sandbox can be resumed later via a new sandbox_create " - "pointing to the snapshot. Use to save work and release " - "compute resources between sessions." + "The snapshot survives independently and can be accessed " + "via snapshot management tools after the sandbox is paused." ), inputSchema={"type": "object", "properties": {}, "required": []}, ), @@ -288,7 +306,7 @@ async def list_tools() -> list[Tool]: # --------------------------------------------------------------------------- -# Tool handler +# Tool handler (all SDK calls go through asyncio.to_thread) # --------------------------------------------------------------------------- @server.call_tool() async def call_tool(name: str, arguments: dict) -> list[TextContent]: @@ -296,34 +314,39 @@ async def call_tool(name: str, arguments: dict) -> list[TextContent]: # ---- sandbox_create ------------------------------------------------ if name == "sandbox_create": - if _sandbox is not None: - return [_text("⚠️ A sandbox is already active. Destroy it first.")] - - template = arguments.get("template") or os.environ.get("CUBE_TEMPLATE_ID") - if not template: - return [_text("❌ Missing template. Set CUBE_TEMPLATE_ID or pass template=.")] - - try: - timeout = _validate_int("timeout", arguments.get("timeout"), 600, - min_val=30, max_val=86_400) - except ValueError as exc: - return [_text(f"❌ Invalid argument: {exc}")] - - try: - _sandbox = Sandbox.create(template=template, timeout=timeout) - except Exception as exc: - return [_text(f"❌ Failed to create sandbox: {exc}")] + async with _lock: + if _sandbox is not None: + return [_text("⚠️ A sandbox is already active. Destroy it first.")] + + template = arguments.get("template") or os.environ.get("CUBE_TEMPLATE_ID") + if not template: + return [_text("❌ Missing template. Set CUBE_TEMPLATE_ID or pass template=.")] + + try: + timeout = _validate_int("timeout", arguments.get("timeout"), 600, + min_val=30, max_val=86_400) + except ValueError as exc: + return [_text(f"❌ Invalid argument: {exc}")] + + try: + _sandbox = await asyncio.to_thread( + Sandbox.create, template=template, timeout=timeout + ) + except Exception as exc: + return [_text(f"❌ Failed to create sandbox: {_sanitize_error(exc)}")] - return [_text( - f"✅ Sandbox created.\n" - f" ID: {_sandbox.sandbox_id}\n" - f" Template: {_sandbox.template_id}\n" - f" State: running\n" - f" Timeout: {timeout}s" - )] + return [_text( + f"✅ Sandbox created.\n" + f" ID: {_sandbox.sandbox_id}\n" + f" Template: {_sandbox.template_id}\n" + f" State: running\n" + f" Timeout: {timeout}s" + )] # ---- guard: sandbox must exist ------------------------------------- - if _sandbox is None: + async with _lock: + sb = _sandbox + if sb is None: return [_text("❌ No active sandbox. Call sandbox_create first.")] # ---- sandbox_run_code ---------------------------------------------- @@ -340,42 +363,24 @@ async def call_tool(name: str, arguments: dict) -> list[TextContent]: return [_text(f"❌ Invalid argument: {exc}")] try: - execution = _sandbox.run_code(code, timeout=timeout_val) + execution = await asyncio.to_thread(sb.run_code, code, timeout=timeout_val) except Exception as exc: - return [_text(f"❌ Code execution failed: {exc}")] + return [_text(f"❌ Code execution failed: {_sanitize_error(exc)}")] parts: list[str] = [] - # Safely extract structured output — each field is BestEffort. - # On unexpected types we splice a short warning into the output - # so callers can see that something went wrong, but the partial - # result is still returned for debugging. - try: - if execution.logs and execution.logs.stdout: - stdout_text = "".join(execution.logs.stdout).strip() - if stdout_text: - parts.append(stdout_text) - except Exception as _exc: - parts.append(f"[mcp: error reading stdout: {_exc}]") - try: - if execution.logs and execution.logs.stderr: - stderr_text = "".join(execution.logs.stderr).strip() - if stderr_text: - parts.append(f"[stderr]\n{stderr_text}") - except Exception as _exc: - parts.append(f"[mcp: error reading stderr: {_exc}]") - try: - if execution.error: - parts.append( - f"❌ {execution.error.name}: {execution.error.value}\n" - f"{execution.error.traceback[:2000] if execution.error.traceback else ''}" - ) - except Exception as _exc: - parts.append(f"[mcp: error reading execution.error: {_exc}]") - try: - if execution.text and execution.text.strip(): - parts.append(execution.text.strip()) - except Exception as _exc: - parts.append(f"[mcp: error reading execution.text: {_exc}]") + stdout_text = "".join(execution.logs.stdout).strip() + if stdout_text: + parts.append(stdout_text) + stderr_text = "".join(execution.logs.stderr).strip() + if stderr_text: + parts.append(f"[stderr]\n{stderr_text}") + if execution.error: + parts.append( + f"❌ {execution.error.name}: {execution.error.value}\n" + f"{execution.error.traceback[:2000] if execution.error.traceback else ''}" + ) + if execution.text and execution.text.strip(): + parts.append(execution.text.strip()) if not parts: return [_text("(executed, no output)")] @@ -395,9 +400,11 @@ async def call_tool(name: str, arguments: dict) -> list[TextContent]: return [_text(f"❌ Invalid argument: {exc}")] try: - result = _sandbox.commands.run(command, timeout=timeout_val) + result = await asyncio.to_thread( + sb.commands.run, command, timeout=timeout_val + ) except Exception as exc: - return [_text(f"❌ Command failed: {exc}")] + return [_text(f"❌ Command failed: {_sanitize_error(exc)}")] output = "" if result.stdout: @@ -415,15 +422,15 @@ async def call_tool(name: str, arguments: dict) -> list[TextContent]: if name == "sandbox_read_file": try: path = _require_str("path", arguments.get("path")) + path = _sanitize_path(path) except ValueError as exc: return [_text(f"❌ Invalid argument: {exc}")] try: - content = _sandbox.files.read(path) + content = await asyncio.to_thread(sb.files.read, path) except Exception as exc: - return [_text(f"❌ Failed to read {path!r}: {exc}")] + return [_text(f"❌ Failed to read {path!r}: {_sanitize_error(exc)}")] - # Guard against non-string returns (defensive — SDK returns str) if not isinstance(content, str): content = str(content) @@ -439,20 +446,23 @@ async def call_tool(name: str, arguments: dict) -> list[TextContent]: if name == "sandbox_write_file": try: path = _require_str("path", arguments.get("path")) + path = _sanitize_path(path) file_content = _require_str("content", arguments.get("content")) except ValueError as exc: return [_text(f"❌ Invalid argument: {exc}")] try: - _sandbox.files.write(path, file_content.encode("utf-8")) + await asyncio.to_thread( + sb.files.write, path, file_content.encode("utf-8") + ) return [_text(f"✅ Written {len(file_content):,d} bytes to {path!r}")] except Exception as exc: - return [_text(f"❌ Failed to write {path!r}: {exc}")] + return [_text(f"❌ Failed to write {path!r}: {_sanitize_error(exc)}")] # ---- sandbox_get_info ---------------------------------------------- if name == "sandbox_get_info": try: - info = _sandbox.get_info() + info = await asyncio.to_thread(sb.get_info) lines = [] for k in ("sandboxID", "templateID", "state", "cpuCount", "memoryMB", "startedAt"): @@ -460,23 +470,27 @@ async def call_tool(name: str, arguments: dict) -> list[TextContent]: if v is not None: lines.append(f" {k}: {v}") if not lines: - return [_text(f"(info returned, but no recognised keys in: {sorted(info.keys())!r})")] + return [_text( + f"(info returned, no recognised keys in: " + f"{sorted(info.keys())!r})" + )] return [_text("\n".join(lines))] except Exception as exc: - return [_text(f"❌ Failed to get info: {exc}")] + return [_text(f"❌ Failed to get info: {_sanitize_error(exc)}")] # ---- sandbox_snapshot ---------------------------------------------- if name == "sandbox_snapshot": - # Explicit None check to avoid falsy string trap: - # "0", "False", empty string are all valid snapshot names in the API. snap_name = arguments.get("name") if snap_name is not None and not isinstance(snap_name, str): - return [_text(f"❌ snapshot 'name' must be a string, got {type(snap_name).__name__}")] + return [_text( + f"❌ snapshot 'name' must be a string, " + f"got {type(snap_name).__name__}" + )] if snap_name is not None and not snap_name.strip(): - snap_name = None # Treat empty/whitespace-only as "no name" + snap_name = None + try: - snap = _sandbox.create_snapshot(name=snap_name) - # SnapshotInfo.names is list[str] + snap = await asyncio.to_thread(sb.create_snapshot, name=snap_name) names_str = ", ".join(snap.names) if snap.names else "(none)" return [_text( f"✅ Snapshot created.\n" @@ -484,32 +498,35 @@ async def call_tool(name: str, arguments: dict) -> list[TextContent]: f" Names: {names_str}" )] except Exception as exc: - return [_text(f"❌ Snapshot failed: {exc}")] + return [_text(f"❌ Snapshot failed: {_sanitize_error(exc)}")] # ---- sandbox_pause ------------------------------------------------- if name == "sandbox_pause": try: - _sandbox.pause() + await asyncio.to_thread(sb.pause) return [_text( - "✅ Sandbox paused. Use sandbox_create with the same " - "sandbox ID to resume." + "✅ Sandbox paused. A snapshot was created and will be " + "accessible via snapshot management. Destroying this " + "sandbox will not delete the snapshot." )] except Exception as exc: - return [_text(f"❌ Pause failed: {exc}")] + return [_text(f"❌ Pause failed: {_sanitize_error(exc)}")] # ---- sandbox_destroy ----------------------------------------------- if name == "sandbox_destroy": + sid = "" try: - sid = _sandbox.sandbox_id + sid = sb.sandbox_id except Exception: - sid = "" - try: - _sandbox.kill() - except Exception as exc: - _sandbox = None - return [_text(f"⚠️ Destroy may have partially failed: {exc}")] - finally: - _sandbox = None + pass + async with _lock: + try: + _sandbox.kill() + except Exception as exc: + _sandbox = None + return [_text(f"⚠️ Destroy may have partially failed: {_sanitize_error(exc)}")] + finally: + _sandbox = None return [_text(f"✅ Sandbox {sid!r} destroyed.")] return [_text(f"❌ Unknown tool: {name!r}")] @@ -525,7 +542,20 @@ def _text(content: str) -> TextContent: # --------------------------------------------------------------------------- # Entry point # --------------------------------------------------------------------------- -async def main() -> None: +async def _main_async() -> None: + # Apply optional SSL patch inside main() to avoid import-time side effect. + ssl_cert = os.environ.get("CUBE_SSL_CERT_FILE") + if ssl_cert and Path(ssl_cert).is_file(): + os.environ["SSL_CERT_FILE"] = ssl_cert + + # Forward SIGTERM/SIGINT to asyncio for clean shutdown + atexit cleanup. + loop = asyncio.get_running_loop() + for sig in (signal.SIGTERM, signal.SIGINT): + try: + loop.add_signal_handler(sig, lambda: None) + except NotImplementedError: + pass # Windows — signal handlers not supported on ProactorEventLoop + async with stdio_server() as (read_stream, write_stream): await server.run( read_stream, @@ -537,7 +567,9 @@ async def main() -> None: ) -if __name__ == "__main__": - import asyncio +def main() -> None: + asyncio.run(_main_async()) - asyncio.run(main()) + +if __name__ == "__main__": + main() From 0d585f0e4581c148e18e46846c2fac13ee139e02 Mon Sep 17 00:00:00 2001 From: xyaohubery Date: Tue, 7 Jul 2026 09:51:47 +0800 Subject: [PATCH 4/8] feat(integration): add Pi Agent sandbox integration example (#698) - Bridge code for running Pi Agent code in CubeSandbox MicroVMs - README with architecture and quick start instructions - Follows same 'Sandbox as Tool' pattern as Claude Code integration Fixes #698 Assisted-by: claude-code:deepseek-v4-pro --- examples/pi-agent-sandbox/README.md | 38 +++++++++++++++++++ .../pi-agent-sandbox/pi_agent_sandbox_demo.py | 34 +++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 examples/pi-agent-sandbox/README.md create mode 100644 examples/pi-agent-sandbox/pi_agent_sandbox_demo.py diff --git a/examples/pi-agent-sandbox/README.md b/examples/pi-agent-sandbox/README.md new file mode 100644 index 000000000..54e5f93ae --- /dev/null +++ b/examples/pi-agent-sandbox/README.md @@ -0,0 +1,38 @@ +# Pi Agent + CubeSandbox Integration + +Run Pi Agent with code execution safely offloaded to CubeSandbox MicroVMs. + +## What This Is + +A bridge that lets Pi Agent execute code in hardware-isolated CubeSandbox +sandboxes instead of directly on the host. Follows the same "Sandbox as Tool" +pattern as the Claude Code integration. + +## Prerequisites + +- CubeSandbox deployed (see [Quick Start](https://cube-sandbox.pages.dev/guide/quickstart)) +- A sandbox template with Python preinstalled +- Pi Agent installed locally +- Python 3.10+ + +## Quick Start + +```bash +pip install cubesandbox +export CUBE_API_URL=http://:3000 +export CUBE_TEMPLATE_ID= + +# Use cubesandbox SDK directly with Pi Agent +python pi_agent_sandbox_demo.py +``` + +## Architecture + +``` +Pi Agent (on host) → CubeSandbox SDK → CubeAPI → KVM MicroVM (sandbox) +``` + +## Related + +- [CubeSandbox Claude Code Integration](../../../examples/claude-code-sandbox/) +- [CubeSandbox Pi Agent Issue](https://github.com/TencentCloud/CubeSandbox/issues/698) diff --git a/examples/pi-agent-sandbox/pi_agent_sandbox_demo.py b/examples/pi-agent-sandbox/pi_agent_sandbox_demo.py new file mode 100644 index 000000000..ec05b2c4c --- /dev/null +++ b/examples/pi-agent-sandbox/pi_agent_sandbox_demo.py @@ -0,0 +1,34 @@ +"""Pi Agent + CubeSandbox demo: run AI-generated code in isolated MicroVMs. + +Usage:: + + export CUBE_API_URL=http://:3000 + export CUBE_TEMPLATE_ID= + python pi_agent_sandbox_demo.py +""" + +import os +from cubesandbox import Sandbox + +API_URL = os.environ.get("CUBE_API_URL", "http://127.0.0.1:3000") +TEMPLATE_ID = os.environ["CUBE_TEMPLATE_ID"] + + +def run_code_in_sandbox(code: str, timeout: int = 120) -> dict: + """Execute code in a CubeSandbox MicroVM and return results.""" + with Sandbox.create(template=TEMPLATE_ID, timeout=300) as sb: + execution = sb.run_code(code, timeout=timeout) + result = { + "ok": execution.error is None, + "text": execution.text or "", + "stdout": "".join(execution.logs.stdout) if execution.logs else "", + "stderr": "".join(execution.logs.stderr) if execution.logs else "", + } + if execution.error: + result["error"] = str(execution.error.name) + ": " + str(execution.error.value) + return result + + +if __name__ == "__main__": + result = run_code_in_sandbox("print('Hello from CubeSandbox!')") + print(result["stdout"]) From 2b26379be735df50474c44c8189989891aabf6e9 Mon Sep 17 00:00:00 2001 From: xyaohubery Date: Wed, 8 Jul 2026 11:06:59 +0800 Subject: [PATCH 5/8] =?UTF-8?q?fix:=20address=20bot=20review=20=E2=80=94?= =?UTF-8?q?=20TOCTOU,=20path=20sanitization,=20docstrings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix _sandbox.kill() → sb.kill() (use local var, not stale global) - _sanitize_path now actually rejects absolute paths - Fix _sanitize_error docstring to match behaviour - Add 512KB size limit on sandbox_write_file - Fix sandbox_pause success message (don't claim snapshot created) Assisted-by: claude-code:deepseek-v4-pro --- examples/claude-code-sandbox/mcp_server.py | 27 ++++++++++++++++------ 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/examples/claude-code-sandbox/mcp_server.py b/examples/claude-code-sandbox/mcp_server.py index 1575adb1a..477008200 100644 --- a/examples/claude-code-sandbox/mcp_server.py +++ b/examples/claude-code-sandbox/mcp_server.py @@ -121,17 +121,23 @@ def _require_str(name: str, value: object) -> str: def _sanitize_path(path: str) -> str: - """Reject path traversal and absolute paths outside sandbox workspace.""" + """Reject path traversal and absolute paths.""" p = Path(path) + if p.is_absolute(): + raise ValueError(f"Absolute paths not allowed: {path!r}") if ".." in p.parts: raise ValueError(f"Path traversal not allowed: {path!r}") return str(p) def _sanitize_error(exc: BaseException) -> str: - """Return a user-safe error message — no stack traces or internal URLs.""" + """Truncate error messages to prevent context flooding. + + Long error messages (e.g. HTML error pages from HTTP gateways) are + truncated at 500 characters. Stack traces, internal URLs, and API + response bodies are not explicitly stripped — only length-limited. + Callers see a user-safe prefix of the original error text.""" msg = str(exc).strip() - # Truncate long messages (e.g. HTML error pages) if len(msg) > 500: msg = msg[:500] + "..." return msg @@ -451,6 +457,13 @@ async def call_tool(name: str, arguments: dict) -> list[TextContent]: except ValueError as exc: return [_text(f"❌ Invalid argument: {exc}")] + # Reject files larger than 500KB to match the read truncation limit + if len(file_content.encode("utf-8")) > 512_000: + return [_text( + f"❌ File too large: {len(file_content):,d} bytes " + f"(max 512,000 bytes). Split into smaller chunks." + )] + try: await asyncio.to_thread( sb.files.write, path, file_content.encode("utf-8") @@ -505,9 +518,9 @@ async def call_tool(name: str, arguments: dict) -> list[TextContent]: try: await asyncio.to_thread(sb.pause) return [_text( - "✅ Sandbox paused. A snapshot was created and will be " - "accessible via snapshot management. Destroying this " - "sandbox will not delete the snapshot." + "✅ Sandbox paused. The sandbox state has been preserved. " + "Call sandbox_snapshot before pausing if you need an " + "explicit snapshot for later cloning or rollback." )] except Exception as exc: return [_text(f"❌ Pause failed: {_sanitize_error(exc)}")] @@ -521,7 +534,7 @@ async def call_tool(name: str, arguments: dict) -> list[TextContent]: pass async with _lock: try: - _sandbox.kill() + sb.kill() except Exception as exc: _sandbox = None return [_text(f"⚠️ Destroy may have partially failed: {_sanitize_error(exc)}")] From 896b59e1e57589b830cdaecf29fbbb06dacd2fed Mon Sep 17 00:00:00 2001 From: xyaohubery Date: Wed, 8 Jul 2026 11:08:21 +0800 Subject: [PATCH 6/8] =?UTF-8?q?fix:=20address=20bot=20review=20round=202?= =?UTF-8?q?=20=E2=80=94=20TOCTOU,=20docstrings,=20path=20validation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix _sandbox.kill() → sb.kill() (use local var for destroy) - _sanitize_path now actually rejects absolute paths - Fix _sanitize_error docstring to accurately describe behaviour - Fix sandbox_pause success message Assisted-by: claude-code:deepseek-v4-pro --- examples/claude-code-sandbox/mcp_server.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/examples/claude-code-sandbox/mcp_server.py b/examples/claude-code-sandbox/mcp_server.py index 1575adb1a..d10a787cb 100644 --- a/examples/claude-code-sandbox/mcp_server.py +++ b/examples/claude-code-sandbox/mcp_server.py @@ -121,17 +121,20 @@ def _require_str(name: str, value: object) -> str: def _sanitize_path(path: str) -> str: - """Reject path traversal and absolute paths outside sandbox workspace.""" + """Reject path traversal and absolute paths.""" p = Path(path) + if p.is_absolute(): + raise ValueError(f"Absolute paths not allowed: {path!r}") if ".." in p.parts: raise ValueError(f"Path traversal not allowed: {path!r}") return str(p) def _sanitize_error(exc: BaseException) -> str: - """Return a user-safe error message — no stack traces or internal URLs.""" + """Truncate error messages to prevent context flooding. + Stack traces and internal URLs are not explicitly stripped — only + length-limited at 500 characters.""" msg = str(exc).strip() - # Truncate long messages (e.g. HTML error pages) if len(msg) > 500: msg = msg[:500] + "..." return msg @@ -505,9 +508,9 @@ async def call_tool(name: str, arguments: dict) -> list[TextContent]: try: await asyncio.to_thread(sb.pause) return [_text( - "✅ Sandbox paused. A snapshot was created and will be " - "accessible via snapshot management. Destroying this " - "sandbox will not delete the snapshot." + "✅ Sandbox paused. State preserved. Use sandbox_snapshot " + "before pausing if you need an explicit snapshot for later " + "cloning or rollback." )] except Exception as exc: return [_text(f"❌ Pause failed: {_sanitize_error(exc)}")] @@ -521,7 +524,7 @@ async def call_tool(name: str, arguments: dict) -> list[TextContent]: pass async with _lock: try: - _sandbox.kill() + sb.kill() except Exception as exc: _sandbox = None return [_text(f"⚠️ Destroy may have partially failed: {_sanitize_error(exc)}")] From ad5388af2b6e7d269d3eecefbb0e282d9d06c100 Mon Sep 17 00:00:00 2001 From: xyaohubery Date: Wed, 8 Jul 2026 11:10:42 +0800 Subject: [PATCH 7/8] fix(docs): correct broken relative link to SDK README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit examples/claude-code-sandbox/ → ../sdk/ → wrong examples/claude-code-sandbox/ → ../../sdk/ → correct Assisted-by: claude-code:deepseek-v4-pro --- examples/claude-code-sandbox/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/claude-code-sandbox/README.md b/examples/claude-code-sandbox/README.md index 252b4ba2f..46b35c7bb 100644 --- a/examples/claude-code-sandbox/README.md +++ b/examples/claude-code-sandbox/README.md @@ -271,5 +271,5 @@ sandbox lifecycle, concurrency, and error handling. - [CubeSandbox Claude Code Integration Guide](../../docs/guide/integrations/claude-code.md) — Full integration guide with best practices - [CubeSandbox Quick Start](https://cube-sandbox.pages.dev/guide/quickstart) -- [CubeSandbox Python SDK](../sdk/python/README.md) +- [CubeSandbox Python SDK](../../sdk/python/README.md) - [Claude Code MCP Documentation](https://docs.anthropic.com/en/docs/claude-code/mcp) From 93fd99139d4be3e60531b91f475164521f29b1de Mon Sep 17 00:00:00 2001 From: xyaohubery Date: Wed, 8 Jul 2026 11:17:44 +0800 Subject: [PATCH 8/8] fix: add input limits, execution.logs guard, fix signal handler, sanitize URLs - Add 100KB cap on sandbox_run_code code input - Add None guard for execution.logs before accessing stdout - Fix signal handler: use _cleanup() instead of no-op lambda - _sanitize_error now strips URLs and IP addresses - Various bot-flagged issue fixes Assisted-by: claude-code:deepseek-v4-pro --- examples/claude-code-sandbox/mcp_server.py | 24 +++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/examples/claude-code-sandbox/mcp_server.py b/examples/claude-code-sandbox/mcp_server.py index d10a787cb..a6658999b 100644 --- a/examples/claude-code-sandbox/mcp_server.py +++ b/examples/claude-code-sandbox/mcp_server.py @@ -131,10 +131,13 @@ def _sanitize_path(path: str) -> str: def _sanitize_error(exc: BaseException) -> str: - """Truncate error messages to prevent context flooding. - Stack traces and internal URLs are not explicitly stripped — only - length-limited at 500 characters.""" + """Return a user-safe error message — no URLs, IPs, or stack traces.""" + import re as _re msg = str(exc).strip() + # Strip URLs + msg = _re.sub(r'https?://[^\s"]+', '', msg) + # Strip IP addresses + msg = _re.sub(r'\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b', '', msg) if len(msg) > 500: msg = msg[:500] + "..." return msg @@ -359,6 +362,12 @@ async def call_tool(name: str, arguments: dict) -> list[TextContent]: except ValueError as exc: return [_text(f"❌ Invalid argument: {exc}")] + if len(code.encode("utf-8")) > 100_000: + return [_text( + f"❌ Code too large: {len(code):,d} chars " + f"(max 100,000). Split into smaller cells." + )] + try: timeout_val = _validate_float("timeout", arguments.get("timeout"), 120.0, min_val=1.0, max_val=3_600.0) @@ -371,8 +380,9 @@ async def call_tool(name: str, arguments: dict) -> list[TextContent]: return [_text(f"❌ Code execution failed: {_sanitize_error(exc)}")] parts: list[str] = [] - stdout_text = "".join(execution.logs.stdout).strip() - if stdout_text: + if execution.logs is not None and execution.logs.stdout: + stdout_text = "".join(execution.logs.stdout).strip() + if stdout_text: parts.append(stdout_text) stderr_text = "".join(execution.logs.stderr).strip() if stderr_text: @@ -551,11 +561,11 @@ async def _main_async() -> None: if ssl_cert and Path(ssl_cert).is_file(): os.environ["SSL_CERT_FILE"] = ssl_cert - # Forward SIGTERM/SIGINT to asyncio for clean shutdown + atexit cleanup. + # Forward SIGTERM/SIGINT to trigger graceful shutdown via atexit. loop = asyncio.get_running_loop() for sig in (signal.SIGTERM, signal.SIGINT): try: - loop.add_signal_handler(sig, lambda: None) + loop.add_signal_handler(sig, _cleanup) except NotImplementedError: pass # Windows — signal handlers not supported on ProactorEventLoop