工具层与沙箱
这章讲什么: 模型「动手」的那一层——工具怎么定义、怎么被挑选挂载,以及最敏感的 shell/代码执行怎么被沙箱关起来。
1. 它要解决的小问题
agent 要能「动手」(检索、执行代码、读写文件/笔记)。两个工程问题:(1) 怎么统一描述工具、自动生成各家 LLM 都吃的 function-calling schema;(2) 让模型执行代码时,怎么不把宿主机暴露给不可信的生成代码。
2. 工具层:统一协议 + 上下文门控
2.1 BaseTool 协议
每个工具(内置或插件)都实现 BaseTool(deeptutor/core/tool_protocol.py,BaseTool)。工具用 ToolDefinition + ToolParameter 声明 schema,to_openai_schema 自动转成 OpenAI 格式(tool_protocol.py:60 起)。
一个值得注意的兼容细节:type="array" 的参数,严格的 provider(Gemini、Anthropic)要求必须有 items,否则报 400;OpenAI 却容忍缺失。所以缺 items 时回退成 {"type": "string"},让只声明 ToolParameter(type="array") 的调用也能产出合法 schema(tool_protocol.py:20-26 注释)。适配器工具(如 MCP)的任意 JSON Schema 用 raw_parameters 原样透传,避免重编码丢信息(tool_protocol.py:50-58)。
2.2 ToolRegistry
ToolRegistry(deeptutor/runtime/registry/tool_registry.py,ToolRegistry)是工具查找表:load_builtins 实例化注册所有内置工具,get 取用,还支持别名解析(TOOL_ALIASES)、MCP 热重载时的 unregister、以及延迟工具(deferred_tools,渐进式披露——不一上来全塞给模型)(tool_registry.py:46)。
2.3 上下文门控的工具挂载
不是所有工具一直可见。据 AGENTS.md:只有 4 个用户可开关的工具(brainstorm/web_search/paper_search/reason)常驻 /settings/tools;其余是上下文门控的——聊天能力按 ToolMountFlags(有没有知识库、有没有附件、沙箱可不可用…)自动挂载 rag/read_source/read_memory/write_memory/exec/code_execution/ask_user 等。这让模型每回合看到的工具面是「按当前情境裁过的」,而不是一大坨。
3. 沙箱:三档隔离
这是工具层里安全含量最高的一支。隔离档位定义在 deeptutor/services/sandbox/spec.py:14(IsolationLevel):
| 档位 | 含义 | 谁能用 shell exec |
|---|---|---|
SYSTEM | OS 级强隔离(容器 / bubblewrap 命名空间) | 普通用户也放行 |
APPLICATION | 仅进程内护栏(路径检查 + deny 规则) | 仅管理员 opt-in |
OFF | 没有可用沙箱 | 永不运行不可信代码 |
策略门就钉在这个枚举上(spec.py:17-20 注释):普通用户的 shell exec 只在 SYSTEM 隔离下提供;APPLICATION 因为不做 OS 隔离,只允许管理员显式开启;OFF 一律不跑。rank() 给档位排序便于比较(spec.py:27)。
3.1 三个后端
每个后端报告它实际提供的隔离级别(deeptutor/services/sandbox/backends.py 顶部 docstring):
请求执行(ExecRequest:命令 + 挂载 + 资源上限)
│
┌────┴────────────────┬───────────────────────┐
▼ ▼ ▼
RunnerSidecarBackend BwrapBackend RestrictedSubprocessBackend
(SYSTEM) (SYSTEM) (APPLICATION)
把命令 HTTP 提交给 Linux 裸机用 bwrap 普通子进程 + 清洗 env
独立 runner 容器 挂载命名空间包住 + cwd 路径限制
主 app 永不执行 命令 (本地 dev 降级,如 macOS;
不可信 shell 仅管理员 opt-in)
- RunnerSidecarBackend:Docker 部署的答案——主 app 保持最小权限、永不执行不可信 shell,把命令转交独立 runner 容器(
backends.py:47)。 - BwrapBackend:Linux 裸机用
bwrap挂载命名空间。 - RestrictedSubprocessBackend:本地开发(如 macOS)的降级兜底,只有 env 清洗 + cwd 限制,不做 OS 隔离,所以仅管理员可开。
3.2 资源上限与挂载
每次执行带 ResourceLimits(spec.py:32):timeout_s=30、memory_mb=512、max_output_chars=10000、cpu_seconds=30(best-effort,各后端尽力执行)。Mount(spec.py:42)默认 read_only=True 暴露目录给沙箱。ExecResult.render(spec.py:75)把 stdout/stderr 合并成给模型看的字符串,头尾各截一段防爆上下文。
SandboxService(deeptutor/services/sandbox/service.py,get_sandbox_service/exec_capability_available)是门面:它探测可用后端、报告当前隔离级别,exec 类工具据此决定是否暴露给当前用户。
4. 巧妙之处
- 隔离级别是一等枚举,安全策略钉在它上面。 「谁能跑 shell」不是散落的 if,而是
IsolationLevel.rank()的比较——可审计、可在部署文档里讲清(CONTAINERIZATION.md配套)。 - 能力探测式优雅降级。 沙箱不可用 → exec 工具就不挂(上下文门控);provider 不支持某参数 → 去掉重试(见 01/02 章)。整个系统的风格是「探测真实能力,按真实能力降级」而不是「假设环境完美」。
- 延迟工具的渐进式披露。
deferred_tools让一部分工具不在初始 schema 里,模型需要时再load_tools挂上,控制每回合的工具面大小(呼应记忆/RAG 章的「分层控规模」哲学)。
5. 边界与局限
APPLICATION级(纯进程内护栏)并不真正 OS 隔离,作者明确标注它「admin-opt-in only because it does not OS-isolate」(spec.py:19-20)——本地 dev 方便,但不该在多用户生产里对普通用户开。- 资源上限是 best-effort,具体强度随后端而异(
spec.py:33注释)。
6. 代码地图
| 主题 | 文件 | 符号 |
|---|---|---|
| 工具协议 | deeptutor/core/tool_protocol.py | BaseTool、ToolDefinition、ToolParameter、to_openai_schema |
| 工具注册表 | deeptutor/runtime/registry/tool_registry.py | ToolRegistry、load_builtins、deferred_tools |
| 内置工具集 | deeptutor/tools/ | rag_tool.py、exec_tool.py、web_search.py、ask_user.py 等 |
| 沙箱隔离档位 | deeptutor/services/sandbox/spec.py | IsolationLevel、ResourceLimits、Mount、ExecResult |
| 沙箱后端 | deeptutor/services/sandbox/backends.py | RunnerSidecarBackend、BwrapBackend、RestrictedSubprocessBackend |
| 沙箱门面 | deeptutor/services/sandbox/service.py | SandboxService、get_sandbox_service、exec_capability_available |