1. 为什么代码执行需要独立的安全模型
上一篇介绍了虚拟文件系统与 Backend。当 Backend 具备命令执行能力后,Agent 可以运行测试、处理数据、构建项目并生成报告,但风险也从“回答不准确”升级成了“在真实系统中产生副作用”。
python analyze.py
pip install some-package
git clone https://example.com/project.git
curl https://example.com/script.sh | sh
命令本身不是唯一风险源。用户输入、网页内容、MCP 返回值、Skill、依赖包和模型输出都可能影响最终命令。只要其中存在一段不可信内容,整条执行链就不能按照普通业务代码对待。
1.1 Sandbox 是封闭实验室,而不是绝对安全证明
可以把 Sandbox 类比为封闭实验室:隔离的目的不是保证内部永不出错,而是限制错误或攻击的影响范围。
一个远程容器即使与 Host 隔离,仍然可能发生:
- Prompt Injection 诱导 Agent 执行错误操作;
- 注入 Sandbox 的密钥被代码读取并输出;
- 开放网络被用于外传数据或访问内网;
- 无限循环、Fork Bomb 或超大日志耗尽资源;
- 多租户复用工作区造成数据串读;
- 恶意依赖在构建期间运行安装脚本;
- 恶意产物下载后重新跨越隔离边界。
因此,Sandbox 只是纵深防御的一层。它必须与 Tool 权限、人工审批、网络策略、资源配额、密钥边界、产物检查和生命周期清理共同使用。
1.2 先画出信任边界

