我们如何构建安全、可扩展的智能体沙箱基础设施
从 AWS Lambda 到 Unikraft 微虚拟机,以及控制平面架构。
我们是如何走到这一步的
我们在 Browser Use 运行着数百万个 web agent。我们最初在 AWS Lambda 上使用纯浏览器 agent,每次调用都是隔离的,扩缩容是即时的,而且没有需要担心的密钥。
然后我们加入了代码执行功能。agent 可以编写和运行 Python、执行 shell 命令、创建文件。我们将其构建为一个隔离的沙箱,agent 将其作为工具调用。安全性没有问题:代码在沙箱中运行,而不是在后端。
但 agent 循环仍然与我们的 REST API 运行在同一个后端上。重新部署?所有正在运行的 agent 都会终止。内存消耗大的 agent?API 会变慢。两种根本不同的工作负载共享同一个进程。
两种模式
当 agent 能够运行任意代码时,它可以访问机器上的任何东西:环境变量、API 密钥、数据库凭据、内部服务。它需要与你的基础设施和密钥隔离。有两种方法可以做到这一点。
模式 1:隔离工具。 agent 运行在你的基础设施上。危险操作(代码执行、终端访问)在单独的沙箱中运行。agent 通过 HTTP 调用沙箱。代码在没有任何可泄露内容的地方运行。
模式 2:隔离 agent。 整个 agent 运行在一个零密钥的沙箱中。它通过一个持有所有凭据的控制平面与外部世界通信。
agent 变得可抛弃。没有可窃取的密钥,没有需要保留的状态,你可以终止它、重启它、独立地扩展它。控制平面掌握着真相。
我们从模式 1 开始,后来转向了模式 2。
沙箱
同一个容器镜像在各处运行。在生产环境中,它作为 Unikraft 微虚拟机运行。在本地开发和评估中,它作为 Docker 容器运行。一个配置开关(sandbox_mode: 'docker' | 'ukc')控制供给代码走哪条路径。
生产环境中的 Unikraft
每个 agent 都有自己独立的 Unikraft 微虚拟机,启动时间不到一秒。我们通过 Unikraft Cloud 的 REST API 在 AWS 的专用裸金属机器上为它们提供资源。
沙箱只从外部世界接收三个环境变量:SESSION_TOKEN、CONTROL_PLANE_URL 和 SESSION_ID。没有 AWS 密钥,没有数据库凭据,没有 API 令牌。
Unikraft 开箱即用地为我们提供了缩放到零的能力。当沙箱空闲时,虚拟机挂起。当下一个请求到来时,它恢复运行。一个在查询之间闲置的沙箱几乎不消耗任何成本,但能立即为后续任务唤醒。
我们将沙箱分布在多个 Unikraft 都市区,以防止任何单个都区成为瓶颈。
开发和评估中的 Docker
在本地和我们的评估管道中,同一个镜像作为 Docker 容器运行。相同的镜像、相同的入口点、相同的控制平面协议。我们可以在开发笔记本上运行完全相同的 agent,为评估并行启动数百个,并部署到 Unikraft 进行生产。
加固
在任何 agent 代码运行之前,沙箱会做几件事:
1. 仅字节码执行。 在 Docker 构建期间,我们将所有 Python 源代码编译为 .pyc 字节码,然后删除每个 .py 文件。agent 框架代码以 root 身份加载到内存中。一旦加载完成,源代码就不存在了。
2. 权限降级。 入口点以 root 身份启动(读取 root 所有的字节码需要此权限),然后立即通过 setuid/setgid 降级到 sandbox 用户。从那时起,一切都以非特权身份运行。
3. 环境清理。将 SESSION_TOKEN、CONTROL_PLANE_URL 和 SESSION_ID 读入 Python 变量后,我们从 os.environ 中删除它们。如果智能体检查环境,这些变量已经不存在了。反正该令牌在沙箱网络之外毫无用处。该虚拟机位于私有 VPC 中,除了与控制平面通信外没有任何权限。
控制平面的工作原理
可以把控制平面视为一个代理服务。沙箱无法直接访问外部世界。每个请求都必须经过控制平面。需要调用 LLM?经过控制平面。需要将文件上传到 S3?经过控制平面。这是智能体与其虚拟机之外的任何事物通信的唯一途径。
它是一个无状态的 FastAPI 服务。来自沙箱的每个请求都携带一个 Bearer: {session_token} 头。控制平面通过令牌查找会话,验证其是否仍然有效,然后使用真实凭据执行操作。
LLM 代理
对于每次 LLM 调用,沙箱只发送新消息。控制平面在数据库中保存完整的对话历史,在每次调用时重建它,并将完整的上下文转发给提供商。这使沙箱保持无状态。你可以终止它并启动一个新的,对话会从上次中断的地方继续。
控制平面还执行成本上限并处理计费。沙箱只专注于任务。
通过预签名 URL 进行文件同步
沙箱有一个 /workspace 目录,智能体在其中读写文件。文件同步服务监视更改并定期将它们同步到 S3,但沙箱永远不会看到 AWS 凭据。相反,它向控制平面请求预签名 URL:
- 沙箱检测到
/workspace中的文件更改 - 沙箱使用文件路径调用
POST /presigned-urls - 控制平面生成预签名的 S3 上传 URL(限定于该会话)
- 沙箱使用这些 URL 直接将文件上传到 S3
下载以相反的方式工作。沙箱无需持有任何 AWS 凭据即可获得受限的直接 S3 访问权限。
网关协议
在沙箱内部,智能体通过 Gateway 协议与控制平面通信:
class AgentGateway(Protocol):
async def invoke_llm(self, new_messages, tools, tool_choice) -> LLMResponse: ...
async def persist_messages(self, messages) -> None: ...在生产环境中,ControlPlaneGateway 向控制平面发送 HTTP 请求。对于本地开发和评估,DirectGateway 直接调用 LLM 并将历史记录保存在内存中。智能体代码不知道它使用的是哪一个。相同的接口,相同的行为,不同的后端。
扩展性
控制平面是无状态的:验证令牌,执行工作,返回结果。需要更多智能体?启动更多沙箱。需要更高吞吐量?在负载均衡器后面添加更多控制平面实例。每一层都根据自身的瓶颈进行扩展。
我们的后端运行在私有子网中的 ECS Fargate 上,位于 ALB 之后。控制平面根据 CPU 利用率自动扩展。沙箱通过 Unikraft 独立扩展。每个会话都有自己的虚拟机,Unikraft 负责跨大都市区域的调度。
总结
对能够执行代码的智能体进行沙箱化有两种方式。你可以隔离工具(在沙箱中运行代码执行,将智能体保留在你的后端)或隔离智能体(将整个智能体放入沙箱,通过控制平面与外部世界通信)。
我们采用了模式 2。控制平面持有所有凭据,并充当一切操作的代理:LLM 调用、文件存储、计费。沙箱只接收三个环境变量,无法访问其他任何东西。它在生产环境中作为 Unikraft 微虚拟机运行,在开发和评估环境中作为 Docker 容器运行。所有环境使用相同的镜像。
代价是每次操作都要多经过一次网络跳转,而且需要部署三个服务而不是一个。实际上,与 LLM 的响应时间相比,这种延迟微不足道,而运维复杂性正是运维团队已经知道如何处理的那类问题。
关键要点:你的智能体应该没有任何值得窃取的东西,也没有任何值得保留的东西。