智能体生成代码、与文件系统交互并运行 shell 命令。由于我们无法预测智能体可能执行的操作,因此确保其环境隔离至关重要,以防止其访问凭证、文件或网络。沙盒通过在智能体的执行环境与你的主机系统之间创建边界来提供这种隔离。
在 Deep Agents 中,沙盒是后端,它们定义了智能体运行的环境。与其他仅暴露文件操作的后端(State、Filesystem、Store)不同,沙盒后端还为智能体提供了一个用于运行 shell 命令的 execute 工具。当你配置一个沙盒后端时,智能体将获得:
- 所有标准文件系统工具(
ls、read_file、write_file、edit_file、glob、grep)
- 用于在沙盒中运行任意 shell 命令的
execute 工具
- 保护你主机系统的安全边界
为什么使用沙盒?
沙盒用于安全目的。
它们允许智能体执行任意代码、访问文件和使用网络,而不会危及你的凭证、本地文件或主机系统。
当智能体自主运行时,这种隔离至关重要。
沙盒特别适用于:
- 编码智能体:自主运行的智能体可以使用 shell、git、克隆仓库(许多提供商提供原生 git API,例如 Daytona 的 git 操作),并运行 Docker-in-Docker 进行构建和测试流水线
- 数据分析智能体——在安全、隔离的环境中加载文件、安装数据分析库(pandas、numpy 等)、运行统计计算并创建输出(如 PowerPoint 演示文稿)
使用 Deep Agents CLI? CLI 通过 --sandbox 标志内置了沙盒支持。有关 CLI
特定的设置、标志(--sandbox-id、--sandbox-setup)和示例,请参阅使用远程沙盒。
基本用法
这些示例假设你已经使用提供商的 SDK 创建了沙盒/devbox 并设置了凭证。有关注册、身份验证和提供商特定的生命周期详情,请参阅可用提供商。
import { createDeepAgent } from "deepagents";
import { ChatAnthropic } from "@langchain/anthropic";
import { DenoSandbox } from "@langchain/deno";
// 创建并初始化沙箱
const sandbox = await DenoSandbox.create({
memoryMb: 1024,
lifetime: "10m",
});
try {
const agent = createDeepAgent({
model: new ChatAnthropic({ model: "claude-opus-4-6" }),
systemPrompt: "You are a JavaScript coding assistant with sandbox access.",
backend: sandbox,
});
const result = await agent.invoke({
messages: [
{
role: "user",
content:
"Create a simple HTTP server using Deno.serve and test it with curl",
},
],
});
} finally {
await sandbox.close();
}
可用提供商
没有看到你的提供商?你可以实现自己的沙盒后端。请参阅贡献沙盒集成。
生命周期和作用域
沙盒在关闭之前会消耗资源并产生费用。你如何管理它们的生命周期取决于你的应用程序。
选择沙盒生命周期如何映射到你的应用程序资源。有关此决策的更多信息,请参阅投入生产。
线程作用域(默认)
每个对话都有自己的沙盒。沙盒在第一次运行开始时创建,并在同一对话线程的后续消息中重复使用。当对话线程被清理(或沙盒 TTL 过期)时,沙盒将被销毁。这是大多数智能体的正确默认设置。
示例:一个数据分析机器人,每个对话都从一个干净的环境开始。
助手作用域
给定助手的所有对话线程共享一个沙盒。沙盒 ID 存储在助手的配置中,因此每个对话都会返回到相同的环境。文件、安装的包和克隆的仓库在对话之间持续存在。当智能体维护一个长期运行的工作区时使用此模式。
示例:一个编码助手,在对话之间维护一个克隆的仓库和已安装的依赖项。
助手作用域的沙盒会随着时间的推移积累文件、安装的包和其他沙盒内状态。请使用你的沙盒提供商配置
TTL,使用快照定期重置,或实现清理逻辑以防止沙盒的磁盘和内存无限增长。线程作用域的沙盒通过每次对话都从新开始来避免此问题。
基本生命周期
// 创建并初始化
const sandbox = await ModalSandbox.create(options);
// 使用沙盒(直接或通过智能体)
const result = await sandbox.execute("echo hello");
// 完成后清理
await sandbox.close();
每次对话的生命周期
在聊天应用程序中,对话通常由 thread_id 表示。
通常,每个 thread_id 应使用其自己唯一的沙盒。
在你的应用程序中存储沙盒 ID 和 thread_id 之间的映射,或者如果沙盒提供商允许将元数据附加到沙盒,则在沙盒中存储。
聊天应用程序的 TTL。
当用户可以在空闲时间后重新参与时,你通常不知道他们是否会返回或何时返回。在沙盒上配置生存时间(TTL)——例如,TTL
归档或 TTL 删除——以便提供商自动清理空闲的沙盒。许多沙盒提供商支持此功能。
import "dotenv/config";
import { randomUUID } from "node:crypto";
import { Daytona } from "@daytonaio/sdk";
import type { CreateSandboxFromSnapshotParams } from "@daytonaio/sdk";
import { DaytonaSandbox } from "@langchain/daytona";
import { createDeepAgent } from "deepagents";
const client = new Daytona();
const threadId = randomUUID();
// 通过 thread_id 获取或创建沙盒
let sandbox;
try {
sandbox = await client.findOne({ labels: { thread_id: threadId } });
} catch {
const params: CreateSandboxFromSnapshotParams = {
labels: { thread_id: threadId },
// 添加 TTL,以便沙盒在空闲时被清理(分钟)
autoDeleteInterval: 3600,
};
sandbox = await client.create(params);
}
const backend = await DaytonaSandbox.fromId(sandbox.id);
const agent = createDeepAgent({
backend,
systemPrompt:
"你是一个可以访问沙盒的编码助手。你可以在沙盒中创建和运行代码。",
});
try {
const result = await agent.invoke(
{
messages: [
{
role: "user",
content: "创建一个 hello world Python 脚本并运行它",
},
],
},
{
configurable: {
thread_id: threadId,
},
},
);
const lastMessage = result.messages[result.messages.length - 1];
console.log(
typeof lastMessage.content === "string"
? lastMessage.content
: String(lastMessage.content),
);
} catch (err) {
// 可选:在异常时主动删除沙盒
await client.delete(sandbox);
throw err;
}
集成模式
有两种架构模式用于将智能体与沙盒集成,基于智能体运行的位置。
沙盒内智能体模式
智能体在沙盒内运行,你通过网络与其通信。你构建一个预装了智能体框架的 Docker 或 VM 镜像,在沙盒内运行它,并从外部连接以发送消息。
优点:
- ✅ 与本地开发非常相似。
- ✅ 智能体与环境紧密耦合。
权衡:
- 🔴 API 密钥必须存放在沙盒内(安全风险)。
- 🔴 更新需要重新构建镜像。
- 🔴 需要通信基础设施(WebSocket 或 HTTP 层)。
要在沙盒内运行智能体,构建一个镜像并在其上安装 deepagents。
FROM python:3.11
RUN pip install deepagents-cli
然后在沙盒内运行智能体。
要在沙盒内使用智能体,你必须添加额外的基础设施来处理你的应用程序与沙盒内智能体之间的通信。
沙盒作为工具模式
智能体在你的机器或服务器上运行。当它需要执行代码时,它调用沙盒工具(如 execute、read_file 或 write_file),这些工具调用提供商的 API 在远程沙盒中运行操作。
优点:
- ✅ 无需重新构建镜像即可即时更新智能体代码。
- ✅ 智能体状态与执行之间更清晰的分离。
- API 密钥保留在沙盒外。
- 沙盒故障不会丢失智能体状态。
- 可以选择在多个沙盒中并行运行任务。
- ✅ 仅为执行时间付费。
权衡:
import "dotenv/config";
import { DaytonaSandbox } from "@langchain/daytona";
import { createDeepAgent } from "deepagents";
// 也可以使用 E2B、Runloop、Modal 实现
const sandbox = await DaytonaSandbox.create();
const agent = createDeepAgent({
backend: sandbox,
systemPrompt:
"你是一个可以访问沙盒的编码助手。你可以在沙盒中创建和运行代码。",
});
try {
const result = await agent.invoke({
messages: [
{
role: "user",
content: "创建一个 hello world Python 脚本并运行它",
},
],
});
const lastMessage = result.messages[result.messages.length - 1];
console.log(
typeof lastMessage.content === "string"
? lastMessage.content
: String(lastMessage.content),
);
} catch (err) {
// 可选:在异常时主动删除沙盒
await sandbox.close();
throw err;
}
本文档中的示例使用沙盒作为工具模式。
当你的提供商的 SDK 处理通信层并且你希望生产环境与本地开发相似时,选择沙盒内智能体模式。
当你需要快速迭代智能体逻辑、将 API 密钥保留在沙盒外,或者更喜欢更清晰的关注点分离时,选择沙盒作为工具模式。
沙盒如何工作
隔离边界
所有沙盒提供商都保护你的主机系统免受智能体的文件系统和 shell 操作的影响。智能体无法读取你的本地文件、访问你机器上的环境变量或干扰其他进程。然而,沙盒本身不能防止:
- 上下文注入:控制智能体部分输入的攻击者可以指示其在沙盒内运行任意命令。沙盒是隔离的,但智能体在其中拥有完全控制权。
- 网络数据泄露:除非网络访问被阻止,否则上下文注入的智能体可以通过 HTTP 或 DNS 将数据从沙盒中发送出去。一些提供商支持阻止网络访问(例如,Modal 上的
blockNetwork: true)。
有关如何处理密钥和缓解这些风险的信息,请参阅安全注意事项。
execute 方法
沙盒后端具有简单的架构:提供商必须实现的唯一方法是 execute(),它运行一个 shell 命令并返回其输出。所有其他文件系统操作(read、write、edit、ls、glob、grep)都是由 BaseSandbox 基类在 execute() 之上构建的,该基类构造脚本并通过 execute() 在沙盒内运行它们。
这种设计意味着:
- 添加新提供商很简单。 实现
execute()——基类处理其他所有事情。
execute 工具是条件可用的。 在每次模型调用时,运行时检查后端是否实现了 SandboxBackendProtocol。如果没有,该工具将被过滤掉,智能体永远不会看到它。
当智能体调用 execute 工具时,它提供一个 command 字符串,并返回组合的 stdout/stderr、退出码,以及如果输出过大时的截断通知。
你也可以在应用程序代码中直接调用后端 execute() 方法。
例如:
bash: foobar: command not found
[命令失败,退出码为 127]
如果命令产生非常大的输出,结果会自动保存到一个文件中,并指示智能体使用 read_file 来增量访问它。这可以防止上下文窗口溢出。
文件访问的两个层面
文件进出沙盒有两种不同的方式,理解何时使用每种方式非常重要:
智能体文件系统工具:read_file、write_file、edit_file、ls、glob、grep 和 execute 是 LLM 在执行期间调用的工具。这些通过沙盒内的 execute() 进行。智能体使用它们来读取代码、写入文件和运行命令,作为其任务的一部分。
文件传输 API:你的应用程序代码调用的 uploadFiles() 和 downloadFiles() 方法。这些使用提供商的原生文件传输 API(不是 shell 命令),旨在在你的主机环境和沙盒之间移动文件。使用这些来:
- 在智能体运行之前为沙盒提供种子,包含源代码、配置或数据
- 在智能体完成后检索工件(生成的代码、构建输出、报告)
- 预填充依赖项,智能体将需要这些依赖项
处理文件
为沙盒提供种子
使用 uploadFiles() 在智能体运行之前填充沙盒。文件内容以 Uint8Array 形式提供:
const encoder = new TextEncoder();
const responses = await sandbox.uploadFiles([
["src/index.js", encoder.encode("console.log('Hello')")],
["package.json", encoder.encode('{"name": "my-app"}')],
]);
// 每个响应指示成功或失败
for (const res of responses) {
if (res.error) {
console.error(`上传 ${res.path} 失败: ${res.error}`);
}
}
检索工件
使用 downloadFiles() 在智能体完成后从沙盒中检索文件:
const results = await sandbox.downloadFiles(["src/index.js", "output.txt"]);
const decoder = new TextDecoder();
for (const result of results) {
if (result.content) {
console.log(`${result.path}: ${decoder.decode(result.content)}`);
} else {
console.error(`下载 ${result.path} 失败: ${result.error}`);
}
}
在沙盒内,智能体使用其自己的文件系统工具(read_file、write_file):而不是
uploadFiles 或
downloadFiles。这些方法是供你的应用程序代码在主机和沙盒之间的边界移动文件使用的。
安全注意事项
沙盒将代码执行与你的主机系统隔离,但它们不能防止上下文注入。控制智能体部分输入的攻击者可以指示其在沙盒内读取文件、运行命令或泄露数据。这使得沙盒内的凭证尤其危险。
切勿将密钥放入沙盒内。 通过环境变量、挂载文件或 secrets 选项注入沙盒的
API
密钥、令牌、数据库凭证和其他密钥可以被上下文注入的智能体读取和泄露。这甚至适用于短期或有范围的凭证——如果智能体可以访问它们,攻击者也可以。
安全处理密钥
如果你的智能体需要调用经过身份验证的 API 或访问受保护的资源,你有两个选择:
-
将密钥保留在沙盒外的工具中。 定义在你的主机环境中运行(而不是在沙盒内)的工具,并在那里处理身份验证。智能体按名称调用这些工具,但永远看不到凭证。这是推荐的方法。
-
使用注入凭证的网络代理。 一些沙盒提供商支持代理,这些代理拦截来自沙盒的传出 HTTP 请求,并在转发之前附加凭证(例如,
Authorization 头)。智能体永远看不到密钥——它只是向 URL 发出普通请求。这种方法尚未在提供商中广泛可用。
如果你必须将密钥注入沙盒(不推荐),请采取以下预防措施:
- 为所有工具调用启用Human in the Loop审批,而不仅仅是敏感调用
- 阻止或限制沙盒的网络访问以限制泄露路径
- 使用尽可能窄的凭证范围和尽可能短的生存时间
- 监控沙盒网络流量以发现意外的出站请求
即使有这些保障措施,这仍然是一个不安全的变通方法。足够有创意的上下文注入攻击可以绕过输出过滤和 HITL 审查。
一般最佳实践
- 在应用程序中对沙盒输出采取行动之前,先对其进行审查
- 在不需要时阻止沙盒网络访问
- 使用中间件过滤或编辑工具输出中的敏感模式
- 将沙盒内产生的所有内容视为不受信任的输入
将这些文档连接到 Claude、VSCode 等,通过 MCP
获取实时答案。