主题
沙箱架构与文件工具
本章目标:
- 理解沙箱抽象层(Sandbox / SandboxProvider)与两种实现(Local / Aio)的取舍
- 看懂虚拟路径映射:Agent 看到的
/mnt/user-data/*如何翻译成宿主物理路径- 弄清
allow_host_bash: true的安全边界与 a-cdm 为何接受这个权衡
TL;DR
沙箱给 Agent 提供"可执行命令 + 读写文件"的隔离环境。抽象层是 Sandbox(execute_command/read_file/write_file/list_dir)+ SandboxProvider(acquire/get/release 生命周期)。a-cdm 用 LocalSandboxProvider(单例,直接在宿主机执行),Agent 看到的虚拟路径 /mnt/user-data/{workspace,uploads,outputs}、/mnt/skills、/mnt/kingdee 由 path_mappings 翻译成宿主 backend/.deer-flow/threads/{thread_id}/...。文件工具(ls/read_file/write_file/str_replace/grep/glob/bash)在 sandbox/tools.py。config.yaml 开了 allow_host_bash: true,意味着 LocalSandbox 不是安全隔离边界——a-cdm 因"内部可信、非公开"接受这个权衡。
Overview(为什么 Agent 需要沙箱,又为什么是"虚拟路径")
让 LLM 跑 bash、读写文件是 Agent 能"干活"(分析数据、生成报告、跑脚本)的前提。但直接给 LLM 宿主机的真实路径有两个问题:① 不同 thread 的文件会互相污染;② LLM 可能拼出 /etc/passwd 这种宿主敏感路径。
deer-flow 的解法是虚拟路径沙箱:Agent 永远只看到 /mnt/user-data/workspace 这类稳定的虚拟路径,沙箱层在执行前把虚拟路径翻译成 backend/.deer-flow/threads/{thread_id}/user-data/workspace 这种按 thread 隔离的物理路径。这样 LLM 的"心智模型"是干净的容器视角,实际落盘按线程隔离,prompt 里也不暴露宿主真实结构。SandboxProvider 抽象则让"本地直接执行"和"Docker 真隔离"可以换实现而 Agent 代码不变。
Architecture:抽象层 + 两种实现
| 抽象 | 定义 | 关键方法 | Source |
|---|---|---|---|
Sandbox(ABC) | 执行环境接口 | execute_command read_file write_file list_dir | deer-flow/backend/packages/harness/deerflow/sandbox/sandbox.py:6-85 |
SandboxProvider | 生命周期管理 | acquire get release | deer-flow/backend/packages/harness/deerflow/sandbox/sandbox_provider.py |
两种实现(config.yaml 的 sandbox.use 选):
| 实现 | 隔离 | a-cdm 用 | Source |
|---|---|---|---|
LocalSandboxProvider | 无(直接宿主机执行,单例) | ✓ 当前 | sandbox/local/local_sandbox_provider.py:13 |
AioSandboxProvider | docker-in-docker 真隔离 | 长期目标(未启用) | community/aio_sandbox(config.yaml:698) |
LocalSandboxProvider 是单例(local_sandbox_provider.py:10、:103-106):acquire() 时若 _singleton is None 就建 LocalSandbox("local", path_mappings=...),所有 thread 共用同一个 LocalSandbox 实例(sandbox_id 恒为 "local"),靠虚拟路径里的 thread_id 做隔离而非靠多实例。release() 是 no-op(单例无需清理,local_sandbox_provider.py:116)。_RESERVED_CONTAINER_PREFIXES(:51)保留 /mnt/skills、/mnt/acp-workspace、/mnt/user-data 三个前缀不被用户 mount 覆盖。
Components:虚拟路径系统
| Agent 视角虚拟路径 | 宿主物理路径 | 用途 | Source |
|---|---|---|---|
/mnt/user-data/workspace | backend/.deer-flow/threads/{tid}/user-data/workspace | Agent 工作区 | deer-flow/backend/CLAUDE.md(Virtual Path) |
/mnt/user-data/uploads | 同上 /uploads | 用户上传文件 | 同上 |
/mnt/user-data/outputs | 同上 /outputs | 产出文件(present_files 才可见) | 同上 |
/mnt/skills | deer-flow/skills/ | 技能目录 | config.yaml:802-811 |
/mnt/kingdee | /data/erp-wiki/kingdee(bind mount) | ERP wiki(a-cdm 定制 mount) | config.yaml:680-683 |
/mnt/acp-workspace | {base}/threads/{tid}/acp-workspace(只读) | ACP agent 工作区 | deer-flow/backend/CLAUDE.md |
翻译靠 replace_virtual_path() / replace_virtual_paths_in_command(),is_local_sandbox() 判 sandbox_id == "local"(deer-flow/backend/CLAUDE.md Virtual Path System)。/mnt/kingdee 是 a-cdm 定制——config.yaml:680-683 配 sandbox.mounts 把它映射到宿主 /data/erp-wiki/kingdee,而 langgraph 容器又做了同名 bind mount(见 Aegra 章),确保 sandbox bash 在容器内 subprocess 跑时该路径可达。
文件工具速查表
config.yaml:604-636 把这些工具按 group 注册,use 指向 sandbox/tools.py:
| 工具 | group | 作用 | 截断 | Source |
|---|---|---|---|---|
ls | file:read | 目录树(≤2 层) | ls_output_max_chars(20000) | config.yaml:605-607 |
read_file | file:read | 读文件(可指定行范围) | read_file_output_max_chars(50000),head 截断 | config.yaml:609-611 |
glob | file:read | 文件名匹配(max 200) | — | config.yaml:613-616 |
grep | file:read | 内容搜索(max 100) | — | config.yaml:618-621 |
write_file | file:write | 写/追加,自动建目录 | — | config.yaml:623-625 |
str_replace | file:write | 子串替换(单/全部) | 同路径串行化按 (sandbox.id, path) scope | config.yaml:627-629 |
bash | bash | 执行命令(路径翻译) | bash_output_max_chars(20000),中间截断 | config.yaml:631-636 |
bash 工具仅在隔离 shell 或显式 allow_host_bash: true 时激活(config.yaml:632-633 注释)。截断策略有讲究:bash 用中间截断(头+尾,因错误可能出现在任何位置),read_file/ls 用头截断(内容前置)。
Data Flow:一次 bash 调用
Configuration / 安全标注
| 配置 | 值 | 安全含义 | Source |
|---|---|---|---|
sandbox.use | LocalSandboxProvider | ⚠️ 无隔离,直接宿主机执行 | deer-flow/config.yaml:667 |
sandbox.allow_host_bash | true | ⚠️⚠️ 放开宿主 shell,LocalSandbox 不是安全边界 | deer-flow/config.yaml:671-673 |
sandbox.mounts | kingdee read_only: false | ⚠️ Agent 可写 ERP wiki 目录 | deer-flow/config.yaml:680-683 |
bash_output_max_chars | 20000 | 中间截断 | deer-flow/config.yaml:689 |
allow_host_bash: true 是本项目最高风险的单个配置。config.yaml:671-673 注释写明权衡:LocalSandboxProvider 不是安全隔离边界,a-cdm 因"内部可信团队、非公开暴露"启用以支持自研 skill(如 attendance-analysis),长期应迁移到 AioSandboxProvider(docker-in-docker,火山 all-in-one-sandbox 镜像)。这条与红线安全段直接相关,也是为什么前面"鉴权授权""公网边缘加固"那么严——因为一旦 Agent 被滥用,bash 是直通宿主的。
Common Pitfalls / 实战 Tips
- LocalSandbox 是单例,sandbox_id 恒为 "local":多 thread 不是多实例,隔离全靠虚拟路径里的 thread_id。别假设"每个 thread 一个 sandbox 进程"。
/mnt/kingdee路径在容器内必须可达:依赖 langgraph 容器的同名 bind mount(Aegra 章),漏配会让 sandbox bash 找不到 ERP wiki。allow_host_bash: true不要随手关也不要扩大:关了自研 skill 坏;真要收紧应迁 AioSandbox 而非小修。改它前读config.yaml:671-673的权衡说明。- outputs 要 present_files 才对用户可见:写到
/mnt/user-data/outputs不等于用户能看到,需present_files工具显式呈现(见工具装配章)。 - str_replace 串行化按 (sandbox.id, path):同路径并发替换会串行,隔离沙箱不会因相同虚拟路径互相阻塞。
References
deer-flow/backend/packages/harness/deerflow/sandbox/sandbox.py:1-85— Sandbox 抽象接口deer-flow/backend/packages/harness/deerflow/sandbox/local/local_sandbox_provider.py:1-116— 单例 Provider(本章主源)deer-flow/backend/packages/harness/deerflow/sandbox/local/local_sandbox.py— LocalSandbox 实现deer-flow/backend/packages/harness/deerflow/sandbox/tools.py— ls/read/write/str_replace/bash 工具deer-flow/config.yaml:666-691— sandbox 配置 + allow_host_bash + mounts(本章主源)deer-flow/backend/packages/harness/deerflow/sandbox/middleware.py— SandboxMiddleware 生命周期
Related Pages
| Page | Relationship |
|---|---|
| deer-flow 引擎配置体系 | 本章 allow_host_bash/mounts 定制在该章首次提出 |
| Agent 中间件链机制 | 本章沙箱由该章 SandboxMiddleware/SandboxAudit 管理 |
| Aegra 运行时与 LangGraph | 本章 /mnt/kingdee 依赖该章容器 bind mount |
| SKILL 技能系统 | 本章 /mnt/skills 是该章技能的沙箱视图 |
| MCP 集成与工具装配 | 本章文件工具由该章 get_available_tools 装配 |