主题
沙箱与工具配置
本章目标:
- 掌握
sandbox.use三种模式(local / docker-AIO / provisioner-K8s)如何在config.yaml配置,以及scripts/docker.sh如何在启动前从配置中检测模式。- 理解
tools[]与tool_groups[]的配置结构(use变量路径 +group分组),以及skills.path/skills.container_path的解析与容器挂载方式。- 识别会"放开隔离边界"的安全开关(
allow_host_bash等),知道它们的默认值、风险与触发条件。
TL;DR
sandbox.use 是一个类路径字符串,DeerFlow 用反射(resolve_class)把它实例化成 SandboxProvider:指向 LocalSandboxProvider 即本地直跑(local),指向 AioSandboxProvider 即容器隔离(aio),后者再加 provisioner_url 即升级为 Kubernetes/k3s 动态 Pod(provisioner)。tools[] 每项靠 use 变量路径(如 deerflow.sandbox.tools:bash_tool)被 resolve_variable 解析成工具实例,并按 group 落入 tool_groups[] 声明的逻辑分组以供按组过滤。skills.path(主机路径)与 skills.container_path(容器内路径,默认 /mnt/skills)决定技能目录如何只读挂载进沙箱。核心安全开关 sandbox.allow_host_bash 默认 false,因为本地沙箱并非安全隔离边界;把它设为 true 等于在主机上直接执行模型生成的 shell 命令。
Overview
DeerFlow 的"沙箱"是 agent 执行 bash / 文件读写等工具的运行边界。为什么要把它做成"配置即可切换的三种模式",而不是写死一种?因为不同部署场景对隔离强度与运维复杂度的取舍完全不同:本地单人开发追求零依赖、快速迭代(local);团队共享 / 不可信输入需要容器隔离(aio);生产 / 多租户需要每个会话独立 Pod 的强隔离与弹性伸缩(provisioner)。
把模式收敛到一个 sandbox.use 类路径字段,带来三个直接收益:配置层只声明"用哪个 provider 类",实例化交给反射系统(sandbox_provider.py:48-62);AioSandboxProvider 自己根据有无 provisioner_url 在 aio 与 provisioner 之间二选一(aio_sandbox_provider.py:135-156);启动脚本 docker.sh 可以纯文本扫描 config.yaml 提前判断要不要拉起 provisioner 服务(docker.sh:18-61)。
同理,工具不写死在代码里,而是 tools[] 列表 + use 变量路径,让"用哪个 web 搜索 provider""是否开 bash"都变成配置开关。技能目录通过 skills.path / skills.container_path 两个字段,统一以只读方式呈现给本地与容器两种沙箱。
Architecture
配置到运行实例的转换链涉及四个 Pydantic 配置模型(SandboxConfig / ToolConfig / ToolGroupConfig / SkillsConfig),它们在 AppConfig 中被聚合,并由反射系统与各 provider 在运行时消费。
Source:
- config/sandbox_config.py:12-83 —
SandboxConfig/VolumeMountConfig字段定义 - config/tool_config.py:4-20 —
ToolConfig/ToolGroupConfig - config/skills_config.py:16-72 —
SkillsConfig与路径解析 - config/app_config.py:89-92 — 四个配置在
AppConfig的聚合点 - sandbox/security.py:23-45 — host bash 安全门控
- scripts/docker.sh:18-61 — 启动期模式检测
Components / Subsystems
SandboxConfig — 沙箱配置
SandboxConfig.use 是唯一必填字段,描述 sandbox provider 的类路径(如 deerflow.sandbox.local:LocalSandboxProvider)sandbox_config.py:30-33。模型设了 extra="allow",因此 provisioner_url 这种未在模型中显式声明的字段也能被接受并通过 getattr 读取 sandbox_config.py:83。
AIO 专属字段都是可选(None 默认),由 AioSandboxProvider._load_config() 在为空时套用各自的 DEFAULT_* 常量:image / port / container_prefix / idle_timeout / replicas / mounts / environment,并把 provisioner_url 用 getattr(..., None) or "" 取出 aio_sandbox_provider.py:160-178。environment 中以 $ 开头的值会被 _resolve_env_vars 解析为主机环境变量 aio_sandbox_provider.py:180-190。
输出截断三项(bash_output_max_chars 默认 20000、read_file_output_max_chars 默认 50000、ls_output_max_chars 默认 20000)对所有模式生效,设为 0 表示不截断 sandbox_config.py:67-81。
VolumeMountConfig — 额外目录挂载
mounts[] 每项是一个 VolumeMountConfig,三字段:host_path(主机绝对路径)、container_path(容器内虚拟路径)、read_only(默认 false)sandbox_config.py:4-9。仅 AioSandboxProvider 的本地容器后端会消费这些挂载(aio_sandbox_provider.py:150-156)。
ToolConfig / ToolGroupConfig — 工具与分组
ToolGroupConfig 只有一个必填字段 name(分组唯一名)tool_config.py:4-8。ToolConfig 有三个必填字段:name(工具唯一名)、group(所属分组名,需与某个 tool_groups[].name 对应)、use(工具 provider 的变量路径,如 deerflow.sandbox.tools:bash_tool)tool_config.py:11-20。两个模型均 extra="allow",因此 max_results / timeout / api_key 等 provider 特有参数可以直接写在工具项里(见 config.example.yaml 的 web_search、web_fetch 等)。
get_available_tools() 用 groups 参数按 tool.group 过滤,然后对每个 cfg.use 调用 resolve_variable(cfg.use, BaseTool) 得到工具实例;当配置里的 name 与工具对象自身的 .name 不一致时仅记 warning,实际绑定以工具对象的 .name 为准 tools.py:66-88。
SkillsConfig — 技能目录与容器路径
SkillsConfig 三个相关字段:use(SkillStorage 实现类路径,默认 deerflow.skills.storage.local_skill_storage:LocalSkillStorage)、path(主机技能目录,可空)、container_path(技能在沙箱容器内的挂载路径,默认 /mnt/skills)skills_config.py:16-30。
get_skills_path() 的解析顺序固定为:① 显式 path 字段 → ② DEER_FLOW_SKILLS_PATH 环境变量 → ③ 项目根下的 skills 目录 → ④ monorepo 兼容的仓库根候选;都不存在时仍返回项目根默认路径以便上层稳定显示"无技能"位置 skills_config.py:32-59。get_skill_container_path() 以 {container_path}/{category}/{skill_name} 拼出单个技能在容器内的完整路径 skills_config.py:61-72。
安全门控 — host bash 是否放行
is_host_bash_allowed() 决定 bash 工具是否暴露给 agent:若当前 provider 不是 LocalSandboxProvider(即用了隔离容器),直接返回 True;若是本地 provider,则只在 sandbox.allow_host_bash 显式为 True 时返回 True,否则 False security.py:35-45。get_available_tools() 据此过滤掉 group=="bash" 或 use=="deerflow.sandbox.tools:bash_tool" 的工具项 tools.py:26-34,69-71。
Data Flow
下图描述「config.yaml 文本 → 沙箱与工具运行实例」的解析时序,以及启动脚本独立的文本探测路径。
速查表
三种 sandbox 模式对比矩阵
| 维度 | local | aio(docker) | provisioner(K8s) | Source |
|---|---|---|---|---|
sandbox.use | deerflow.sandbox.local:LocalSandboxProvider | deerflow.community.aio_sandbox:AioSandboxProvider | 同 aio | config.example.yaml:560 / :586 / :627 |
是否需 provisioner_url | 否 | 否 | 是(如 http://provisioner:8002) | config.example.yaml:626-628 |
| 隔离强度 | 主机直跑(非安全边界) | Docker/Apple Container 容器 | 每会话独立 k3s Pod | sandbox_config.py:17-19 |
| 后端选择逻辑 | LocalSandboxProvider | LocalContainerBackend | RemoteSandboxBackend(provisioner_url 触发) | aio_sandbox_provider.py:138-156 |
| 启动脚本检测结果 | local | aio | provisioner(额外起 provisioner 服务) | docker.sh:50-60 / :166-176 |
| host bash 默认行为 | 默认禁用,需 allow_host_bash:true | 默认放行(容器内隔离) | 默认放行 | security.py:35-45 |
| 未知 provider 时检测回退 | — | — | 回退到 local | docker.sh:58-60 |
模式检测的回归用例完整覆盖了上述所有分支(含被注释掉的 provisioner_url 不应激活 provisioner)test_docker_sandbox_mode_detection.py:44-107。
tools / tool_groups 配置项
| 字段 | 所属 | 必填 | 含义 | Source |
|---|---|---|---|---|
tool_groups[].name | ToolGroupConfig | 是 | 分组唯一名(如 web/file:read/file:write/bash) | tool_config.py:7 / config.example.yaml:358-362 |
tools[].name | ToolConfig | 是 | 工具唯一名(绑定时以工具对象 .name 为准) | tool_config.py:14 |
tools[].group | ToolConfig | 是 | 所属分组,对应某个 tool_groups[].name | tool_config.py:15 |
tools[].use | ToolConfig | 是 | 工具 provider 变量路径(module:variable) | tool_config.py:16-19 |
额外参数(如 max_results/timeout/api_key) | ToolConfig(extra=allow) | 否 | 透传给 provider 的特有参数 | tool_config.py:20 / config.example.yaml:371-374 |
skills.path | SkillsConfig | 否 | 主机技能目录(绝对或相对项目根) | skills_config.py:23-26 |
skills.container_path | SkillsConfig | 否(默认 /mnt/skills) | 技能在沙箱内的只读挂载路径 | skills_config.py:27-30 / config.example.yaml:725 |
Configuration
config.example.yaml 的 sandbox 段以 local 模式为默认启用项,aio / provisioner 以注释形式给出模板,切换时取消注释并注释掉 local 段即可 config.example.yaml:559-628。
| 配置键 | 默认值 | 作用 | 安全相关 | Source |
|---|---|---|---|---|
sandbox.use | deerflow.sandbox.local:LocalSandboxProvider | 选择 provider(决定模式) | — | config.example.yaml:560 |
sandbox.allow_host_bash | false | 是否允许 bash 工具在主机直接执行 | ⚠️ 放开隔离:本地 provider 非安全边界,设 true 等于让模型在主机跑任意 shell;仅限完全可信单用户环境 | sandbox_config.py:34-37 / config.example.yaml:561-564 |
sandbox.provisioner_url | 未设(extra 字段) | 设置后 AIO 切到远程 provisioner / k3s 模式 | — | config.example.yaml:626-628 / aio_sandbox_provider.py:144-147 |
sandbox.image | None→内置 DEFAULT_IMAGE | AIO 容器镜像 | — | sandbox_config.py:38-41 |
sandbox.port | None→DEFAULT_PORT(8080) | AIO 容器基础端口 | — | sandbox_config.py:42-45 |
sandbox.replicas | None→DEFAULT_REPLICAS(3) | 并发容器上限,超限按 LRU 回收 | — | sandbox_config.py:46-49 |
sandbox.container_prefix | None→deer-flow-sandbox | 容器名前缀 | — | sandbox_config.py:50-53 |
sandbox.idle_timeout | None→600 秒(0 禁用) | 空闲多久后释放沙箱 | — | sandbox_config.py:54-57 |
sandbox.mounts[] | [] | 额外主机→容器目录挂载 | ⚠️ read_only:false(默认)会把主机目录可写暴露给沙箱进程 | sandbox_config.py:58-61 / config.example.yaml:565-570 |
sandbox.environment | {} | 注入容器的环境变量($VAR 解析主机环境) | ⚠️ 注入的密钥会进入沙箱内可执行环境 | sandbox_config.py:62-65 |
sandbox.bash_output_max_chars | 20000(0 禁用) | bash 输出保留字符上限(头尾截断) | — | sandbox_config.py:67-71 |
sandbox.read_file_output_max_chars | 50000(0 禁用) | read_file 输出上限(头部截断) | — | sandbox_config.py:72-76 |
sandbox.ls_output_max_chars | 20000(0 禁用) | ls 输出上限(头部截断) | — | sandbox_config.py:77-81 |
skills.use | ...:LocalSkillStorage | SkillStorage 实现类 | — | skills_config.py:19-22 |
skills.path | None(走解析顺序) | 主机技能目录 | — | skills_config.py:23-26 |
skills.container_path | /mnt/skills | 沙箱内技能挂载路径(只读) | — | skills_config.py:27-30 |
技能目录在 AIO 模式下由 _get_skills_mount() 统一以只读方式挂载(read_only=True),并在 DooD 场景下用 DEER_FLOW_HOST_SKILLS_PATH 修正主机侧路径 aio_sandbox_provider.py:286-304。
Common Pitfalls / Tips
- 切模式时别忘了关掉旧段:
config.example.yaml中 local 段是默认启用的;启用 aio/provisioner 时必须同时把 local 的sandbox:段注释掉,否则 YAML 中后出现的sandbox:才生效,前面的会被忽略,容易误判 config.example.yaml:559-628。 - 被注释的
provisioner_url不会激活 provisioner:docker.sh的 awk 探测会跳过以#开头的行,这一行为有专门回归测试覆盖 test_docker_sandbox_mode_detection.py:88-96。 - 未知 provider 不会报错而是退回 local:
docker.sh对识别不了的use字符串静默回退local,启动看起来正常但隔离已失效,务必核对use拼写 docker.sh:58-60。 tools[].name与工具对象.name不一致只警告不报错:实际绑定以工具对象自身.name为准,配置里写错名字会导致模型 schema 与运行时路由对不上(issue #1803),改use后留意日志 warning tools.py:75-88。- bash 工具消失多半是安全门控:用 local provider 且
allow_host_bash:false(默认)时,所有group:bash/bash_tool工具项会被静默过滤,这是设计行为而非配置丢失 security.py:35-45。 skills.path留空时的兜底:不写path也不设DEER_FLOW_SKILLS_PATH时,会依次找项目根skills/与 monorepo 仓库根候选;都没有也不报错而是返回项目根默认路径,排查"技能没加载"先确认目录是否真实存在 skills_config.py:45-59。- AIO 可选字段为空才套默认值:
replicas/idle_timeout用getattr(..., None)读取,显式写0与"不写"语义不同(idle_timeout:0表示禁用空闲回收)aio_sandbox_provider.py:165-173。
References
- backend/packages/harness/deerflow/config/sandbox_config.py —
SandboxConfig/VolumeMountConfig字段与默认值 - backend/packages/harness/deerflow/config/tool_config.py —
ToolConfig/ToolGroupConfig - backend/packages/harness/deerflow/config/skills_config.py — 技能路径解析顺序与容器路径拼接
- backend/packages/harness/deerflow/sandbox/security.py — host bash 安全门控逻辑
- backend/packages/harness/deerflow/sandbox/sandbox_provider.py — provider 单例与
resolve_class实例化 - backend/packages/harness/deerflow/community/aio_sandbox/aio_sandbox_provider.py — AIO 配置加载与后端选择
- backend/packages/harness/deerflow/tools/tools.py —
get_available_tools按组过滤与resolve_variable - scripts/docker.sh — 启动期沙箱模式文本检测
- backend/tests/test_docker_sandbox_mode_detection.py — 模式检测回归用例
- config.example.yaml — sandbox / tools / tool_groups / skills 配置模板
Related Pages
| 章节 | 关系 |
|---|---|
| ./04-配置系统与AppConfig.md | 上游:本章四个配置模型如何被聚合进 AppConfig 及缓存/优先级机制 |
| ./06-MCP与Skills扩展配置.md | 平行:extensions_config.json 中技能启用状态与本章 skills.path 互补 |
| ./17-沙箱系统架构.md | 下游:本章配置如何驱动 SandboxProvider 的 acquire/get/release 运行机制与路径映射 |
| ./18-沙箱工具与路径映射.md | 下游:/mnt/user-data、/mnt/skills 虚拟路径在工具层的实际翻译 |
| ./20-工具系统与内置工具.md | 下游:tools[] 解析出的工具与内置/MCP/子代理工具如何汇总绑定 |
| ./25-Community工具与搜索集成.md | 下游:tools[] 中 community 搜索/抓取 provider 的具体实现与所需 API key 在 25 章 |