1. 为什么要把一堆工具塞进同一个容器
第一次看到 AIO Sandbox 这个项目名的时候,我脑子里蹦出来的第一个念头是:这不就是把之前散落在各个角落的 Agent 工具链,硬生生捏成一个"全家桶"吗?但仔细扒完它的设计思路之后,我改主意了。这东西解决的是一个非常具体的痛点——Agent 在执行任务时,需要在多个环境之间反复横跳,而每一次跳转都是一次上下文丢失和状态断裂。
举个我自己踩过的真实场景。之前做一个自动化数据采集的 Agent,流程大概是这样的:先用 Playwright 打开网页抓数据,抓完之后要把结果写到本地文件,写文件的过程中可能需要跑一段 Python 脚本做清洗,清洗完再调 VSCode 里的某个插件做格式化,最后通过 MCP 协议把结果推给上游服务。这一套下来,浏览器在一个进程里,Shell 在另一个终端里,文件系统是共享的但权限经常打架,MCP 服务又是单独跑的一个进程。每次调试,光是把这些环境串起来就要花掉大半天。
AIO Sandbox 的思路很直接:既然这些工具都是 Agent 干活时要用到的,那为什么不把它们全部塞进同一个容器里,让它们共享同一个文件系统、同一个网络命名空间、同一套进程管理机制?这个思路听起来简单,但真正落地的时候,涉及到的技术细节远比想象中复杂。
这个项目适合谁看?如果你正在做 Agent 开发,尤其是那种需要"浏览器操作 + 代码执行 + 文件读写 + 工具调用"四件套齐活的场景,那这个项目的设计思路值得你花时间研究。如果你只是偶尔跑跑简单的 Agent demo,那可能用不上这么重的方案。但如果你已经被多环境切换折磨过,那这篇文章应该能帮你省下不少试错时间。
提示:AIO Sandbox 的核心价值不在于"功能多",而在于"隔离与共享的平衡"。它把该隔离的隔离了,该共享的共享了,这个边界划分才是真正值得学的地方。
2. 核心架构拆解:一个容器里到底装了什么
2.1 浏览器层:Playwright 的无头方案为什么是首选
AIO Sandbox 里集成的浏览器能力,底层用的是 Playwright。这个选择其实挺讲究的。市面上做浏览器自动化的方案不少,Selenium、Puppeteer、Playwright 各有拥趸,但在容器化场景下,Playwright 的优势非常明显。
第一,Playwright 自带浏览器二进制管理。你不需要在 Dockerfile 里手动下载 Chrome 或者 Chromium,Playwright 自己会处理浏览器的下载和版本匹配。这在容器构建的时候能省掉一大堆麻烦事。我之前用 Selenium 的时候,光是处理 ChromeDriver 和 Chrome 版本不匹配的问题就折腾了好几次,每次 Chrome 自动更新,整个自动化流程就崩了。
第二,Playwright 的上下文隔离机制天然适合多 Agent 场景。每个 BrowserContext 相当于一个独立的"隐身窗口",cookie、localStorage、session 都是隔离的。这意味着你可以在同一个容器里跑多个 Agent 任务,互不干扰。如果用 Selenium,你得自己想办法管理多个 driver 实例,资源开销大不说,还容易出各种诡异的冲突。
第三,Playwright 支持多种语言绑定。Python、JavaScript、Java、.NET 都有官方支持。AIO Sandbox 里主要用的是 Python 和 Node.js 两种,这样不同技术栈的 Agent 都能接入。
在实际配置中,容器里的 Playwright 通常需要设置几个关键参数:
from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch( headless=True, args=[ '--no-sandbox', '--disable-dev-shm-usage', '--disable-gpu', '--single-process' ] ) context = browser.new_context( viewport={'width': 1920, 'height': 1080}, user_agent='Mozilla/5.0 ...' )这里面的--no-sandbox和--disable-dev-shm-usage是两个在容器环境里必须加的参数。前者是因为容器里通常没有完整的沙箱权限,不加会直接启动失败;后者是因为容器默认的/dev/shm分区太小,Chrome 渲染复杂页面时会因为共享内存不足而崩溃。这两个坑我都踩过,尤其是第二个,报错信息特别隐晦,排查起来很费劲。
2.2 Shell 层:不只是开个终端那么简单
AIO Sandbox 里的 Shell 能力,表面上看就是给你一个能执行命令的终端,但实际上它承担的角色要重得多。它是 Agent 与操作系统交互的"手",Agent 通过 Shell 来安装依赖、运行脚本、管理进程、查看系统状态。
在容器里做 Shell 执行,最大的挑战是权限控制。你不能让 Agent 随便执行rm -rf /这种命令,但你又不能限制得太死,否则很多正常的操作都做不了。AIO Sandbox 的做法是:在容器层面做隔离,在容器内部给足权限。也就是说,容器本身是一个受限的环境,它只能访问挂载进去的目录,只能使用分配到的资源。但在容器内部,Agent 拥有较高的操作自由度。
这个设计思路的合理性在于:容器的隔离边界是内核级别的,比任何应用层的权限控制都可靠。你用 seccomp、AppArmor 这些工具在容器内部做细粒度权限控制,配置复杂不说,还容易出兼容性问题。不如把边界画在容器这一层,容器里面的事情让 Agent 自己管。
实际使用中,Shell 执行通常通过一个 HTTP 接口或者 WebSocket 来暴露。Agent 发送命令,服务端执行后返回输出。这里有个细节值得注意:命令执行要有超时机制。我遇到过 Agent 执行了一个死循环脚本,把整个容器的 CPU 占满的情况。后来加了超时限制,超过指定时间自动 kill 进程,问题就解决了。
import subprocess import signal def execute_command(cmd, timeout=30): try: result = subprocess.run( cmd, shell=True, capture_output=True, text=True, timeout=timeout ) return { 'stdout': result.stdout, 'stderr': result.stderr, 'returncode': result.returncode } except subprocess.TimeoutExpired: return { 'error': f'Command timed out after {timeout} seconds', 'returncode': -1 }2.3 文件系统层:共享与隔离的平衡术
文件系统是 AIO Sandbox 里最微妙的部分。浏览器下载的文件、Shell 生成的脚本、VSCode 编辑的代码、MCP 服务读取的配置,这些都需要通过文件系统来交换。如果每个工具都有自己独立的文件空间,那数据流转就成了大问题。
AIO Sandbox 的方案是:容器内共享一个工作目录,但不同工具通过不同的挂载点访问。比如/workspace是主工作目录,所有工具都能读写;/tmp是临时目录,容器重启就清空;/data是持久化目录,通过 volume 挂载到宿主机,容器销毁数据还在。
这种设计的好处是数据流转路径清晰。Agent 在浏览器里下载了一个文件,默认会落到/workspace/downloads下面;Shell 脚本要处理这个文件,直接去这个路径读就行;VSCode 要编辑这个文件,打开/workspace目录就能看到。不需要任何额外的数据搬运操作。
但这里有个坑需要注意:文件权限问题。容器里的进程可能以不同的用户身份运行,如果浏览器以root身份写了一个文件,Shell 以sandbox用户去读,就可能遇到权限拒绝。解决办法是在容器启动时统一用户 ID,或者把工作目录的权限设置得宽松一些。
RUN mkdir -p /workspace && \ chmod 777 /workspace && \ chown -R sandbox:sandbox /workspace注意:把工作目录权限设成 777 在生产环境里是有风险的,但在开发调试阶段能省掉很多权限相关的麻烦。正式部署的时候,建议用更精细的权限控制方案。
2.4 MCP 层:工具调用的标准化接口
MCP 是这两年 Agent 领域最热的关键词之一。它的全称是 Model Context Protocol,本质上是一套标准化的工具调用协议。在没有 MCP 之前,每个 Agent 框架都有自己的工具定义方式,OpenAI 的 function calling 是一种格式,LangChain 的 Tool 是另一种格式,Anthropic 的 tool use 又是另一种。你想把一个工具从 A 框架迁移到 B 框架,得重写一遍。
MCP 要解决的就是这个问题:定义一套通用的协议,让工具的描述、调用、返回都有统一的标准。AIO Sandbox 里集成 MCP,意味着容器内的各种能力(浏览器操作、Shell 执行、文件读写)都可以通过 MCP 协议暴露给外部的 Agent 框架。
MCP 的通信方式主要有两种:stdio和SSE。stdio 是通过标准输入输出流来通信,适合本地进程间的调用;SSE 是通过 HTTP 长连接来通信,适合跨网络的场景。AIO Sandbox 通常两种都支持,你可以根据 Agent 框架的位置来选择。
{ "mcpServers": { "aio-sandbox": { "command": "docker", "args": [ "exec", "-i", "aio-sandbox", "python", "-m", "mcp_server" ] } } }这是一个典型的 MCP 配置示例。Agent 框架通过docker exec进入容器,启动 MCP 服务,然后通过 stdio 进行通信。这种方式的优点是不需要暴露额外的网络端口,安全性更好。缺点是只能在本机使用,如果 Agent 框架跑在另一台机器上,就得改用 SSE 方式。
2.5 VSCode 层:为什么要把 IDE 也塞进去
第一次看到 AIO Sandbox 里集成 VSCode 的时候,我觉得这个设计有点多余。Agent 干活要 IDE 干什么?但后来想明白了:VSCode 在这里不是给人用的,是给 Agent 用的。
VSCode 的核心能力是什么?代码编辑、语法检查、格式化、调试、插件生态。这些能力对 Agent 来说同样有价值。比如 Agent 生成了一段代码,它可以用 VSCode 的语法检查功能来验证代码是否正确;Agent 需要格式化一个 JSON 文件,它可以用 VSCode 的格式化插件来完成;Agent 需要调试一段逻辑,它可以用 VSCode 的调试器来单步执行。
更重要的是,VSCode 的插件生态可以无限扩展 Agent 的能力。今天需要一个 Markdown 预览功能,装个插件就行;明天需要一个数据库客户端,再装个插件。不需要修改 Agent 的核心代码,只需要在容器里配置好插件就行。
在容器里跑 VSCode,通常用的是code-server这个方案。它把 VSCode 的后端跑在服务器上,前端通过浏览器访问。这样 Agent 可以通过 API 来操作 VSCode,人也可以通过浏览器来查看和干预。
# code-server 启动配置 code-server --bind-addr 0.0.0.0:8080 \ --auth none \ --disable-telemetry \ /workspace--auth none在容器内部使用是没问题的,因为外部访问会经过容器的网络隔离。但如果你把端口映射到了宿主机,就一定要加上认证,否则任何人都能访问你的 IDE。
3. 从零搭建一个 AIO Sandbox 的实操记录
3.1 基础镜像选型与依赖安装
搭建 AIO Sandbox 的第一步是选基础镜像。这个选择会直接影响后续的构建速度和运行稳定性。我试过几种方案,各有优劣。
方案一:Ubuntu 22.04 基础镜像。优点是软件源丰富,什么都能装;缺点是镜像体积大,构建时间长。一个完整的 AIO Sandbox 镜像,用 Ubuntu 做基础,轻松超过 2GB。
方案二:Alpine 基础镜像。优点是体积小,构建快;缺点是 musl libc 和 glibc 的兼容性问题,很多预编译的二进制包跑不起来。Playwright 的浏览器在 Alpine 上就需要额外的兼容层。
方案三:Playwright 官方镜像。这是我最推荐的方案。Playwright 官方维护了一套 Docker 镜像,里面已经预装了浏览器和所有依赖。你只需要在这个基础上加装 Shell 工具、code-server 和 MCP 服务就行。
FROM mcr.microsoft.com/playwright/python:v1.40.0-jammy # 安装基础工具 RUN apt-get update && apt-get install -y \ curl \ wget \ git \ vim \ htop \ net-tools \ && rm -rf /var/lib/apt/lists/* # 安装 code-server RUN curl -fsSL https://code-server.dev/install.sh | sh # 安装 Python 依赖 COPY requirements.txt /tmp/ RUN pip install --no-cache-dir -r /tmp/requirements.txt # 创建工作目录 RUN mkdir -p /workspace && chmod 777 /workspace WORKDIR /workspace这个 Dockerfile 的关键点在于层叠顺序。把不经常变动的操作放在前面,经常变动的放在后面,这样构建缓存才能最大化利用。比如安装系统工具这一步,可能几个月都不会变,放在最前面;复制 requirements.txt 并安装 Python 依赖,这个变动频率中等;复制项目代码,这个变动最频繁,放在最后。
3.2 容器启动参数与资源限制
容器启动的时候,有几个参数直接决定了沙箱的可用性和安全性。我整理了一个对照表,方便你根据实际场景调整。
| 参数 | 作用 | 推荐值 | 说明 |
|---|---|---|---|
--memory | 内存上限 | 4g | 浏览器很吃内存,低于 2g 容易崩 |
--cpus | CPU 核心数 | 2.0 | 根据宿主机配置调整 |
--shm-size | 共享内存大小 | 2g | 默认 64m 不够 Chrome 用 |
--network | 网络模式 | bridge | 需要外部访问时用 host |
-v | 目录挂载 | /data:/workspace | 持久化工作目录 |
--restart | 重启策略 | unless-stopped | 崩溃后自动恢复 |
--shm-size这个参数我要特别强调一下。Docker 默认的共享内存是 64MB,这个大小对于简单的页面渲染勉强够用,但一旦页面复杂一点,或者同时开多个标签页,Chrome 就会因为共享内存不足而崩溃。报错信息通常是Failed to allocate shared memory或者直接Target closed,看起来像是代码问题,实际上是资源问题。
docker run -d \ --name aio-sandbox \ --memory 4g \ --cpus 2.0 \ --shm-size 2g \ -p 8080:8080 \ -p 3000:3000 \ -v /host/data:/workspace \ --restart unless-stopped \ aio-sandbox:latest3.3 MCP 服务的配置与联调
MCP 服务的配置是整个过程里最容易出问题的环节。因为它涉及到进程间通信、协议格式、超时设置等多个方面,任何一个环节出问题都会导致调用失败。
首先,MCP 服务需要注册到 Agent 框架里。以 Claude Desktop 为例,配置文件通常在~/Library/Application Support/Claude/claude_desktop_config.json(macOS)或者%APPDATA%\Claude\claude_desktop_config.json(Windows)。
{ "mcpServers": { "aio-sandbox": { "command": "docker", "args": [ "exec", "-i", "aio-sandbox", "python", "-m", "aio_sandbox.mcp_server" ], "env": { "WORKSPACE": "/workspace", "TIMEOUT": "30" } } } }配置好之后,重启 Agent 框架,它应该能自动发现 MCP 服务提供的工具列表。如果发现不了,按以下顺序排查:
- 容器是否在运行:
docker ps | grep aio-sandbox - MCP 服务是否能手动启动:
docker exec -it aio-sandbox python -m aio_sandbox.mcp_server - 协议格式是否正确:MCP 服务启动后,手动发送一个初始化请求,看是否返回正确的响应
- 权限是否足够:
docker exec需要当前用户有 Docker 操作权限
我遇到过一次比较诡异的问题:MCP 服务在容器里手动跑没问题,但通过 Agent 框架调用就超时。后来发现是stdio 缓冲区的问题。MCP 服务输出日志的时候,如果日志量太大,把 stdio 缓冲区占满了,协议消息就发不出去。解决办法是把日志重定向到文件,不要输出到 stdio。
import logging import sys # 把日志写到文件,不要占用 stdio logging.basicConfig( filename='/var/log/mcp_server.log', level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s' ) # 确保 stdout 只用于 MCP 协议通信 sys.stdout = open('/dev/stdout', 'w')3.4 浏览器与 Shell 的联动测试
搭建完成之后,一定要做一次完整的联动测试,确保各个组件之间的配合没有问题。我通常用这样一个测试流程:
第一步,通过 MCP 调用浏览器打开一个页面。让 Agent 执行"打开 example.com 并截图"这个任务。如果截图能正常生成并保存到/workspace目录,说明浏览器和文件系统是通的。
第二步,通过 Shell 处理截图文件。让 Agent 执行"把截图文件压缩成 zip 包"这个任务。如果 zip 包能正常生成,说明 Shell 和文件系统是通的。
第三步,通过 VSCode 查看文件。打开 code-server 的 Web 界面,导航到/workspace目录,看截图和 zip 包是否都在。如果能看到,说明 VSCode 和文件系统是通的。
第四步,通过 MCP 调用 Shell 执行一个长时间任务。比如"运行一个持续 60 秒的脚本",然后在执行过程中通过 VSCode 查看进程状态。如果能看到进程在运行,说明进程管理和监控是通的。
这个测试流程走下来,基本上能覆盖 90% 的使用场景。如果某一步失败了,问题范围就缩小到了具体的组件上,排查起来会快很多。
4. 实际使用中踩过的坑与解决方案
4.1 浏览器内存泄漏的排查与处理
浏览器在容器里长时间运行,内存泄漏几乎是一定会遇到的。表现是容器内存占用持续上升,最终触发 OOM Killer,容器被强制重启。
排查内存泄漏的第一步是确认泄漏源。是浏览器本身泄漏,还是 Agent 代码泄漏,还是 MCP 服务泄漏?我的做法是在容器里跑一个监控脚本,定期记录各个进程的内存占用。
#!/bin/bash while true; do echo "=== $(date) ===" ps aux --sort=-%mem | head -10 echo "---" free -m sleep 60 done如果发现是浏览器进程的内存持续增长,那大概率是BrowserContext 没有正确关闭。Playwright 的 BrowserContext 如果不显式关闭,它占用的内存不会自动释放。正确的做法是用with语句或者try/finally来确保关闭。
# 错误做法:context 可能不会被关闭 context = browser.new_context() page = context.new_page() page.goto('https://example.com') # 忘记 context.close() # 正确做法:确保 context 被关闭 with browser.new_context() as context: page = context.new_page() page.goto('https://example.com') # 退出 with 块时自动关闭另一个常见原因是页面没有正确关闭。每个 page 都会占用一定的内存,如果打开了大量 page 而不关闭,内存也会持续增长。建议在 Agent 逻辑里加一个页面池,限制同时打开的页面数量。
4.2 文件权限冲突的典型场景
文件权限问题在 AIO Sandbox 里出现的频率很高,因为容器里跑着多个不同用户身份的进程。浏览器可能以root运行,Shell 可能以sandbox运行,code-server 可能以coder运行。它们对同一个文件的读写权限如果不一致,就会出问题。
最常见的场景是:浏览器下载了一个文件,Shell 脚本无法读取。原因是浏览器以root身份创建了文件,权限是644,而 Shell 以sandbox身份运行,没有读权限。
解决方案有三种:
方案一:统一用户身份。让所有进程都以同一个用户运行。这需要在 Dockerfile 里显式指定USER,并且在启动脚本里确保所有服务都切换到该用户。
方案二:设置 umask。在容器启动时设置umask 000,这样创建的文件默认就是666权限,所有人都能读写。这个方案简单粗暴,但在多租户场景下不安全。
方案三:使用 ACL。给工作目录设置默认 ACL,让新创建的文件自动继承权限。
# 设置默认 ACL setfacl -d -m u:sandbox:rwx /workspace setfacl -d -m u:coder:rwx /workspace setfacl -d -m o::rx /workspace我个人的建议是方案一和方案三结合。统一用户身份能解决大部分问题,ACL 作为补充,处理一些特殊情况。
4.3 MCP 调用超时的参数调优
MCP 调用超时是另一个高频问题。Agent 发送一个工具调用请求,等了半天没响应,最后超时失败。这个问题可能出在多个环节:网络延迟、服务处理慢、结果太大传输慢。
调优的第一步是确认超时发生在哪个环节。在 MCP 服务端加日志,记录请求到达时间和响应发送时间。如果请求到达时间就很晚,说明是网络问题;如果处理时间很长,说明是服务逻辑问题;如果响应发送时间很晚,说明是结果序列化或传输问题。
import time import logging logger = logging.getLogger(__name__) async def handle_tool_call(request): start = time.time() logger.info(f"Request received: {request.tool_name}") try: result = await execute_tool(request) elapsed = time.time() - start logger.info(f"Tool executed in {elapsed:.2f}s") return result except Exception as e: elapsed = time.time() - start logger.error(f"Tool failed after {elapsed:.2f}s: {e}") raise如果确认是服务处理慢,可以考虑几个优化方向:增加并发处理能力(用 asyncio 或者多线程)、优化工具实现(比如浏览器操作加缓存)、拆分大任务(把一个耗时 60 秒的任务拆成 6 个 10 秒的子任务)。
如果确认是结果太大导致传输慢,可以在 MCP 服务端做结果截断。比如浏览器截图,不要返回完整的 base64 图片,而是保存到文件,只返回文件路径。
4.4 容器资源隔离的边界设置
AIO Sandbox 把这么多工具塞进一个容器,资源隔离的边界设置就变得很重要。如果某个工具占用了过多资源,会影响其他工具的正常运行。
CPU 隔离相对简单,用--cpus参数限制总核数就行。但内存隔离就复杂一些,因为浏览器、Shell、VSCode 对内存的需求差异很大。浏览器的内存占用波动很大,打开一个复杂页面可能瞬间吃掉 1GB;Shell 脚本的内存占用通常比较稳定;VSCode 的内存占用介于两者之间。
我的做法是给容器设置一个总的内存上限,然后在容器内部用 cgroup 做二级限制。这样既能保证容器不会拖垮宿主机,又能防止某个工具独占容器内存。
# 容器级别限制 docker run --memory 4g --memory-swap 4g ... # 容器内部二级限制(在容器启动脚本里) cgcreate -g memory:/aio/browser cgset -r memory.limit_in_bytes=2g /aio/browser cgset -r memory.limit_in_bytes=1g /aio/shell注意:容器内部的 cgroup 操作需要容器有相应的权限。如果 Docker 的安全配置比较严格,可能需要在启动时加
--privileged或者--cap-add SYS_ADMIN。但这会降低容器的安全性,需要权衡。
5. 几个容易被忽略的细节与经验之谈
5.1 镜像体积优化的实际收益
AIO Sandbox 的镜像很容易做到 3GB 以上,因为 Playwright 的浏览器本身就很大,再加上 code-server、Python 依赖、系统工具,体积很难控制。但镜像体积直接影响的是分发效率和启动速度。
我做过一次优化,把镜像从 3.2GB 压到了 1.8GB,启动时间从 45 秒降到了 20 秒。主要的优化手段有:
多阶段构建。把编译依赖和运行时依赖分开,最终镜像只保留运行时需要的东西。比如 Python 的gcc、make这些编译工具,在安装完依赖之后就可以删掉。
清理缓存。apt-get的包缓存、pip的下载缓存、npm的缓存,这些在构建完成后都不需要保留。在 Dockerfile 里加清理命令,能省下几百 MB。
合并层。把多个RUN指令合并成一个,减少镜像层数。每一层都有开销,层数越少,镜像越小。
# 优化前:多个 RUN,层数多,缓存大 RUN apt-get update RUN apt-get install -y curl wget git RUN rm -rf /var/lib/apt/lists/* # 优化后:单个 RUN,层数少,缓存清理干净 RUN apt-get update && \ apt-get install -y --no-install-recommends \ curl wget git && \ apt-get clean && \ rm -rf /var/lib/apt/lists/*--no-install-recommends这个参数也很关键。默认情况下,apt-get install会安装推荐依赖,这些依赖很多是用不上的。加上这个参数,能少装不少东西。
5.2 日志管理的正确姿势
AIO Sandbox 里跑着多个服务,每个服务都在输出日志。如果不做管理,日志文件会迅速膨胀,把磁盘占满。我遇到过容器跑了三天,日志文件占了 10GB 的情况。
日志管理的基本原则是:分级输出、定期轮转、按需保留。
分级输出是指不同级别的日志写到不同的地方。ERROR 级别的日志写到单独的文件,方便快速定位问题;INFO 级别的日志写到常规文件;DEBUG 级别的日志只在调试时开启,平时关掉。
定期轮转是指日志文件不能无限增长。用logrotate或者 Python 的RotatingFileHandler来做轮转,比如每天轮转一次,保留最近 7 天的日志。
from logging.handlers import RotatingFileHandler handler = RotatingFileHandler( '/var/log/aio_sandbox.log', maxBytes=10*1024*1024, # 10MB backupCount=5, # 保留 5 个备份 encoding='utf-8' )按需保留是指不是所有日志都需要长期保存。调试日志、访问日志这些,保留几天就够了;错误日志、审计日志这些,可能需要保留更长时间。根据实际需求来定。
5.3 安全加固的几个关键点
AIO Sandbox 把这么多能力集中在一个容器里,安全风险也随之集中。如果容器被攻破,攻击者就同时获得了浏览器、Shell、文件系统、IDE 的控制权。所以安全加固是必须的。
第一,不要用 root 运行容器。在 Dockerfile 里创建普通用户,用USER指令切换。虽然容器内的 root 和宿主机的 root 不是一回事,但多一层防护总是好的。
第二,限制容器的 capabilities。默认情况下,Docker 容器拥有一组 Linux capabilities,其中一些是不需要的。用--cap-drop把不需要的都去掉,只保留必要的。
docker run --cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID ...第三,只读挂载不必要的目录。容器里的/usr、/lib、/etc这些目录,通常不需要写入。用--read-only把根文件系统设为只读,只把需要写入的目录(如/workspace、/tmp)挂载为可写。
第四,网络访问控制。如果 Agent 不需要访问外网,就把容器的网络模式设为none。如果需要访问外网,用--network bridge并配合 iptables 规则做限制。
第五,定期更新基础镜像。基础镜像里的系统库、浏览器、Python 解释器,都可能存在安全漏洞。定期更新镜像,打上安全补丁。
5.4 性能调优的实测数据
最后分享一组我实测的性能数据,供你参考。测试环境是 4 核 8G 的云服务器,容器分配了 2 核 4G 内存。
| 操作 | 耗时 | 备注 |
|---|---|---|
| 容器启动 | 20s | 优化后的镜像 |
| 浏览器启动 | 3s | 首次启动,后续复用 |
| 打开简单页面 | 1.5s | example.com |
| 打开复杂页面 | 5s | 带大量 JS 的 SPA |
| 截图并保存 | 0.8s | 1920x1080 |
| Shell 执行简单命令 | 0.1s | echo、ls 等 |
| Shell 执行 Python 脚本 | 1.2s | 含解释器启动 |
| MCP 调用往返 | 0.3s | 本地 stdio |
| VSCode 打开文件 | 0.5s | 小文件 |
这些数据是在比较理想的条件下测的,实际使用中会因为网络、页面复杂度、脚本逻辑等因素有较大波动。但整体来看,AIO Sandbox 的性能是可以接受的,尤其是浏览器复用和 MCP 本地调用这两个场景,响应速度很快。
如果发现某个操作特别慢,优先排查这几个方向:浏览器是否复用了 context、Shell 命令是否有不必要的等待、MCP 服务是否在处理大结果。大部分性能问题都能从这三个方向找到原因。
这个项目后续还可以往几个方向扩展:比如加入 GPU 支持,让容器能跑一些轻量级的模型推理;比如加入分布式文件系统,让多个容器实例共享工作目录;比如加入更细粒度的资源监控,实时展示各个工具的资源占用。这些扩展不一定都要做,但知道有这些可能性,在设计自己的 Agent 系统时就能留出相应的接口。