在选择 Provider 之前,先列出资产、入口和允许的数据流:
| 需要保护的对象 | 典型风险 | 首要控制 |
|---|---|---|
| Host 文件和进程 | 读取 .env、SSH Key,修改源码,终止服务 | 真实执行隔离,不挂载无关目录 |
| 内部网络 | 访问 Metadata Endpoint、扫描内网服务 | 默认禁网或目的地白名单 |
| 第三方系统 | 使用 Token 修改数据、产生费用 | 短期凭据、窄接口 Tool、HITL |
| 其他租户 | 缓存、进程和历史文件串读 | 独立 Sandbox、身份绑定、TTL |
| Agent 上下文 | 敏感输出重新发送给模型 | 输出过滤、最小上下文、审计 |
| 下载产物 | 宏、脚本、压缩炸弹、路径穿越 | 白名单下载、类型和恶意内容扫描 |
计算两个整数、分析内部财务 CSV、编译陌生 Git 仓库,所需的隔离等级完全不同。安全设计必须从威胁模型出发,而不是看到 “Sandbox” 这个名称就默认可信。
1.3 四类控制各自解决什么问题
| 控制 | 回答的问题 | 无法独立解决的问题 |
|---|---|---|
| FilesystemPermission | 内置文件 Tool 能访问哪些虚拟路径 | Shell、自定义 Tool、MCP 和 SDK 直接访问 |
| interrupt_on / HITL | 哪次敏感 Tool Call 需要人审查 | 审批后代码在什么环境运行 |
| Sandbox | 命令在哪个隔离环境执行 | 路径业务规则、网络出口和凭据最小化 |
| Backend Policy | 自定义校验、审计、限流和内容检查 | OS 级隔离本身 |
例如,read_file 被禁止读取 /secrets/token,不代表 execute(“cat /secrets/token”) 也会被禁止;两者走的是不同能力入口。
2. FilesystemPermission 的规则与真实边界
本文主线锁定 deepagents==0.6.12。以下默认行为、Tool 清单和异常类型均以这一安装版本的源码与测试为准;快速演进项目不能把“当前官网”自动等同于“旧版本运行时”。
2.1 三个字段
from deepagents import FilesystemPermission
rule = FilesystemPermission(
operations=["read", "write"],
paths=["/workspace/**"],
mode="allow",
)
| 字段 | 可选值 | 含义 |
|---|---|---|
| operations | read、write | 一条规则可以覆盖一种或两种操作 |
| paths | 绝对 Glob 列表 | 支持 **、{a,b} 等匹配语法 |
| mode | allow、deny、interrupt | 放行、拒绝或进入人工审批 |
路径 Pattern 必须以 / 开头,不能包含 ..,~ 也不受支持。错误配置会在构造规则时尽早失败:
FilesystemPermission(
operations=["read"],
paths=["relative/**"], # ValueError
)
2.2 锁定版本与当前文档的 delete 差异
这是本章最容易被版本变化误导的地方。
| 信息来源 | read 覆盖 | write 覆盖 |
|---|---|---|
| 本文安装的 deepagents==0.6.12 | ls、read_file、glob、grep | write_file、edit_file |
| 当前官方 Permissions 文档 | 上述读取 Tool | write_file、edit_file、delete |
本文运行时的 _DEFAULT_FS_TOOL_OPS 和实际 Tool 列表都没有 delete。P05 测试明确断言了这一点,避免把较新文档中的能力倒写成 0.6.12 已有功能。
较新版本已经把内置 delete 纳入 write 权限,并对目录执行“目标路径及所有后代均通过才整体删除”的检查。升级后应增加以下测试:
- 删除允许路径;
- 删除受保护文件;
- 删除包含受保护后代的目录时整体失败;
- 删除需要审批的目录时先中断、审批后再执行。
如果必须停留在 0.6.12,不要自行暴露一个无鉴权的删除 Tool。自定义删除能力必须在 Tool 内完成路径归一化、根目录约束、后代检查、审批和审计,因为 FilesystemPermission 不会自动保护自定义 Tool。
相关版本说明可对照 Deep Agents Permissions。
2.3 第一条匹配规则生效,未匹配默认允许
规则按声明顺序检查,第一个同时匹配 operation 和 path 的规则立即生效。下面的宽泛规则会提前放行 .env:
wrong_order = [
FilesystemPermission(
operations=["read", "write"],
paths=["/workspace/**"],
mode="allow",
),
FilesystemPermission(
operations=["read", "write"],
paths=["/workspace/.env"],
mode="deny",
),
]
另一个容易误解的规则是:没有任何规则匹配时默认 allow。仅允许 /workspace/** 并不会自动拒绝 /outside/**,最小权限策略必须在末尾增加兜底拒绝。
PERMISSIONS = [
FilesystemPermission(
operations=["read", "write"],
paths=["/workspace/.env", "/secrets/**"],
mode="deny",
),
FilesystemPermission(
operations=["write"],
paths=["/reports/**"],
mode="interrupt",
),
FilesystemPermission(
operations=["read"],
paths=["/workspace/**", "/reports/**"],
mode="allow",
),
FilesystemPermission(
operations=["write"],
paths=["/workspace/**"],
mode="allow",
),
FilesystemPermission(
operations=["read", "write"],
paths=["/**"],
mode="deny",
),
]
这组策略的确定性矩阵如下:
| 操作 | 路径 | 结果 |
|---|---|---|
| 读取 | /workspace/readme.md | allow |
| 写入 | /workspace/result.md | allow |
| 读取 | /workspace/.env | deny |
| 写入 | /secrets/token.txt | deny |
| 写入 | /reports/final.md | interrupt |
| 读写 | /outside/** | deny |

2.4 deny 必须同时验证消息和副作用
在锁定版本中,直接调用内置 write_file 底层函数,拒绝结果是 status=”error” 的 ToolMessage,Backend 不会收到写入请求:
denied = write_file.func(
file_path="/secrets/token.txt",
content="must-not-exist",
runtime=runtime,
)
assert denied.status == "error"
assert "permission denied" in str(denied.content)
assert not (root / "secrets" / "token.txt").exists()
不过 .func 是当前 StructuredTool 的实现入口,不能单独代表长期稳定的 Agent 契约。因此本文还用确定性 Tool Calling 模型走过完整的 Agent → ToolNode → write_file 链路,并断言:
- 模型确实生成 write_file Tool Call;
- Agent State 中出现错误 ToolMessage;
- 目标文件没有产生;
- 模型收到 Tool 结果后继续生成最终回答。
这种测试比只比较异常文本更可靠,因为安全控制最终要保证“副作用没有发生”。
2.5 interrupt 的完整恢复流程
interrupt 不是稍后自动重试。应用必须读取 action_requests,为每项请求按顺序提交 decision,再用同一 thread_id 和 Command(resume=…) 从中断点恢复。
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import Command
agent = create_deep_agent(
model=model,
backend=backend,
permissions=PERMISSIONS,
checkpointer=InMemorySaver(),
)
config = {"configurable": {"thread_id": "fs-review-001"}}
paused = agent.invoke(
{"messages": [{"role": "user", "content": "发布最终报告"}]},
config=config,
version="v2",
)
requests = paused.interrupts[0].value["action_requests"]
assert requests[0]["name"] == "write_file"
resumed = agent.invoke(
Command(resume={"decisions": [{"type": "approve"}]}),
config=config,
version="v2",
)
本文版本自动生成的 review config 允许 approve、edit、reject 和 respond。edit 是否应该开放,必须结合具体 Tool 参数决定;不应允许审核人把受控 Tool 改名成另一种能力。多个 Tool Call 同时中断时,decision 数量和顺序必须与 action_requests 一一对应。仅再次调用相同线程不会自动批准。
ls、glob、grep 等批量工具会根据搜索子树是否可能覆盖受保护前缀进行保守判断。应优先使用 /secrets/** 这类带固定前缀的 Pattern;/**/secrets 可能退化到根范围并造成过度中断。
完整恢复协议可参考 Deep Agents Human-in-the-loop。
2.6 权限不覆盖哪些入口
在锁定版本中,permissions= 保护的内置 Tool 是:
ls, read_file, glob, grep, write_file, edit_file
它不会自动保护:
- execute Shell 命令;
- 自定义 Python Tool;
- MCP Tool 和 Provider 原生 Tool;
- Interpreter 通过 PTC 暴露的 Tool;
- Sandbox SDK 的 upload_files() / download_files();
- 应用代码直接访问文件、数据库或对象存储。
对 SandboxBackendProtocol Backend 直接配置覆盖 Sandbox default 区域的文件权限,会在 Agent / FilesystemMiddleware 装配阶段抛出 NotImplementedError。这不是 Backend 执行命令时动态拒绝,而是框架拒绝制造一个虚假的路径安全边界。
CompositeBackend 的例外是:权限 Pattern 可以全部落在明确的非 Sandbox route 下。例如 default 是 Sandbox,而 /memories/ 路由到 StoreBackend,则可以限制 /memories/;/workspace/ 或 /** 触及 Sandbox default 时仍会在装配阶段失败。P05 同时测试了允许与拒绝这两种组合。
2.7 SubAgent 是继承还是替换
SubAgent 未声明 permissions 时继承主 Agent 规则;一旦显式声明,就是完整替换,不是追加。
readonly_reviewer = {
"name": "reviewer",
"description": "审核报告,不得修改文件",
"system_prompt": "只检查事实、数字和引用。",
"permissions": [
FilesystemPermission(
operations=["read"],
paths=["/reports/**"],
mode="allow",
),
FilesystemPermission(
operations=["read", "write"],
paths=["/**"],
mode="deny",
),
],
}
子智能体规则同样需要兜底拒绝。角色是“内部 Reviewer”并不意味着它天然可信。
3. execute 能力与 LocalShellBackend
3.1 SandboxBackendProtocol 是能力协议
在 Deep Agents 类型系统中,实现 SandboxBackendProtocol 表示 Backend 能提供 execute() / aexecute() 和唯一 id,从而使文件中间件可以调用命令执行能力。
协议名中的 Sandbox 不等于实现已经具备容器、VM 或 MicroVM 隔离。隔离强度取决于实际 Provider。LocalShellBackend 就是最明确的反例。
执行结果使用结构化对象:
ExecuteResponse(
output="...",
exit_code=0,
truncated=False,
)
调用方必须同时检查 exit_code、output 和 truncated。看到 stdout 不代表命令成功。
3.2 LocalShellBackend 直接运行宿主 Shell
import os
from deepagents.backends import LocalShellBackend
backend = LocalShellBackend(
root_dir=workspace.resolve(),
virtual_mode=True,
timeout=5,
max_output_bytes=256,
env={"PATH": os.environ.get("PATH", "/usr/bin:/bin")},
inherit_env=False,
)
锁定版本内部使用:
subprocess.run(
command,
check=False,
shell=True,
capture_output=True,
stdin=subprocess.DEVNULL,
text=True,
timeout=effective_timeout,
env=self._env,
cwd=str(self.cwd),
)
这意味着管道、重定向、变量展开、命令替换和其他 Shell 元字符都会生效。P05 在临时目录中执行了固定命令:
printf 'safe fixture' | tr 'a-z' 'A-Z' > shell.txt
结果文件是 SAFE FIXTURE,证明这里具有完整 Shell 语义,而不是参数数组式进程调用。
LocalShellBackend 不仅缺少执行隔离,还会用 shell=True 解释命令。绝不能把用户输入、网页文本或模型生成的未经约束参数拼接进“可信”命令模板。virtual_mode、超时和环境变量白名单都不能阻止命令注入。
它只适合可信个人项目的临时调试,或已经位于独立隔离 Worker 内的受控测试。不适合多租户服务、不可信仓库和持有生产凭据的 Host。
3.3 virtual_mode=True 限制不了 Shell
virtual_mode=True 约束文件 Backend 的虚拟路径解析,但 Shell 仍然可以访问 Host 可见的绝对路径:
cat /etc/hosts
python -c "from pathlib import Path; print(Path.home())"
root_dir 只是命令工作目录,不是 chroot,也不是 mount namespace。P05 只在 Pytest 创建的临时目录里运行确定性命令,这用于验证 API,不能作为生产安全方案。
3.4 环境变量要显式白名单
示例设置 inherit_env=False,只传入执行 Python 所需的 PATH。测试先向 Host 环境加入一个临时敏感变量,再确认子命令看不到它。
不要把 Host 环境整体复制给模型可控进程。云密钥、数据库密码、代理地址、Tracing Token 和 CI 凭据都可能被命令打印或发送到网络。
远程 Sandbox 同样应使用按任务注入的短期凭据。如果任务只是调用某个业务 API,更安全的方案通常是把密钥留在 Host,通过参数受限的 Tool 代为调用,而不是把通用 Token 放进 Sandbox。
3.5 超时与输出上限不是进程隔离
timeout=5
max_output_bytes=256
max_output_bytes 限制保留到返回对象的文本,截断后还会追加说明,所以最终字符串可能略大于 256 字节。应判断 truncated is True,不能只断言字符串长度。
timeout 首先是一次调用的等待上限,不能自动当作完整进程树终止保证。不同 Backend / Provider 对子进程、后台服务和超时清理的语义不同。生产环境仍需要 Provider 级进程管理、PID 限制、cgroup / 配额和 Sandbox 生命周期清理。
4. BaseSandbox 与 Daytona 适配
4.1 BaseSandbox 提供什么抽象
锁定版本的 BaseSandbox 以 execute() 为核心,并在其上实现 ls、read、glob、grep、write 和 edit 等能力。子类的抽象成员是:
id
execute(command, *, timeout=None) -> ExecuteResponse
upload_files(files) -> list[FileUploadResponse]
download_files(paths) -> list[FileDownloadResponse]
仅定义四个同名函数还不够。实现必须遵守准确签名、返回类型、绝对路径、错误和部分失败契约,并处理 Provider 身份、生命周期、超时和异常转换。
BaseSandbox 是适配器基类,不会凭空创造安全隔离。真正边界仍来自容器、MicroVM、虚拟机、内核策略和网络控制。
4.2 Daytona SDK 与 DaytonaSandbox 是两层对象
daytona.Daytona / daytona.Sandbox
创建和删除环境,提供原生 Process、Session 与文件能力
langchain_daytona.DaytonaSandbox
把 SDK Sandbox 适配成 Deep Agents SandboxBackendProtocol
本文锁定环境的实际签名是:
Daytona.create(params=None, *, timeout=60, ...) -> Sandbox
Daytona.delete(sandbox, timeout=60) -> None
DaytonaSandbox(*, sandbox, timeout=1800, sync_polling_interval=0.1)
因此示例使用 Client 生命周期 API,而不是未经该版本验证的 sandbox.delete():
from daytona import Daytona
from langchain_daytona import DaytonaSandbox
daytona = Daytona()
sandbox = daytona.create()
try:
backend = DaytonaSandbox(sandbox=sandbox, timeout=60)
# upload -> execute -> download
finally:
daytona.delete(sandbox)
Daytona SDK 会演进;其他版本可能同时提供实例方法。升级时应重新运行 inspect.signature() 和真实云端 Contract Test,不能只复制旧博客中的清理写法。
4.3 Host 和 Agent 有两条文件通道
| 通道 | 调用者 | 用途 | 权限来源 |
|---|---|---|---|
| Agent 文件 Tool | 模型 | 任务中读取、编辑和搜索 Sandbox 文件 | Agent Tool / Backend Policy |
| SDK 上传下载 | Host 应用 | 运行前注入输入,运行后取回产物 | 应用白名单与身份授权 |
Host 不应允许模型自由决定任意下载路径。更安全的流程是由应用预先定义输入清单与允许导出的产物清单,然后分别调用 upload_files() 和 download_files()。
4.4 上传、执行、下载和清理
daytona = Daytona()
sandbox = daytona.create()
try:
backend = DaytonaSandbox(sandbox=sandbox, timeout=60)
upload = backend.upload_files(
[("/workspace/input.txt", b"1\n2\n3\n4\n5\n")]
)[0]
if upload.error:
raise RuntimeError(f"upload failed: {upload.error}")
result = backend.execute(command, timeout=30)
if result.exit_code != 0:
raise RuntimeError(f"command failed: {result.output}")
download = backend.download_files(["/workspace/result.txt"])[0]
if download.error or download.content is None:
raise RuntimeError(f"download failed: {download.error}")
assert download.content.decode() == "15"
finally:
daytona.delete(sandbox)
Deep Agents 的 DaytonaSandbox Adapter 统一要求传输路径是绝对路径;相对路径返回 invalid_path。这属于 Sandbox Backend Adapter 契约,不等于 Daytona 原生 SDK 的所有文件接口都采用相同路径规则。批量上传下载还应逐项检查 error,因为部分请求可能成功、部分失败。
4.5 本地 Fake SDK 能证明什么
当前环境没有 DAYTONA_API_KEY,因此真实 Daytona Cloud 测试没有执行。本文使用 Fake SDK 驱动真实 langchain_daytona.DaytonaSandbox,覆盖:
- Session 创建与删除;
- 命令提交、状态查询和日志读取;
- 字节上传与下载;
- 相对路径拒绝;
- 缺少 DAYTONA_API_KEY 时入口明确失败。
这只能证明本文 Fake SDK 与 Adapter 之间的本地交互路径正确,不能证明真实返回字段、网络错误、日志分页、权限、限流和 SDK / Cloud 版本兼容。真实云端 Contract Test 仍是上线前的必要步骤,本文不会把 Fake 结果写成云端成功记录。
配置真实密钥后可执行:
cd source/_posts/deepagent/examples
export DAYTONA_API_KEY="从密钥管理器注入的值"
.venv_deepagent/bin/python -m p05_permissions_sandbox.daytona_execute
预期脱敏输出是 sandbox result: 15。缺少密钥时程序抛出明确的配置错误,不会创建匿名资源,也不会把其他环境变量输出到日志。
5. 生产 Sandbox 还需要哪些控制
5.1 生命周期与租户隔离
| 作用域 | 状态复用 | 成本 | 主要风险 |
|---|---|---|---|
| 每 Tool Call | 最低 | 最高 | 启动频繁、文件难复用 |
| 每 Run | 同一任务 | 较高 | 多轮对话不能直接续用 |
| 每 Thread | 同一会话 | 中等 | 必须绑定身份并设置 TTL |
| 每 Assistant | 跨会话 | 较低 | 多租户串读和残留进程风险最高 |
Thread-scoped 是官方文档重点介绍的常见方案之一:同一对话复用依赖和文件,再通过 TTL 清理。它不是唯一标准答案。最终选择取决于并发、成本、恢复、敏感度和租户隔离要求。
5.2 网络出口和内网访问
Sandbox 隔离 Host 文件,不代表网络默认关闭。允许互联网访问时,Prompt Injection 可以借助 HTTP、DNS、Git Push 或包管理请求外传数据。
生产策略通常包括:
- 默认拒绝出口,按任务开放域名或服务;
- 禁止云 Metadata、控制面和内部网段;
- 包下载通过固定镜像、代理和哈希校验;
- 记录目的域名、方法、流量和失败原因;
- 将公开网页抓取与内部敏感数据处理放入不同环境。
5.3 CPU、内存、磁盘和进程
除了命令 timeout,还要设置:
- CPU 核数或时间配额;
- 内存上限和 OOM 行为;
- 临时磁盘容量、inode 与文件数;
- 最大进程数、线程数和文件描述符;
- stdout / stderr、上传和下载大小;
- Sandbox 最长生存时间和后台进程清理。
超时只让调用方返回错误并不够,Provider 必须确保残留进程最终被终止,资源被可靠回收。
5.4 密钥边界
安全优先级从高到低通常是:
- 不把密钥放入 Sandbox,通过窄接口 Tool 代办;
- 注入短期、单用途、最小范围凭据;
- 限制凭据可访问的网络目标和资源;
- 禁止凭据进入 stdout、Trace、报告和产物;
- 任务结束后立即撤销或等待短 TTL 失效。
持有生产管理员密钥的环境不应直接执行模型生成的代码。
5.5 产物下载会重新穿过信任边界
应用不应自动打开或执行下载文件。至少要:
- 仅下载预先声明路径;
- 限制文件数量、单文件和总大小;
- 校验扩展名、MIME 和 Magic Number;
- 防止 Zip Slip、符号链接逃逸和压缩炸弹;
- 扫描脚本、二进制和 Office 宏;
- 移除报告中的密钥、内部地址和绝对路径;
- 先写入隔离对象存储,而不是覆盖 Host 项目。
5.6 Docker 不自动等于安全 Sandbox
企业可以基于容器、MicroVM 或虚拟机,并通过 BaseSandbox 适配器建设内部执行环境。但仅仅启动一个 Docker 容器仍然不够:
- 不挂载 Docker Socket,不使用 privileged;
- 使用非 root 用户和只读基础文件系统;
- 仅挂载必要目录,并为任务分配独立临时盘;
- 收紧 Linux Capability、Seccomp、AppArmor 等策略;
- 隔离控制面和执行面;
- 固定镜像摘要并扫描依赖;
- 设置网络、资源、租户和生命周期限制。
高风险任务可以使用 MicroVM 或独立虚拟机增强内核边界,但仍不能省略网络、密钥与产物控制。
6. QuickJS、Tool 与 Sandbox 怎样选
6.1 QuickJS Interpreter 的定位
当前官方文档把 Interpreters 标记为 Beta。QuickJS 运行在应用进程内的嵌入式 JavaScript Runtime,适合在 Agent Loop 内完成循环、分支、聚合和受控 Tool 编排。默认没有文件、网络和 Agent Tool 能力;通过 Programmatic Tool Calling 暴露的 Tool 构成能力白名单。
本文锁定环境没有安装 langchain-quickjs,也没有执行 PTC 与 HITL 的交互测试,因此不会断言“PTC 一定绕过”或“一定经过”普通 interrupt_on。升级或引入 Interpreter 时,应针对锁定版本分别测试普通 Tool Call 与 PTC Call 是否进入 Middleware 和审批链。
官方能力状态可参考 Deep Agents Interpreters。
6.2 多维选型,而不是线性升级

| 方案 | 系统能力 | 隔离强度 | 启动成本 | 典型用途 |
|---|---|---|---|---|
| 普通 Tool | 单一窄能力 | 由 Tool 实现决定 | 低 | API 查询、确定性业务操作 |
| QuickJS | JS 控制流和 PTC | 同进程能力边界 | 低 | 聚合、循环、受控 Tool 编排 |
| LocalShell | 完整 Host Shell | 无 | 低 | 可信本地临时调试 |
| 容器 Sandbox | Shell、文件、依赖 | 取决于容器加固 | 中 | 构建、测试、数据处理 |
| MicroVM / 远程 Sandbox | 完整执行环境 | 较强 | 中到高 | 多租户、不可信代码、高风险构建 |
Tool、QuickJS、LocalShell 和远程 Sandbox 不是“越来越高级”的单一直线。应同时评估代码可信度、系统能力、隔离、状态复用、网络、依赖安装和审计要求。
6.3 纵深防御清单

完整执行链至少包括:
- 输入信任:标记用户、网页、文件和 Skill 中的不可信内容;
- Tool Policy:最小化文件和业务能力;
- HITL:高风险调用暂停并人工审查;
- Sandbox:代码进入隔离执行面;
- 资源限制:限制 CPU、内存、进程、时间和磁盘;
- 网络策略:默认拒绝或目的地白名单;
- 密钥边界:短期、最小权限、按任务注入;
- 产物检查:限制路径、大小、格式和恶意内容;
- 生命周期:身份绑定、TTL、停止和删除;
- 审计:记录 Tool Call、审批、命令、退出码和产物哈希。
每一层都可能失败,不能把其中任何一层单独作为安全证明。
7. 测试、验证状态与故障排查
7.1 锁定环境
| 组件 | 版本 | 验证状态 |
|---|---|---|
| Python | 3.12.11 | 本地执行 |
| deepagents | 0.6.12 | 源码核对、离线集成测试 |
| langchain | 1.3.13 | 离线集成测试 |
| langchain-daytona | 0.0.7 | Fake SDK 路径测试 |
| daytona | 0.197.0 | 已安装、签名核对,未连接 Cloud |
| Daytona Cloud | — | 未执行:缺少 DAYTONA_API_KEY |
| QuickJS Interpreter | — | 未安装、未验证 |
| 本地系统 | macOS / arm64 | LocalShell 临时目录测试 |
版本已安装不等于外部服务已验证。表中的验证等级必须与文章结论严格对应。
7.2 实际运行命令与结果
cd source/_posts/deepagent/examples
.venv_deepagent/bin/python -m ruff check p05_permissions_sandbox
.venv_deepagent/bin/pytest p05_permissions_sandbox -q
实际结果:
All checks passed!
...................... [100%]
22 passed
22 项离线测试覆盖:
- 七组 allow、deny、interrupt 权限矩阵;
- 相对 Pattern 在构造阶段失败;
- 底层 write_file.func 的允许与拒绝;
- 完整 Agent / ToolNode 路径返回错误 ToolMessage 且无副作用;
- interrupt 暂停、action_requests、同线程 Command(resume) 与审批后写入;
- 0.6.12 没有内置 delete Tool;
- Sandbox default 与全局权限在装配阶段冲突;
- Composite 非 Sandbox route 可以独立配置权限;
- LocalShell 确定性执行、完整 Shell 语义、环境变量不继承和输出截断;
- Daytona Adapter Session、日志、传输与绝对路径;
- 缺少 Daytona 密钥时云端入口明确失败。
真实 Daytona Cloud:not run。这 22 项不能被解释成云端服务已经成功创建 Sandbox。
7.3 常见错误
| 现象 | 根因 | 处理方式 |
|---|---|---|
| /outside 仍可写 | 未匹配默认 allow | 最后一条增加 /** deny |
| .env 仍可读 | 宽泛 allow 排在前面 | 具体 deny 放在首位 |
| 找不到内置 delete | 运行的是 0.6.12 | 按版本能力编写测试,升级后重新验收 |
| 重复调用线程仍未恢复 | 没有提交 resume decisions | 使用相同 config 与 Command(resume=…) |
| 大量 glob 要求审批 | interrupt Pattern 无固定前缀 | 改成 /prefix/** |
| 自定义 Tool 绕过权限 | Permissions 只保护内置文件 Tool | 在 Tool 或 Backend Policy 中授权 |
| 配权限后 Agent 构造失败 | Pattern 触及 Sandbox default | 限定 Composite 非 Sandbox route |
| virtual_mode 后仍可读 Host | 它不是 OS 隔离 | 使用真实 Sandbox |
| 命令有输出但任务失败 | 未检查 exit_code | 检查退出码、输出和截断状态 |
| Daytona 留下资源 | 异常路径未执行清理 | Client delete() 放进 finally |
| 下载后 Host 出现风险 | 产物跨越隔离边界 | 白名单下载并扫描 |
8. 总结
FilesystemPermission 解决的是内置文件 Tool 的虚拟路径规则,不是通用文件 ACL。第一条匹配生效、未匹配默认允许、SubAgent 显式规则会替换父规则,这三点必须用测试固定。deny 要同时检查错误消息和副作用;interrupt 则必须配合 Checkpointer、同一 thread_id、按序 decisions 与 Command(resume) 才能完成闭环。
版本边界同样属于正确性的一部分:当前官方文档已经包含 delete,但本文锁定的 deepagents==0.6.12 运行时没有该 Tool,不能伪造删除测试。升级后应针对目录后代权限重新验收。
SandboxBackendProtocol 表示命令执行能力,不等于安全隔离。LocalShellBackend 使用 shell=True 直接运行 Host Shell,virtual_mode、超时和环境白名单都不能把它变成 Sandbox。生产代码执行应使用经过加固的远程或独立执行环境,并同步控制网络、资源、密钥、产物、生命周期和审计。