Plugin-Terminal-Backends stecken nicht mehr in /root: default_cwd und supports_streams


Du bindest ein Plugin-Terminal-Backend in Hermes ein — Cua Spaces, eine QEMU-VM oder den internen Ausführungsdienst deiner Firma. Das Plugin lädt, terminal.backend zeigt darauf, und der erste Befehl landet in /root, obwohl das Projekt unter /home/acme/project liegt. Du bittest den Agenten, dorthin zu cd-en; der nächste Befehl ist wieder in /root. Seltsamerweise löst read_file den Projektpfad weiterhin auf. Versuchst du, Bot Desktop in diese Sandbox zu legen, verweigert es — ohne klaren Grund.

Nichts davon war der Fehler deines Plugins. Drei fest verdrahtete Annahmen im Kern haben das verursacht, und PR #142225, am 20. Oktober 2026 gemergt, hat sie in Optionen verwandelt, die ein Provider selbst deklarieren kann. Hier kommen der Mechanismus, die API und die zwei Fallstricke, um die es sich zu designen lohnt.

Die drei fest verdrahteten Annahmen

Erstens: das Startverzeichnis war /root oder nichts. Eine Tabelle _DEFAULT_CWD_BY_BACKEND deckte nur ssh und vercel_sandbox ab. Jedes andere Backend fiel auf /root zurück, und ein Plugin hatte keine Möglichkeit zu sagen, wo es starten soll.

Zweitens: der Container-Guard verwarf die Home-Pfade der Sandbox. Das Arbeitsverzeichnis jedes Befehls kommt aus dem in der Session gespeicherten cwd, sonst aus dem Config-Default (_resolve_command_cwd) — und execute() bevorzugt diesen Wert gegenüber dem env.cwd der Umgebung. Also gingen Befehle weiter nach /root, und wenn du in /home/acme/project cd-test, stufte der Guard /home/... als Host-Pfad ein und verwarf ihn beim nächsten Befehl. Die Datei-Tools behielten den Pfad, weshalb Befehle und Datei-Tools auf verschiedenen Planeten landeten.

Drittens: stream-fähige Backends waren ein festes Tuple. exec_prefix() in tools/environments/streams.py erkannte die Environment-Klassen Docker, SSH und Singularity, und tools/bot_desktop/placement.py hatte _STREAM_BACKENDS = ("docker", "ssh", "singularity") hart kodiert. Ein Plugin konnte nur beitreten, indem es den Kern editierte und seinen eigenen Namen hinzufügte.

Attribut eins: default_cwd

Ein Provider fügt eine einzige schreibgeschützte Property hinzu. Gib einen absoluten Pfad innerhalb der Sandbox zurück, oder "~" für „das eigene Home jeder Sandbox“:

from agent.terminal_env_provider import TerminalEnvironmentProvider


class AcmeBoxProvider(TerminalEnvironmentProvider):
    name = "acmebox"
    is_container = True

    @property
    def default_cwd(self):
        return "/home/acme"        # bekanntes Home: ein absoluter Pfad genügt

Bei Sandboxes, deren Home erst nach dem Verbinden bekannt ist — der Normalfall bei einem Hosted-Space-Produkt — gib "~" zurück:

    @property
    def default_cwd(self):
        return "~"

Hermes expandiert diese Tilde nie auf dem Host. Es reicht cwd="~" unverändert an create_environment() weiter, und deine Umgebung löst sie auf das Home auf, das sie tatsächlich bekommen hat:

class AcmeBoxEnvironment(BaseEnvironment):
    def __init__(self, *, cwd, timeout, sandbox):
        if cwd == "~":
            cwd = sandbox.home()      # z. B. /home/acme
        super().__init__(cwd=cwd, timeout=timeout)

Hermes schreibt diesen aufgelösten Pfad anschließend auf das Environment-Objekt. Damit überlebt jedes cd unterhalb des Sandbox-Homes in spätere Befehle, und die Datei-Tools verankern relative Pfade an derselben Stelle — die Trennung zwischen Befehls-cwd und Datei-Tool-cwd verschwindet.

