沙箱后端
职责
agent 要执行命令、跑代码,但不能让任意命令直接打到宿主机。Hermes 的终端工具支持 6 种执行后端,按安全/隔离/成本权衡选择:Local(本机直跑)、Docker(容器隔离)、SSH(远端机器)、Singularity(HPC 容器)、Modal(云沙箱,direct 或 managed)、Daytona(云开发环境)。容器硬化 + 命名空间隔离,危险命令还经审批回调拦截。
设计动机
为什么是 6 种而不是 1 种?agent 的执行场景跨度太大——开发时只想在本机跑得快,生产跑批需要容器隔离,HPC 用户必须在 Singularity 里跑(没法用 Docker),云端无人值守要 Modal 这种按秒计费的云沙箱。任何一种后端都覆盖不全。Hermes 把「执行」抽象成统一接口,后端按场景拼装,危险命令审批回调跨后端共用。Docker 后端要专门检测 bind mount 是否暴露宿主路径,因为容器隔离的安全前提是「容器看不到外面」,一旦 docker run -v /:/host 就破功;Modal 要区分 direct(用户自己的凭据)和 managed(网关托管),因为前者算用户账单、后者算 Nous 门户额度,权限和计费路径完全不同。这些检测逻辑不写在后端里,而是抽到 tool_backend_helpers.py,让选后端和判安全级别解耦。
关键文件
terminal_tool 头部文档:1-15— 列出 local / docker / modal / ssh / singularity / daytona 六后端Docker host 访问检测:260-290—_docker_volume_uses_host_path/_docker_has_host_access(检测 bind mount 是否暴露宿主路径)Singularity / Modal 导入:65-90—_get_scratch_dir(来自 environments/singularity)environments/singularity.py— Singularity scratch 目录与 SIF 缓存Modal 模式解析:77-137—coerce_modal_mode/has_direct_modal_credentials/resolve_modal_backend_state(direct vs managed)_DEFAULT_BROWSER_PROVIDER:12— 默认 provider 为 local子代理审批回调:75-103—_subagent_auto_deny/_subagent_auto_approve(沙箱内执行的危险命令管控)tools/daemon_pool.py— 子代理常驻线程池(承载沙箱执行)tools/ 目录— terminal / close_terminal / read_terminal / tool_backend_helpers / environments/
数据流
- agent 决定执行命令 → tool_call
terminal→tools/terminal_tool.py。 - 按作业配置选后端(local/docker/modal/ssh/singularity/daytona):
local:直接宿主执行(最快,默认)docker:tools/terminal_tool.py:260检测 bind mount 是否暴露宿主路径,据此决定权限modal:经tools/tool_backend_helpers.py:102解析 direct(用户自己的 Modal 凭据)还是 managed(网关托管)singularity:经tools/environments/singularity.py准备 scratch 与 SIFssh/daytona:远端/云环境
- 危险命令经审批回调拦截——主循环用交互式审批;子代理默认 auto-deny(
tools/delegate_tool.py:75),cron/批量可 opt-in auto-approve。 - 命令输出回填主循环;长任务可后台化(
tools/daemon_pool.py)。
后端列表的声明很简单,terminal_tool.py 顶部就一句注释:「A terminal tool that executes commands in local, Docker, Modal, SSH, Singularity, and Daytona environments.」
Docker 后端检测 bind mount 是不是宿主路径,看 spec 字首:以 /、~、./、../ 开头,或像 Windows 盘符 C:\ 那样第二个字符是冒号的,都算宿主路径,触发 has_host_access=True,后续权限走更严格的分支:
def _docker_volume_uses_host_path(volume_spec: str) -> bool:
"""Return True when a docker volume spec bind-mounts a host path."""
if not isinstance(volume_spec, str):
return False
vol = volume_spec.strip()
return bool(vol) and (
vol.startswith(("/", "~", "./", "../")) or
(len(vol) >= 3 and vol[1] == ":" and vol[2] in ("/", "\\"))
)Modal 解析有三档:direct(用户自己的 Modal 凭据)、managed(Nous 门户托管)、auto(默认,managed 不可用时 fallback 到 direct)。resolve_modal_backend_state 集中在一处,选不出后端就返回 None。has_direct_modal_credentials 同时看 MODAL_TOKEN_ID/MODAL_TOKEN_SECRET 环境变量和 ~/.modal.toml 文件,两个都缺才算 direct 不可用。managed_nous_tools_enabled 走 Nous Portal 账户查询,失败时 fail closed,不会因查询出错就误开通 managed 路径。
Singularity 是 HPC 场景的特殊存在——用户没法装 Docker,只有 Apptainer/Singularity。_find_singularity_executable 先找 apptainer 再找 singularity,两个都没有就报错指明安装地址。scratch 目录优先用 /scratch(HPC 集群标准挂载),没有就落到 get_sandbox_dir() / "singularity"。容器是 --containall --no-home 加 capability dropping——HPC 共享集群上必须这么跑,否则会越权看到其他用户家目录。
边界与失败
- Docker bind mount 误判:检测函数认
/、~、./、../开头为宿主路径,但 named volume(如mydata:/data)会被放行。命名卷背后挂宿主路径(自定义 driver)时,_docker_has_host_access看不到,会误判为低风险。 - Modal managed 失败的回退:
auto模式下 managed 不可用 fallback 到 direct,但如果用户只配了 managed 没配 direct 凭据,selected_backend变None,后端选择失败到运行时才炸,创建终端那一刻可能看不到清晰错误。 - Singularity SIF 缓存膨胀:SIF 镜像常几百 MB 到几 GB,scratch 目录没轮转策略会撑爆磁盘;HPC 节点的
/scratch通常有集群级清理策略,两者对不齐时容易出现「缓存被集群清掉,agent 还以为 SIF 在」。 - 远端后端网络抖动:SSH/Daytona 的命令执行经网络,连接断开后正在跑的子进程输出就丢了。输出回填假设单次同步返回,没有重连续传,长任务在远端后端上风险最大。
- 超时与 OOM:local/docker 有
script_timeout,但远端后端的超时要靠远端进程自己 kill,本地调度器等不到响应就会卡住,或被_run_job_script_with_claim_heartbeat的租约 TTL 误判为死掉。
小结
沙箱层是「执行隔离 + 危险命令管控」:6 后端覆盖从本机到 HPC 到云;Docker 检测 bind mount 防越权;Modal 区分 direct/managed 凭据;危险命令统一走审批回调。子代理与 cron 走 auto-deny/auto-approve 的非交互路径。