沙箱与代码执行:让 Agent 安全跑代码的 RPC 架构
execute_code 如何用 Unix domain socket 实现进程隔离、环境变量清洗、资源限制
通过 Hermes 探秘 Agent 工程 | 第 6 篇 · 查看全部
[系列文章导航]
| # | 文章 | 定位 |
|---|---|---|
| 1 | Agent Loop:Agent 的核心执行循环 | 入口 |
| 2 | System Prompt:身份、上下文与策略的三层架构 | 认知层 |
| 3 | 工具系统:从注册到调度 | 工具层 |
| 4 | 工具调度系统:从注册到执行的完整生命周期 | 调度层 |
| 5 | 安全防护体系:当 Agent 拥有终端时如何防止「做出格」的事 | 安全层 |
| 6 | 沙箱与代码执行:让 Agent 安全跑代码的 RPC 架构 | 执行层 |
| 7 | 上下文压缩:让 Agent 在有限窗口里「记得住」 | 记忆层 |
| 8 | 记忆系统:跨会话持久化的工程实现 | 记忆层 |
| 9 | 技能系统:Agent 如何把经验变成可复用的程序化记忆 | 记忆层 |
| 10 | Provider 抽象层:让 Hermes 同时驾驭 30+ 个 LLM 提供商 | 模型层 |
| 11 | Gateway 网关:连接 20+ 平台的统一消息路由 | 接入层 |
| 12 | 多 Agent 协作:委托、调度与看板 | 协作层 |
问题:让模型写代码,但别让它搞破坏
execute_code 是 Hermes 里最危险的工具——没有之一。
它能运行任意本地 Python 代码。这意味着模型可以调用 subprocess、ctypes、os 模块直接操作文件系统、用 shutil 删除整个目录。
但同时,execute_code 又是最强大的工具——它让模型可以把多轮工具调用折叠成一轮推理(减少 token 消耗),并且中间工具结果不会进入 LLM 的上下文窗口。
Hermes 的做法是:不完全禁止,而是把危险关进笼子。
两种传输方式
1. 本地后端(UDS)
在本地 Linux/macOS 上运行时,使用 Unix domain socket
父进程(Hermes Agent)
│
├─ RPC listener 线程(UDS server)
│
▼
子进程(LLM 写的 Python 脚本)
hermit_tools.py 提供 RPC stubs
只返回 stdout——中间工具结果不会进入 LLM 的上下文窗口。
2. 远程后端(file-based RPC)
在 Docker / SSH / Modal / Daytona 等远程环境运行时,使用文件。
自适应轮询频率(初始 50ms,退避到 250ms)。
工具存根生成
子进程不会直接拿到所有工具——它只能看到白名单里声明的工具:
SANDBOX_ALLOWED_TOOLS = frozenset([
"web_search", "web_extract",
"read_file", "write_file", "search_files", "patch",
"terminal",
])
生成 hermes_tools.py 时,只有同时在 SANDBOX_ALLOWED_TOOLS 和当前会话 enabled_tools 里的工具才会被生成存根。
环境变量清洗
清洗规则(按顺序执行):
- 声明白名单优先:skill 或 config.yaml 里声明的变量直接通过
- 黑洞规则:环境变量名包含
KEY、TOKEN、SECRET、PASSWORD、CREDENTIAL→ 删除 - 安全前缀:以
PATH、HOME、USER、LANG等开头的 → 通过 - Hermmes 运行时变量:
HERMES_HOME、HERMES_PROFILE等 → 通过 - 其他 HERMES_* 变量:显式删除(防止敏感配置泄露)
凭证保护双层验证
第一层:当 skill 声明 required_environment_variables 时,如果其中包含 Hermes 管理的 Provider 凭证,注册会被拒绝——防御恶意 skill 把 Hermes 自己的 API key 骗进子进程。
第二层:运行时 _scrub_child_env() 的黑洞规则仍然会拦截——环境变量名包含 KEY/TOKEN/SECRET 等子串的变量无论如何都不会进入子进程。
Prompt 注入防御
沙箱内的代码可以调用 terminal(),这意味着一段脚本可以间接地在命令行里嵌入 prompt 注入攻击。
Hermes 的防御:
- shell 注释剥离:在发送命令给 smart approval 之前剥离
#后的内容 - terminal guard:父进程的 approval context 会传播到 RPC listener 线程——脚本在子进程里调用
terminal()时,命令审批仍然经过正常流程
沙箱逃逸的边界
1. Python 标准库完全可用
subprocess、os.system、ctypes、shutil 等在沙箱中完全可用。这些 API 不经过 terminal() 的审批机制。
缓解:在 Gateway 或 interactive 模式下,对整个脚本做一次性审批;在隔离容器中跳过审批——因为沙箱本身已经提供了 OS 级别的隔离。
2. 资源耗尽
缓解:
- DEFAULT_TIMEOUT = 300(5 分钟超时)
- DEFAULT_MAX_TOOL_CALLS = 50(最多 50 次工具调用)
- MAX_STDOUT_BYTES = 50,000(stdout 上限 50KB)
3. 网络访问
脚本可以自由访问网络——沙箱不隔离网络。这是设计取舍:如果隔离了网络,web_search 就用不了了。
execute_code 也是 execute_code
一个有趣的细节:tools/code_execution_tool.py 自身也使用 handle_function_call() 来派发工具调用。它走过完整的工具调用链路——包括所有中间件、pre_tool_call hook、post_tool_call hook。
下一篇:上下文压缩:让 Agent 在有限窗口里「记得住」 — 当对话越来越长,Agent 如何让历史既能"记得住"又不压爆 token 预算