Drei Details sind in der Praxis wichtig. default_cwd wird verwendet, wenn terminal.cwd nicht gesetzt ist oder wenn der Container-Guard ein host-artiges terminal.cwd verwirft. Der Wert wird bei jedem Befehl gelesen, halte ihn also billig — keine Netzwerkaufrufe. Relative Pfade werden ignoriert, weil Hermes sie auf dem Host verankern würde.

Attribut zwei: supports_streams und exec_prefix()

Der Bildschirm, computer_use und der Browser folgen dem Terminal in eine Sandbox, indem sie dort langlebige Prozesse mit offen gehaltenem stdin und stdout starten. Docker, SSH und Singularity machen das schon immer so. Ein Plugin-Backend kann jetzt einsteigen:

  1. Setze supports_streams = True auf dem Provider. bot_desktop.placement: auto behandelt dein Backend dann als gültiges Placement-Ziel, statt jedes Plugin-Backend abzulehnen.
  2. Gib der Umgebung eine Methode exec_prefix(*, user=None, interactive=True). Sie liefert ein lokales argv, das alles Nachfolgende in der Sandbox ausführt — die Form von docker exec -i <container> — oder None, wenn es gerade nicht geht.
class AcmeBoxEnvironment:
    def exec_prefix(self, *, user=None, interactive=True):
        return ["acmebox", "exec", "--stdin", self.sandbox_id, "--"]

Drei Konventionen: Hermes hängt bash -c <script> als separate Wörter an dein argv an, der Client muss sie also unverändert durchreichen und die Sandbox braucht bash; gib eine Liste zurück, niemals einen einzelnen Befehlsstring; user benennt das Konto, unter dem ausgeführt wird, also ignoriere den Parameter, wenn dein Backend keine Benutzer wechseln kann.

Auch auf der Image-Seite gibt es Anforderungen: Xvnc, Xfce, Chromium und cua-driver müssen in der Sandbox liegen — nousresearch/hermes-sandbox:desktop bringt sie mit. Bot Desktop und der Browser legen ihre Scratch-Dateien unter dem get_temp_dir() der Umgebung ab.

Beide Attribute sind optional und standardmäßig aus. Ein Plugin, das keines von beiden setzt, verhält sich exakt wie zuvor.

Zwei Fallstricke

Der Isolationsmodus frisst beide Features. Sowohl exec_prefix() als auch die ~-Home-Auflösung lesen das Environment-Objekt selbst, funktionieren also nur für Plugins, die in-process geladen sind. Unter plugins.isolation: host erreicht die Umgebung Hermes als opaker Platzhalter: Bot Desktop lehnt ein supports_streams-Backend mit einer Meldung ab, die die Einstellung nennt, und default_cwd = "~" bleibt eine wörtliche Tilde. Wenn dein Backend den Bildschirm beherbergen soll, lass dieses Plugin nicht isoliert laufen.

Gib ihm keine Host-Pfade. default_cwd akzeptiert einen absoluten Pfad innerhalb der Sandbox oder "~". Ein Host-Pfad wie /Users/you/project wirft keinen Fehler — er gilt als unbrauchbar und es greift der Fallback. Ebenso behält der Guard nur ein cd bei, das unter dem von dir deklarierten Home landet, eine Sandbox-Home, die bloß wie /home/... aussieht, braucht also trotzdem ein gesetztes default_cwd.

Kannst du es heute nutzen?

Die Änderung liegt auf main (gemergt am 20. Oktober 2026), ist aber in keinem Release: die letzte veröffentlichte Version ist v0.22.2 und damit älter als dieses Interface. Upgrade, sobald der nächste Patch erscheint, oder folge main. Für den größeren Zusammenhang lies den terminal backend plugin guide; die Befehle zum Installieren, Aktivieren und Isolieren von Plugins stehen in der hermes plugins reference; und die Arbeit am Plugin-Lebenszyklus behandelt der plugin lifecycle hooks post.

Die Kurzfassung

Ein Plugin-Terminal-Backend kann jetzt zwei Fragen selbst beantworten: wo Befehle starten (default_cwd, entweder ein absoluter Pfad oder "~") und ob der Bildschirm darin leben darf (supports_streams plus exec_prefix()). Beides ist optional bei unveränderten Defaults — und der eine Punkt, um den du herumplanen musst, ist der Isolationsmodus, in dem beides zu einem Platzhalter verkommt.