第 7 章:沙箱、工作区与产物 —— 文件能力如何既好用又不越界
“给 Agent 一个 bash”远远不够。DeerFlow 需要让同一套工具在本地目录、远程容器、E2B 或 K8s 沙箱上保持相同路径语义。
Sandbox 抽象
Sandbox 定义最小文件/命令接口:
execute_command 执行命令,返回退出码与输出
read_file 按行/范围读取文本
download_file 读取二进制
list_dir 枚举目录
write_file 写入或追加
glob / grep 有界搜索
update_file 二进制更新2
3
4
5
6
7
工具层只依赖这个抽象。Provider 负责 acquire/get/release,把 thread_id + user_id 映射到具体 sandbox_id。
每线程隔离
沙箱生命周期以线程为主键,而不是以一次 run 为主键。这样同一对话下一轮仍能看到上一轮文件;不同用户即使碰巧使用相同 thread_id,也因复合 key 而隔离。
SandboxMiddleware 在 Agent 前准备 sandbox 与 thread_data,但工具也支持懒初始化。多个并行工具同时第一次触发初始化时,ThreadState 的 merge_sandbox 允许相同 ID 幂等合并,却拒绝不同 ID——这能把 provider 的竞态变成明确错误,而不是随机选一个工作区。
三类虚拟目录
模型主要看到:
| 虚拟路径 | 用途 | 典型权限 |
|---|---|---|
/mnt/data 或 workspace | Agent 工作文件 | 读写 |
| uploads | 用户上传的原始输入 | 通常只读/受控 |
| outputs | 准备交付给用户的文件 | 读写并可 present |
LocalSandbox 用 PathMapping(container_path, local_path, read_only) 做双向转换:命令入参从虚拟路径翻译到宿主真实路径,命令输出再反向替换,避免模型获得用户机器的绝对目录。
路径映射按“更具体路径优先”排序,否则 /mnt/data/uploads/a.pdf 可能先匹配 /mnt/data 而绕过 uploads 的只读规则。
Local 不等于隔离容器
LocalSandbox 只是统一路径和接口,不是强安全沙箱。命令仍在宿主执行,所以默认会隐藏 host bash。要让模型安全运行任意 shell,应选择 AioSandbox、E2B、BoxLite、Tenki 或 provisioner/K8s 等隔离 provider。
这个区分很重要:虚拟路径解决可移植性,容器/远程执行才解决进程隔离。
文件工具的防线
文件工具还会执行多层限制:
- 写入大小与读取输出有上限;
str_replace要求旧文本唯一匹配;- 路径在进入 provider 前正规化;
- secret 环境变量会从命令输出掩码;
glob/grep限制返回条数;- read-before-write middleware 要求先观察再修改;
- tool progress 事件只暴露安全摘要,不泄露密钥。
这些限制不是重复。沙箱负责“能否到达”,工具负责“参数是否合法”,中间件负责“行为是否符合当前策略”。
上传文件的双阶段处理
HTTP upload router 先把文件写到用户/线程上传区,做文件名、大小、权限和清理处理。UploadsMiddleware 在运行开始时扫描本轮可见上传,更新 uploaded_files,并让 list_uploaded_files 返回可供模型选择的清单。
子代理不注册该工具,因为它有独立 ThreadState,无法可靠判断“本轮新上传”。父 Agent 应在委派 prompt 中明确传入相关路径。
Artifact 与 Present 的区别
“文件已经写在 workspace”不等于“用户会收到”。present_file 是显式交付协议:
- 验证路径属于可交付区域;
- 把路径写入 ThreadState
artifacts; - 前端监听 state 后刷新 Artifact 面板;
- Gateway artifact API 按用户/线程权限读取内容。
Worker 结束时还会比较运行前后 workspace snapshot,生成 workspace changes。Artifact 回答“Agent 选择交付什么”,workspace changes 回答“这次运行实际改了什么”,两者不能混为一谈。
图片为何只存元数据
view_image 读取二进制并向视觉模型注入图片,但 ThreadState 的 viewed_images 只保存 MIME、大小和实际路径。把 base64 放进 state 会让每个 checkpoint 重复整张图片,长对话的数据库体积会指数式难看。
使用完后 middleware 可写空字典显式清除该通道,区别于 None 表示“本节点未触碰”。
源码锚点
sandbox/sandbox.pysandbox/middleware.pysandbox/local/local_sandbox.pytools/builtins/present_file_tool.py
下一章看 DeerFlow 如何把一部分工作切到独立上下文,同时保持父子运行的身份和状态一致。