芥末
发布于 2026-02-14 / 0 阅读
0
0

LangChain DeepAgents Sandboxes:隔离执行环境的设计与安全边界

LangChain DeepAgents 面向的是更复杂的 AI Agent(人工智能代理)场景:代理不只是回答问题,还要能拆任务、写代码、改文件、运行测试,甚至通过 shell 命令完成一组开发工作。

一旦允许代理执行命令,问题就不再只是“模型会不会答错”,而是“它会不会碰到不该碰的东西”。如果代理直接运行在宿主机上,它可能读到环境变量里的密钥,误删工作目录,安装一堆冲突依赖,或者把本地文件通过网络发出去。

Sandboxes 的作用就是把这类执行动作放进一个受控环境中。代理仍然可以运行代码、操作文件系统、安装依赖,但这些动作发生在隔离空间里,而不是直接作用到宿主系统。

Sandbox 解决的核心问题

Sandbox 可以理解为 DeepAgents 的执行后端。它不是一个简单的文件存储层,而是一个带有代码执行能力的运行环境。

在这个环境中,代理可以做这些事情:

  • 执行 shell 命令;
  • 创建、读取、修改、删除文件;
  • 安装依赖;
  • 编译、构建、测试和运行代码;
  • 在任务结束后把需要的产物导出到宿主侧。

它要解决的不是“让代理更聪明”,而是“让代理安全地动手做事”。

问题没有 Sandbox 时的风险Sandbox 的处理方式
命令执行安全命令直接作用于宿主机,可能泄露凭证或破坏文件命令在隔离环境中执行,限制对宿主资源的访问
依赖冲突不同任务安装不同依赖,污染本地环境每个执行环境独立配置依赖
结果复现本地机器差异导致运行结果不一致通过 provider 提供相对一致的运行环境
文件边界代理可能读写宿主文件系统沙箱内部文件系统与宿主隔离
任务清理执行后残留临时文件、依赖和进程沙箱环境可销毁或重建

从架构上看,DeepAgents 不直接把 shell 暴露给宿主机,而是通过 Sandbox 后端间接执行。

flowchart LR
    A[DeepAgent] --> B[工具层<br/>read_file / write_file / ls / grep / run]
    B --> C[Sandbox Backend]
    C --> D[execute 命令执行接口]
    D --> E[(沙箱文件系统)]
    D --> F[沙箱运行时<br/>Shell / Python / Node / 依赖环境]

    H[宿主系统] -.隔离边界.-> C
    H --> G[uploadFiles / downloadFiles]
    G --> C

图中的关键点是:代理的文件操作和命令执行都进入 Sandbox Backend;宿主系统只通过受控接口与沙箱交换文件,不把本地文件系统直接暴露给代理。

Sandbox 不是绝对安全区

隔离环境能降低风险,但不能把所有安全问题清零。Sandbox 的安全边界主要解决的是“代理不要直接碰宿主机”,它并不保证代理生成的所有行为都是可信的。

典型边界包括:

  • 沙箱内进程不能直接读取宿主磁盘;
  • 沙箱内命令不能直接访问宿主环境变量;
  • 沙箱内文件系统与宿主文件系统分离;
  • 沙箱访问权限由 provider 和配置决定。

但还有两类问题需要单独处理。

第一类是上下文注入攻击。Context injection(上下文注入)指外部内容诱导代理改变原本的执行意图,例如让代理忽略安全规则、读取敏感文件或执行额外命令。Sandbox 可以限制宿主资源访问,但不能自动判断一段提示词是不是恶意诱导。

第二类是网络外传。如果沙箱允许访问网络,代理仍然可能把沙箱内能读到的内容发送到外部服务。即使沙箱不能读宿主密钥,只要你把密钥放进了沙箱,它就有机会被命令打印、上传或写入日志。

安全模型可以这样理解:

flowchart TB
    A[宿主系统<br/>代码仓库 / 环境变量 / 凭证] -->|受控文件传输| B[Sandbox]
    B --> C[代理执行命令]
    C --> D[沙箱内部文件]
    C --> E[安装依赖 / 运行测试]
    C --> F{是否允许网络访问}

    F -->|允许| G[外部网络]
    F -->|禁止| H[无法通过网络外传]

    A -.不应直接暴露.-> C

Sandbox 负责把宿主和执行环境隔开,但敏感信息仍然应该留在宿主侧,由受控工具代为处理。

execute() 是 Sandbox 的底层核心

DeepAgents 的 Sandbox 设计围绕一个核心能力展开:execute()

execute() 的职责很直接:在沙箱内部运行一条 shell 命令,并把结果返回给调用方。它通常需要返回这些信息:

  • stdout:标准输出;
  • stderr:标准错误;
  • exit code:退出码;
  • 输出是否因为过长被截断;
  • 可能还包括超时、工作目录、环境信息等 provider 相关字段。

可以用一个简化接口表达它的形态:

type ExecuteResult = {
  stdout: string
  stderr: string
  exitCode: number
  truncated?: boolean
}

interface SandboxBackend {
  execute(command: string): Promise<ExecuteResult>
}

