终端环境提供商插件
Hermes 通过一组可插拔的终端后端运行 shell 命令。内建后端(local、Docker、Singularity、Modal、Daytona、Vercel Sandbox、SSH)位于核心仓库的 tools/environments/ 下。第三方沙箱厂商则作为插件集成——一个安装在 ~/.hermes/plugins/ 下的独立插件仓库,注册一个后端,用户像选择内建后端一样,通过 config.yaml 中的 terminal.backend 选择它。
本页与浏览器提供商插件指南对应——相同的注册流程、相同的范围语义。
提供商控制什么 {#what-a-provider-controls}
一个已注册后端自动参与每个核心表面:
| 表面 | 由什么驱动 |
|---|---|
命令分发(terminal、execute_code、文件工具) | create_environment() |
hermes setup 后端选择器 | display_name、description、setup_instructions()、post_setup() |
| Dashboard 终端后端选择器(探测状态) | probe() |
hermes status / hermes doctor | doctor_checks() |
| 系统提示词环境提示 | is_remote、env_description |
| 危险命令审批跳过 | skip_container_guards |
| 容器路径/cwd 处理 | is_container |
| 同步缓存文件路径转换 | cache_path_base |
| 从派生子进程剥离机密 | strip_env_keys |
按会话沙箱隔离(container_persistent: false) | session_isolated_when_nonpersistent |
在提供商上声明这些标志,就堵住了经典的"新后端漏掉分类点 N"那类 bug——核心在每个点都查注册表,而非一份写死的名字清单。
最小提供商 {#minimal-provider}
from agent.terminal_env_provider import TerminalEnvironmentProvider
class AcmeBoxEnvironment:
"""必须满足 BaseEnvironment 的鸭子类型契约。"""
def __init__(self, cwd, timeout, task_id):
self.cwd, self.timeout, self.task_id = cwd, timeout, task_id
def execute(self, command, timeout=None, **kwargs):
... # 在沙箱中运行命令
return {"output": "...", "exit_code": 0}
def cleanup(self):
... # 拆除 / 分离
class AcmeBoxProvider(TerminalEnvironmentProvider):
name = "acmebox"
display_name = "AcmeBox"
is_remote = True # 命令不在主机上运行
is_container = True # 容器式路径/cwd 语义
@property
def description(self):
return "在 AcmeBox 云沙箱中运行命令。"
@property
def cache_path_base(self):
return "~/.hermes" # 同步缓存文件落点,或 None
@property
def strip_env_keys(self):
return frozenset({"ACMEBOX_TOKEN"})
def is_available(self):
import importlib.util, os
return (
importlib.util.find_spec("acmebox") is not None
and bool(os.getenv("ACMEBOX_TOKEN"))
)
def create_environment(self, *, cwd, timeout, task_id="default",
image=None, container_config=None, **kwargs):
return AcmeBoxEnvironment(cwd, timeout, task_id)
def register(ctx):
ctx.register_terminal_environment_provider(AcmeBoxProvider())
name: acmebox
version: 0.1.0
description: AcmeBox 云沙箱终端后端
kind: backend
启用它、选择它、运行:
hermes plugins enable acmebox
hermes config set terminal.backend acmebox
规则 {#rules}
- 保留名。 与内建后端名(
local、docker、singularity、modal、managed_modal、daytona、vercel_sandbox、ssh)冲突的注册会被拒绝。插件扩展后端集合,绝不遮蔽树内后端。 create_environment必须接受**kwargs并忽略未知键——这是向前兼容契约,让工厂签名演进时不破坏旧插件。is_available()/probe()必须廉价。 不做网络调用——它们在需求检查和 UI 绘制期间运行。- 处处软失败。 一个抛异常的提供商属性被核心当作其默认值处理(例如抛异常的
skip_container_guards保持审批层开启)。不要用异常做控制流。 - 机密放进
strip_env_keys。 你的厂商 token 绝不能被一个模型编写的 shell 命令读到;列出它会无条件地从每个派生子进程中剥离它,如同内建的MODAL_*/DAYTONA_API_KEY处理。
环境对象契约 {#environment-object-contract}
create_environment() 返回一个满足与 tools.environments.base.BaseEnvironment 相同鸭子类型接口的对象:
execute(command, timeout=None, ...)→{"output": str, "exit_code": int}cleanup()——释放资源;在会话拆除 / 空闲回收时调用- 可选:镜像内建云后端的持久化钩子
建议子类化 BaseEnvironment(你会继承共享的文件同步和后台进程管道),但不强制。
会话隔离语义 {#session-isolation-semantics}
若你的沙箱按名字恢复(后端重新挂载的持久 VM),设 session_isolated_when_nonpersistent = True。在 terminal.container_persistent: false 下,每个会话随后获得自己的沙箱身份而非共享一个——否则,两个独立的临时运行可能挂载同一个存活 VM,并在彼此脚下把它删掉。