沙箱与代码执行:让 Agent 安全跑代码的 RPC 架构

execute_code 如何用 Unix domain socket 实现进程隔离、环境变量清洗、资源限制

通过 Hermes 探秘 Agent 工程 | 第 6 篇 · 查看全部

上一篇:安全防护体系:当 Agent 拥有终端时如何防止「做出格」的事


[系列文章导航]

#文章定位
1Agent Loop:Agent 的核心执行循环入口
2System Prompt:身份、上下文与策略的三层架构认知层
3工具系统:从注册到调度工具层
4工具调度系统:从注册到执行的完整生命周期调度层
5安全防护体系:当 Agent 拥有终端时如何防止「做出格」的事安全层
6沙箱与代码执行:让 Agent 安全跑代码的 RPC 架构执行层
7上下文压缩:让 Agent 在有限窗口里「记得住」记忆层
8记忆系统:跨会话持久化的工程实现记忆层
9技能系统:Agent 如何把经验变成可复用的程序化记忆记忆层
10Provider 抽象层:让 Hermes 同时驾驭 30+ 个 LLM 提供商模型层
11Gateway 网关:连接 20+ 平台的统一消息路由接入层
12多 Agent 协作:委托、调度与看板协作层

问题:让模型写代码,但别让它搞破坏

execute_code 是 Hermes 里最危险的工具——没有之一。

它能运行任意本地 Python 代码。这意味着模型可以调用 subprocessctypesos 模块直接操作文件系统、用 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 里的工具才会被生成存根。

环境变量清洗

清洗规则(按顺序执行):

  1. 声明白名单优先:skill 或 config.yaml 里声明的变量直接通过
  2. 黑洞规则:环境变量名包含 KEYTOKENSECRETPASSWORDCREDENTIAL → 删除
  3. 安全前缀:以 PATHHOMEUSERLANG 等开头的 → 通过
  4. Hermmes 运行时变量HERMES_HOMEHERMES_PROFILE 等 → 通过
  5. 其他 HERMES_* 变量:显式删除(防止敏感配置泄露)

凭证保护双层验证

第一层:当 skill 声明 required_environment_variables 时,如果其中包含 Hermes 管理的 Provider 凭证,注册会被拒绝——防御恶意 skill 把 Hermes 自己的 API key 骗进子进程。

第二层:运行时 _scrub_child_env() 的黑洞规则仍然会拦截——环境变量名包含 KEY/TOKEN/SECRET 等子串的变量无论如何都不会进入子进程。

Prompt 注入防御

沙箱内的代码可以调用 terminal(),这意味着一段脚本可以间接地在命令行里嵌入 prompt 注入攻击。

Hermes 的防御:

  1. shell 注释剥离:在发送命令给 smart approval 之前剥离 # 后的内容
  2. terminal guard:父进程的 approval context 会传播到 RPC listener 线程——脚本在子进程里调用 terminal() 时,命令审批仍然经过正常流程

沙箱逃逸的边界

1. Python 标准库完全可用

subprocessos.systemctypesshutil 等在沙箱中完全可用。这些 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 预算