execute() 之所以重要,是因为很多上层工具都可以基于它实现。比如读取文件可以执行 cat,列目录可以执行 ls,搜索内容可以执行 grep,写入文件可以通过 shell 重定向或临时脚本完成。

flowchart TD
    A[read_file] --> E[execute]
    B[write_file] --> E
    C[ls] --> E
    D[grep] --> E
    F[run_tests] --> E
    G[install_dependency] --> E

    E --> H[沙箱 Shell]
    H --> I[(沙箱文件系统)]
    H --> J[语言运行时和依赖]

这种设计有一个好处:DeepAgents 不需要为每种 provider 重新实现完整工具集,只要 provider 能提供一致的 execute() 能力,上层工具就可以复用同一套抽象。

文件访问分成内部操作和外部传输

Sandbox 中的文件处理有两条路径,不能混在一起理解。

一条路径是代理在沙箱内部操作文件。比如代理要查看 package.json、修改某个 Python 文件、搜索关键字、运行测试,这些动作发生在沙箱内部,通常通过 execute() 间接完成。

另一条路径是宿主应用和沙箱之间传输文件。比如任务开始前,把本地项目上传进沙箱;任务完成后,把生成的报告或修改后的代码下载回来。这类动作不一定经过 shell,而是通过 provider 提供的原生传输接口完成,例如 uploadFiles()downloadFiles()

文件路径发起方典型接口是否经过 shell用途
沙箱内部文件操作Agent 工具层read_filewrite_filelsgrep通常经过 execute()代理执行任务时读写沙箱文件
宿主与沙箱文件传输应用层uploadFiles()downloadFiles()通常不经过 shell任务开始前导入文件,任务结束后导出产物

一次典型执行流程如下:

sequenceDiagram
    participant App as 宿主应用
    participant Sandbox as Sandbox Backend
    participant Agent as DeepAgent
    participant Shell as 沙箱 Shell

    App->>Sandbox: uploadFiles(项目文件)
    App->>Agent: 发起任务
    Agent->>Sandbox: 调用 read_file / ls
    Sandbox->>Shell: execute("cat ... / ls ...")
    Shell-->>Sandbox: stdout / stderr / exit code
    Sandbox-->>Agent: 返回工具结果

    Agent->>Sandbox: 调用 write_file / run_tests
    Sandbox->>Shell: execute("写文件或运行测试命令")
    Shell-->>Sandbox: 执行结果
    Sandbox-->>Agent: 返回结果

    App->>Sandbox: downloadFiles(结果文件)
    Sandbox-->>App: 返回产物

这条边界很重要:代理不应该随意读取宿主文件,宿主也不应该把全部本地目录无差别塞进沙箱。传进去什么,代理才能在沙箱内处理什么。

一个简化的使用流程

不同 provider 的软件开发工具包 SDK(Software Development Kit)名称和参数会有差异,但整体使用模式通常类似:创建沙箱、上传输入、让代理执行任务、下载结果、销毁环境。

下面用伪代码表达这个流程,重点看职责划分,而不是具体库名。

// 1. 创建一个 Sandbox 后端
const sandbox = await createSandbox({
  image: "python:3.11",
  network: false,
  timeoutSeconds: 300,
})

// 2. 把需要处理的文件上传进沙箱
await sandbox.uploadFiles([
  {
    localPath: "./project",
    sandboxPath: "/workspace/project",
  },
])

// 3. 让 DeepAgent 使用这个 sandbox 作为执行后端
const agent = createDeepAgent({
  sandbox,
  tools: [
    "read_file",
    "write_file",
    "ls",
    "grep",
    "run_command",
  ],
})

// 4. 代理在沙箱中完成任务
const result = await agent.invoke({
  task: "修复 /workspace/project 中的测试失败问题,并运行测试",
})

// 5. 下载需要的结果
await sandbox.downloadFiles([
  {
    sandboxPath: "/workspace/project",
    localPath: "./output/project",
  },
])

// 6. 清理沙箱
await sandbox.dispose()

network: false 表示禁用网络访问。不是所有任务都能关闭网络,比如需要安装远程依赖时就可能需要访问网络;但对于只处理已上传文件、只运行本地测试的任务,关闭网络能减少数据外传风险。

如果任务需要安装依赖,可以把依赖预装进镜像,或者在受控阶段打开网络完成安装,再进入代理执行阶段关闭网络。具体做法取决于 provider 是否支持分阶段配置。

安全实践:不要把秘密交给沙箱

Sandbox 的使用原则可以归纳为一句话:让沙箱处理可公开的输入和可丢弃的中间结果,不让它持有长期有效的敏感凭证。

1. 不把 API 密钥放进沙箱环境变量

不要把数据库密码、云服务密钥、API(Application Programming Interface,应用程序编程接口)token 放进沙箱环境变量。代理一旦可以执行命令,就可能运行类似下面的操作:

env
cat ~/.config/some-tool/credentials
grep -R "api_key" /workspace

如果这些值存在于沙箱里,隔离宿主系统也无法阻止它们在沙箱内部被读取。

2. 敏感操作放在宿主工具里执行

