MaxKB 沙盒方案选型与改造报告
一、MaxKB 当前沙盒方案调研
1.1 当前沙盒架构
MaxKB 当前采用的是 三层隔离方案,而非 Docker 容器:
| 层面 | 技术 | 核心文件 |
|---|---|---|
| C 拦截层 | LD_PRELOAD 注入 sandbox.so,拦截危险系统调用 | installer/sandbox.c |
| 用户隔离层 | setuid/setgid 降权到 sandbox 用户 | installer/Dockerfile-base |
| Python 资源限制层 | RLIMIT_AS 内存限制 + sched_setaffinity CPU 限制 + 超时控制 | apps/common/utils/tool_code.py |
所有用户代码(工具、技能、本地 MCP)都在 MaxKB 同一进程/容器内,以 sandbox 用户身份通过子进程执行。
1.2 核心代码入口
- 工具/工具库代码执行:
ToolExecutor.exec_code()—apps/common/utils/tool_code.py:101 - 底层子进程执行:
ToolExecutor._exec()—apps/common/utils/tool_code.py:351- 注入
LD_PRELOAD=sandbox.so,通过preexec_fn设置资源限制
- 注入
- 本地 MCP Server 生成:
ToolExecutor.get_tool_mcp_config()—apps/common/utils/tool_code.py:323- 将自定义 Python 工具动态转成 FastMCP server,通过 stdio 通信,同样注入
sandbox.so
- 将自定义 Python 工具动态转成 FastMCP server,通过 stdio 通信,同样注入
- Deep Agent Shell 执行:
SandboxShellBackend.execute()—apps/application/flow/backend/sandbox_shell.py:56- 用
gosu sandbox切换用户 + 注入sandbox.so
- 用
- 技能运行: ZIP 解压到
/tmp/<chat_id>/skills/,由 deep agent 在SandboxShellBackend中通过 shell 访问 - 配置项:
apps/maxkb/conf.py,通过MAXKB_前缀环境变量配置(如MAXKB_SANDBOX=1)
1.3 当前方案的局限性
| 问题 | 说明 |
|---|---|
| 无法运行二进制文件 | sandbox.so 默认拦截 execve/mmap(PROT_EXEC)/mprotect(PROT_EXEC),二进制 skill 无法运行 |
| 隔离强度弱 | LD_PRELOAD 属于用户态 hook,可通过静态链接、直接 syscall、/proc/self/mem 等方式绕过 |
| 共享内核 | 所有沙盒进程与宿主共享同一 Linux 内核,无内核级隔离 |
| 沙盒可被禁用 | is_sandbox_user() 检查可被同名用户欺骗;.sandbox.conf 可被运行时修改 |
| 资源限制有限 | 仅内存/CPU/超时,无文件系统隔离(依赖用户权限),无网络命名空间 |
| 仅支持 Python | 工具代码通过 exec() 执行 Python,不支持其他语言 |
二、候选沙盒方案对比
2.1 六个方案总览
| 方案 | 隔离机制 | 二进制支持 | Python SDK | 部署复杂度 | 本地/远程 |
|---|---|---|---|---|---|
| Clampdown | 容器(Landlock+Seccomp) | 有(exec白名单) | 无 | 中(需Podman+Linux 6.2+) | 本地CLI |
| Daytona | 微VM(独立内核) | 完全支持 | 有(完善) | 低(托管)~高(自建) | 远程API |
| E2B | 微VM(Firecracker) | 完全支持 | 有(完善) | 低(托管)~极高(自建仅AWS/GCP) | 远程API |
| Firecracker | 微VM(KVM裸金属) | 间接(需自备kernel+rootfs) | 无 | 极高 | 本地(底层) |
| Firecracker-containerd | 容器+微VM | 完全支持(OCI) | 无 | 高(需特殊containerd) | 本地(底层) |
| Microsandbox | 微VM(libkrun) | 完全支持(OCI) | 有(PyO3) | 低(一行安装) | 本地嵌入式 |
2.2 按需求维度评估
需求 1:支持运行二进制文件
- 通过:Daytona、E2B、Firecracker-containerd、Microsandbox(完整 VM/OCI 镜像,任意二进制可运行)
- 受限通过:Clampdown(需 SHA-256 白名单)、Firecracker(需自备 rootfs)
- 不满足:无
需求 2:安全隔离性强(优于 LD_PRELOAD)
- 最强:Firecracker、Firecracker-containerd、Microsandbox、Daytona、E2B(硬件级微 VM 隔离,独立内核)
- 强:Clampdown(Landlock+Seccomp,仍共享内核但加固程度高)
- 当前方案:LD_PRELOAD(最弱)
需求 3:改造难度低(有 Python SDK 或 HTTP API)
- 最佳:Daytona、E2B、Microsandbox(均有成熟 Python SDK)
- 无 SDK:Clampdown、Firecracker、Firecracker-containerd(需自行封装调用层)
需求 4:部署简便
- 最简便:Microsandbox(
curl | sh一行安装,无 daemon,无服务器) - 简便(托管模式):Daytona、E2B(注册即用,但需联网)
- 复杂:Firecracker、Firecracker-containerd、Clampdown
三、推荐方案:Microsandbox(首选)+ Daytona(远程备选)
3.1 推荐理由
首选 Microsandbox,理由如下:
- 本地嵌入式,架构契合度高:Microsandbox 以子进程方式启动微 VM,无需长期运行的 daemon 或服务器,与 MaxKB 当前的
subprocess.run()调用模式天然契合。 - Python SDK 直接可用:通过
pip install microsandbox安装,在 MaxKB 代码中直接from microsandbox import Sandbox即可调用。 - 部署极简:一行命令安装运行时,首次使用自动拉取 OCI 镜像并缓存,无需运维 Docker/Kubernetes。
- 硬件级隔离:基于 libkrun 的微 VM,每个沙盒有独立 Linux 内核,安全隔离性远超 LD_PRELOAD。
- 完整二进制支持:通过 OCI 镜像运行任意二进制文件,完美满足不开源 skill 的运行需求。
- 毫秒级启动:平均启动时间 < 100ms,对用户体验影响极小。
- 网络安全内置:默认阻止私有 IP、云元数据端点,DNS rebinding 防护,无需额外配置。
- License 友好:Apache-2.0。
备选 Daytona(适用于需要远程/云端部署的场景):
- 完善的 REST API + 多语言 SDK:适合 MaxKB 通过远程调用方式使用。
- 托管服务可用:注册即用,零运维成本。
- 功能最全面:支持快照、卷、SSH、Web Terminal、LSP 等。
- 适合规模化部署:有完整的 Control Plane / Compute Plane 架构。
3.2 各方案淘汰原因
| 方案 | 淘汰原因 |
|---|---|
| Clampdown | 无 Python SDK/HTTP API,纯 CLI 工具;专为 Claude Code/Codex 等 AI 编码代理设计,与 MaxKB 场景不匹配;需 Linux 6.2+ 内核 |
| E2B | 自托管仅支持 AWS/GCP,不支持普通 Linux 机器;自建部署复杂度极高(需 Terraform 编排基础设施) |
| Firecracker | 纯底层 VMM,无 SDK,需自行准备 kernel+rootfs,面向基础设施开发者而非应用开发者 |
| Firecracker-containerd | 无 Python SDK,需编译特殊版 containerd,部署复杂度高;项目更新节奏慢 |
四、MaxKB 改造方案
4.1 整体架构
改造前:
┌─────────────────────────────────────────────────┐
│ MaxKB 容器 │
│ ┌──────────┐ ┌──────────────────────────┐ │
│ │ MaxKB │ │ subprocess (python) │ │
│ │ 主进程 │──▶│ LD_PRELOAD=sandbox.so │ │
│ │ │ │ setuid(sandbox) │ │
│ └──────────┘ └──────────────────────────┘ │
│ 共享内核,用户态隔离 │
└─────────────────────────────────────────────────┘
改造后(Microsandbox 方案):
┌─────────────────────────────────────────────────┐
│ MaxKB 主机 │
│ ┌──────────┐ ┌──────────────────────────┐ │
│ │ MaxKB │ │ Microsandbox 微 VM │ │
│ │ 主进程 │──▶│ (独立内核, libkrun) │ │
│ │ Python │ │ ┌────────────────────┐ │ │
│ │ SDK │ │ │ OCI 镜像环境 │ │ │
│ │ │ │ │ Python/二进制/MCP │ │ │
│ └──────────┘ │ └────────────────────┘ │ │
│ └──────────────────────────┘ │
│ 硬件级隔离,独立内核 │
└─────────────────────────────────────────────────┘
4.2 配置层改造
在 apps/maxkb/conf.py 中新增沙盒后端配置项:
# 新增配置项(通过 MAXKB_ 前缀环境变量覆盖)
SANDBOX_BACKEND = CONFIG.get("SANDBOX_BACKEND", "legacy")
# 可选值: "legacy"(原sandbox.so) | "microsandbox" | "daytona"
# Microsandbox 配置
MICROSANDBOX_IMAGE = CONFIG.get("MICROSANDBOX_IMAGE", "python:3.11-slim")
MICROSANDBOX_CPUS = int(CONFIG.get("MICROSANDBOX_CPUS", "1"))
MICROSANDBOX_MEMORY = int(CONFIG.get("MICROSANDBOX_MEMORY", "512")) # MB
MICROSANDBOX_TIMEOUT = int(CONFIG.get("MICROSANDBOX_TIMEOUT", "3600"))
# Daytona 配置(远程模式)
DAYTONA_API_KEY = CONFIG.get("DAYTONA_API_KEY", "")
DAYTONA_API_URL = CONFIG.get("DAYTONA_API_URL", "https://app.daytona.io/api")
DAYTONA_TIMEOUT = int(CONFIG.get("DAYTONA_TIMEOUT", "3600"))
4.3 新增沙盒后端抽象层
新建 apps/common/utils/sandbox_backend.py,定义统一的沙盒后端接口:
from abc import ABC, abstractmethod
from dataclasses import dataclass
@dataclass
class ExecResult:
returncode: int
stdout: str
stderr: str
class SandboxBackend(ABC):
"""沙盒后端抽象基类"""
@abstractmethod
def exec_code(self, code: str, function_name: str, params: dict) -> dict:
"""执行 Python 代码并调用指定函数,返回结果"""
...
@abstractmethod
def exec_command(self, command: str, timeout: int = None) -> ExecResult:
"""在沙盒中执行 shell 命令"""
...
@abstractmethod
def upload_files(self, local_path: str, remote_path: str) -> None:
"""上传文件到沙盒(用于 skill 文件部署)"""
...
@abstractmethod
def cleanup(self) -> None:
"""清理沙盒资源"""
...
4.4 实现 Microsandbox 后端
新建 apps/common/utils/sandbox_microsandbox.py:
import asyncio
import json
import os
import tempfile
from common.utils.sandbox_backend import SandboxBackend, ExecResult
from common.utils.logger import maxkb_logger
from maxkb.const import CONFIG
class MicrosandboxBackend(SandboxBackend):
def __init__(self):
from microsandbox import Sandbox
self._Sandbox = Sandbox
self._image = CONFIG.get("MICROSANDBOX_IMAGE", "python:3.11-slim")
self._cpus = int(CONFIG.get("MICROSANDBOX_CPUS", "1"))
self._memory = int(CONFIG.get("MICROSANDBOX_MEMORY", "512"))
self._timeout = int(CONFIG.get("MICROSANDBOX_TIMEOUT", "3600"))
self._sandbox = None
async def _ensure_sandbox(self):
"""延迟创建沙盒实例(每个执行上下文一个)"""
if self._sandbox is None:
self._sandbox = await self._Sandbox.create(
name=f"maxkb-{os.getpid()}",
image=self._image,
cpus=self._cpus,
memory=self._memory,
)
return self._sandbox
async def exec_code(self, code, function_name, params):
"""在微 VM 中执行 Python 代码"""
sandbox = await self._ensure_sandbox()
# 将代码和参数写入临时文件,在沙盒中执行
wrapper_code = self._wrap_code(code, function_name, params)
result = await sandbox.exec(
"python3", ["-c", wrapper_code],
timeout=self._timeout
)
return json.loads(result.stdout_text)
async def exec_command(self, command, timeout=None):
"""在微 VM 中执行 shell 命令"""
sandbox = await self._ensure_sandbox()
result = await sandbox.exec(
"sh", ["-c", command],
timeout=timeout or self._timeout
)
return ExecResult(
returncode=result.exit_code,
stdout=result.stdout_text,
stderr=result.stderr_text,
)
async def upload_files(self, local_path, remote_path):
"""上传文件到微 VM(部署 skill 文件)"""
sandbox = await self._ensure_sandbox()
await sandbox.fs.upload(local_path, remote_path)
async def cleanup(self):
"""销毁微 VM"""
if self._sandbox:
await self._sandbox.stop()
self._sandbox = None
def _wrap_code(self, code, function_name, params):
"""生成包装代码(与原 exec_code 逻辑类似,但不做 setuid/sandbox.so)"""
return f"""
import json, sys
params = json.loads('''{json.dumps(params)}''')
_locals = {{}}
exec('''{code}''', {{"__builtins__": __builtins__}}, _locals)
result = _locals["{function_name}"](**params)
json.dump({{"code": 200, "msg": "success", "data": result}}, sys.stdout, default=str)
"""
4.5 改造 ToolExecutor(核心改造点)
修改 apps/common/utils/tool_code.py,根据配置选择沙盒后端:
# 文件顶部新增
from common.utils.sandbox_backend import SandboxBackend
_sandbox_backend_type = CONFIG.get("SANDBOX_BACKEND", "legacy")
def _get_sandbox_backend() -> SandboxBackend:
"""根据配置返回沙盒后端实例"""
if _sandbox_backend_type == "microsandbox":
from common.utils.sandbox_microsandbox import MicrosandboxBackend
return MicrosandboxBackend()
elif _sandbox_backend_type == "daytona":
from common.utils.sandbox_daytona import DaytonaBackend
return DaytonaBackend()
else:
return None # 使用原有 legacy 方案
修改 exec_code() 方法(第 101-155 行),当后端为微 VM 时走新路径:
class ToolExecutor:
# ...
def exec_code(self, code_str, keywords, function_name=None):
# 新增:如果使用微 VM 沙盒,走新执行路径
if _sandbox_backend_type in ("microsandbox", "daytona"):
return self._exec_code_in_microvm(code_str, keywords, function_name)
# ---- 以下为原有 legacy 逻辑,保持不变 ----
_id = str(uuid.uuid7())
# ... 原有代码 ...
def _exec_code_in_microvm(self, code_str, keywords, function_name):
"""在微 VM 沙盒中执行代码"""
import asyncio
backend = _get_sandbox_backend()
try:
result = asyncio.get_event_loop().run_until_complete(
backend.exec_code(code_str, function_name, keywords)
)
if result.get("code") == 200:
return result.get("data")
raise Exception(result.get("msg"))
finally:
asyncio.get_event_loop().run_until_complete(backend.cleanup())
4.6 改造 SandboxShellBackend(Deep Agent Shell 执行)
修改 apps/application/flow/backend/sandbox_shell.py,当使用微 VM 时,将 shell 命令转发到沙盒中执行:
class SandboxShellBackend(LocalShellBackend):
def __init__(self, root_dir: str, **kwargs):
self._use_microvm = _sandbox_backend_type in ("microsandbox", "daytona")
if self._use_microvm:
# 微 VM 模式下不需要 LD_PRELOAD 和 gosu,隔离由微 VM 保证
kwargs.setdefault("env", {})
else:
# 原有逻辑
...
super().__init__(root_dir=root_dir, **kwargs)
def execute(self, command, *, timeout=None):
if self._use_microvm:
return self._execute_in_microvm(command, timeout)
# ---- 以下为原有 legacy 逻辑 ----
if self.virtual_mode:
command = self._translate_virtual_paths(command)
if _enable_sandbox:
command = (
"env -i LD_PRELOAD=/opt/maxkb-app/sandbox/lib/sandbox.so "
f'PATH="${{PATH}}" PYTHONPATH="${{PYTHONPATH}}" gosu {_run_user} {command}'
)
return super().execute(command=command, timeout=timeout)
def _execute_in_microvm(self, command, timeout):
"""在微 VM 中执行 shell 命令"""
import asyncio
from common.utils.sandbox_microsandbox import MicrosandboxBackend
backend = MicrosandboxBackend()
try:
result = asyncio.get_event_loop().run_until_complete(
backend.exec_command(command, timeout)
)
from deepagents.backends.protocol import ExecuteResponse
return ExecuteResponse(
stdout=result.stdout,
stderr=result.stderr,
exit_code=result.returncode,
)
finally:
asyncio.get_event_loop().run_until_complete(backend.cleanup())
4.7 改造本地 MCP Server 生成逻辑
修改 get_tool_mcp_config()(tool_code.py:323),当使用微 VM 时,MCP server 在微 VM 内以网络端口方式运行,而非 stdio:
def get_tool_mcp_config(self, tool, params):
if _sandbox_backend_type in ("microsandbox", "daytona"):
return self._get_tool_mcp_config_microvm(tool, params)
# ---- 以下为原有 legacy 逻辑 ----
_code = self.generate_mcp_server_code(tool.code, params, tool.name, tool.desc, str(tool.id))
# ... 原有代码 ...
def _get_tool_mcp_config_microvm(self, tool, params):
"""在微 VM 中运行 MCP server,暴露 HTTP 端点"""
code = self._generate_mcp_server_code_http(tool.code, params, tool.name, tool.desc, str(tool.id))
# 生成在微 VM 内以 streamable_http 方式运行的 MCP server 代码
# 返回指向微 VM 的 HTTP MCP 配置
import asyncio
backend = _get_sandbox_backend()
# 在微 VM 中启动 MCP server,获取端口映射
port = asyncio.get_event_loop().run_until_complete(
backend.start_mcp_server(code)
)
return {
"url": f"http://127.0.0.1:{port}/mcp",
"transport": "streamable_http",
}
同时需修改 _generate_mcp_server_code(),将 transport="stdio" 改为 transport="streamable_http" 并绑定到 0.0.0.0。
4.8 改造 Skill 运行逻辑
修改 application/flow/tools.py 中的 _initialize_skills()(第 398-441 行):
async def _initialize_skills(mcp_servers, temp_dir):
skills_dir = os.path.join(temp_dir, "skills")
mcp_config = json.loads(mcp_servers)
if _sandbox_backend_type in ("microsandbox", "daytona"):
# 微 VM 模式:将 skill 文件上传到微 VM 中
if "skills" in mcp_config:
skill_file_items = mcp_config.pop("skills")
backend = _get_sandbox_backend()
for skill_file in skill_file_items:
file = await QuerySet(File).filter(id=skill_file["file_id"]).first()
file_bytes = await file.get_bytes()
# 写入本地临时文件后上传到微 VM
local_zip = os.path.join(temp_dir, f"{skill_file['file_id']}.zip")
with open(local_zip, "wb") as f:
f.write(file_bytes)
await backend.upload_files(local_zip, f"/skills/{skill_file['file_id']}.zip")
# 在微 VM 内解压
await backend.exec_command(f"cd /skills && unzip {skill_file['file_id']}.zip")
client = MultiServerMCPClient(mcp_config)
return client, backend
# ---- 以下为原有 legacy 逻辑 ----
# ... 原有解压逻辑 ...
4.9 Dockerfile 改造
修改 installer/Dockerfile,新增 Microsandbox 运行时安装步骤:
# 安装 Microsandbox 运行时(如果启用微 VM 沙盒)
RUN if [ "$MAXKB_SANDBOX_BACKEND" = "microsandbox" ]; then \
curl -fsSL https://install.microsandbox.dev | sh && \
pip install microsandbox; \
fi
# 保留原有 sandbox.so 编译(向后兼容)
RUN gcc -shared -fPIC -o ${MAXKB_SANDBOX_HOME}/lib/sandbox.so /opt/maxkb-app/installer/sandbox.c -ldl
同时需要在 Docker 运行参数中添加 KVM 设备映射(微 VM 需要 KVM 支持):
docker run --device /dev/kvm -e MAXKB_SANDBOX_BACKEND=microsandbox ...
4.10 改造文件清单
| 文件 | 改造内容 | 改动量 |
|---|---|---|
apps/maxkb/conf.py | 新增 SANDBOX_BACKEND 等配置项 | 小 |
apps/common/utils/sandbox_backend.py | 新建 沙盒后端抽象接口 | 小 |
apps/common/utils/sandbox_microsandbox.py | 新建 Microsandbox 后端实现 | 中 |
apps/common/utils/sandbox_daytona.py | 新建 Daytona 远程后端实现(可选) | 中 |
apps/common/utils/tool_code.py | 改造 exec_code()/_exec()/get_tool_mcp_config(),根据后端类型分支 | 中 |
apps/application/flow/backend/sandbox_shell.py | 改造 execute(),转发命令到微 VM | 小 |
apps/application/flow/tools.py | 改造 _initialize_skills(),文件上传到微 VM | 小 |
installer/Dockerfile | 新增 Microsandbox 运行时安装 | 小 |
installer/Dockerfile-base | 条件化 sandbox 用户创建 | 小 |
pyproject.toml | 新增 microsandbox 依赖 | 小 |
五、改造实施计划
阶段一:基础设施搭建(1-2 周)
- 新建
sandbox_backend.py抽象接口和sandbox_microsandbox.py实现 - 在
conf.py中新增配置项 - 编写 Microsandbox 后端的单元测试
阶段二:核心执行路径改造(1-2 周)
- 改造
ToolExecutor.exec_code()— Python 工具代码执行 - 改造
SandboxShellBackend.execute()— Deep Agent shell 执行 - 改造
get_tool_mcp_config()— 本地 MCP server - 改造
_initialize_skills()— Skill 文件部署
阶段三:Dockerfile 和部署改造(3-5 天)
- 修改 Dockerfile,支持条件安装 Microsandbox 运行时
- 添加 KVM 设备映射到 docker-compose / 部署文档
- 准备预构建 OCI 镜像(包含常用 Python 包)
阶段四:测试与验证(1 周)
- 功能测试:工具执行、MCP 调用、Skill 运行、二进制 skill 运行
- 安全测试:网络隔离验证、文件系统隔离验证、逃逸尝试
- 性能测试:启动延迟、执行性能对比
阶段五:Daytona 远程模式(可选,1-2 周)
- 实现
sandbox_daytona.py远程后端 - 添加远程 MCP 配置支持
- 测试云端托管模式
六、风险与注意事项
| 风险 | 影响 | 缓解措施 |
|---|---|---|
| KVM 不可用(某些云环境/虚拟机) | 微 VM 无法启动 | 检测 KVM 可用性,自动回退到 legacy 模式;或使用 Daytona 远程模式 |
| Microsandbox 仍是 beta (v0.5.7) | 稳定性风险 | 保留 legacy 方案作为回退选项;SANDBOX_BACKEND=legacy 可随时切回 |
| 微 VM 启动延迟 | 首次请求变慢(~100ms) | 使用沙盒池预创建微 VM 实例;或保持长连接复用 |
| OCI 镜像体积 | 部署变慢 | 预构建精简镜像(python:3.11-slim + 常用包),分层缓存 |
| stdio MCP → HTTP MCP 改造 | 兼容性 | 确保 MultiServerMCPClient 支持 streamable_http(已支持) |
| Mac 开发环境无 KVM | 本地开发受阻 | Microsandbox 支持 macOS Apple Silicon(基于 libkrun) |
七、结论
推荐 Microsandbox 作为 MaxKB 的下一代沙盒方案:
- 安全隔离性:从 LD_PRELOAD 用户态隔离升级为 libkrun 硬件级微 VM 隔离(独立内核),安全性质变提升
- 二进制支持:通过 OCI 镜像完整支持运行任意二进制文件,满足不开源 skill 的需求
- 改造难度:有原生 Python SDK,核心改造集中在
ToolExecutor和SandboxShellBackend两个入口,改动可控 - 部署友好:一行安装,无 daemon,对现有 Docker 部署仅增加 KVM 设备映射
- 向后兼容:通过
SANDBOX_BACKEND配置开关,可随时在 legacy 和 microsandbox 之间切换
如需远程/云端部署能力,可额外集成 Daytona 作为远程沙盒后端,二者共用统一的 SandboxBackend 接口。