终端环境提供商插件

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 doctordoctor_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,并在彼此脚下把它删掉。