如果代理确实需要访问某个受保护资源,不要把凭证下发给沙箱。更安全的方式是把敏感逻辑封装成宿主侧工具,让代理只能调用工具,而不能直接拿到凭证。

flowchart LR
    A[DeepAgent] --> B[请求调用工具<br/>例如 query_customer_status]
    B --> C[宿主工具服务]
    C --> D[(安全凭证存储)]
    C --> E[外部受保护系统]
    E --> C
    C --> B
    B --> A

    A -.不能直接读取.-> D

这种模式下,代理只能表达“我要查询某个状态”,真正的认证、权限校验、请求签名都在宿主侧完成。工具还可以增加参数校验、审计日志和速率限制。

3. 不需要网络时关闭网络

如果任务只是静态分析、代码修改、格式化、运行本地测试,网络访问通常不是必需的。关闭网络后,即使代理生成了外传命令,也无法直接把文件发送到外部地址。

任务类型是否建议开启网络说明
读取和修改已上传代码输入已经在沙箱内,不需要外部访问
运行本地单元测试依赖已准备好时可关闭网络
安装第三方依赖视情况可在初始化阶段开启,执行阶段关闭
调用外部 API谨慎优先使用宿主侧受控工具
抓取网页或访问远程仓库视情况需要限制域名、权限和输出内容

4. 给执行设置超时和输出限制

代理可能生成长时间运行的命令,比如死循环、持续监听服务或巨大日志输出。Sandbox 后端应该对执行过程设置限制:

const result = await sandbox.execute("pytest -q", {
  timeoutSeconds: 120,
  maxOutputBytes: 1024 * 1024,
})

超时限制避免任务无限占用资源,输出限制避免大日志拖垮调用链。返回结果里保留 truncated 字段,也能让上层知道输出是否被截断。

provider 的角色:提供一致后端,不提供相同底层

Sandboxes 可以由不同 provider 实现。Provider 负责创建隔离环境、运行命令、管理文件传输和释放资源;DeepAgents 只依赖统一接口,不需要关心底层到底是容器、云端 devbox,还是其他执行环境。

常见 provider 的侧重点不同:

Provider主要特点更适合的场景
Modal云端隔离运行,支持 GPU(Graphics Processing Unit,图形处理器)资源需要云端计算、模型相关任务、较重执行环境
Runloop快速创建 devbox 开发环境代码修改、测试、自动化开发任务
Daytona多语言环境和快速启动多技术栈项目、需要灵活运行时的任务

选择 provider 时,需要重点看这些能力:

  • 是否支持网络开关;
  • 是否支持自定义镜像或预装依赖;
  • 文件上传下载是否方便;
  • 命令执行是否有超时和输出限制;
  • 是否支持并发任务;
  • 沙箱销毁后数据是否彻底清理;
  • 日志和审计能力是否足够。

适合使用 Sandbox 的场景

Sandbox 适合那些“代理需要动手执行,但不能直接碰宿主机”的任务。

场景是否适合原因
自动修复代码并运行测试适合需要读写文件和执行测试命令
生成脚本并验证结果适合代码可以在隔离环境中运行
批量分析项目文件适合文件上传后在沙箱内处理
需要安装临时依赖的任务适合依赖不会污染宿主环境
直接访问生产数据库不适合凭证和权限风险高,应改用宿主侧受控工具
处理高度敏感文件谨慎需要评估 provider、网络隔离和数据清理策略
只做文本问答通常不需要没有代码执行和文件操作需求

设计取舍

Sandbox 带来的能力很明显:代理可以从“只会规划和生成文本”变成“能在隔离环境中完成可执行任务”。但它也引入了新的工程成本。

收益代价
代理可以真实运行代码和测试需要管理沙箱生命周期
宿主环境不被直接污染执行环境初始化会有额外耗时
不同任务可以使用不同依赖镜像、依赖和缓存需要维护
文件边界更清晰需要设计上传和下载策略
风险比直接本机执行更低仍要处理提示注入、网络外传和凭证管理

工程上最容易踩的坑是把 Sandbox 当成“绝对安全容器”。更合理的定位是:Sandbox 是执行隔离层,不是权限治理、密钥管理和内容安全的全部答案。

落地时的检查清单

实际接入 DeepAgents Sandboxes 时,可以按这张清单检查设计是否可靠。

检查项建议
输入文件范围只上传任务所需文件,不上传整个主目录
凭证处理不把长期密钥放进沙箱环境变量
网络访问默认关闭,需要时再按任务开启
执行限制设置超时、输出大小和资源限制
文件导出只下载明确需要的产物
日志记录记录命令、退出码、关键错误,避免记录敏感内容
沙箱销毁任务结束后释放环境并清理数据
敏感能力通过宿主侧工具封装,不让代理直接接触凭证

DeepAgents 的 Sandbox 设计核心可以压缩成三层:统一的 execute() 命令执行接口、基于它构建的代理工具层、与宿主系统隔离的文件传输通道。理解这三层之后,就能判断一个代理任务应该放进沙箱执行,还是应该通过宿主侧工具完成。


评论