Sandbox-Backends (sandbox backends)
Verantwortung
Der Agent muss Befehle ausführen und Code laufen lassen, aber beliebige Befehle dürfen nicht direkt auf den Host durchschlagen. Das Terminal-Werkzeug (tool) von Hermes unterstützt 6 Ausführungs-Backends, gewählt nach Sicherheits-/Isolations-/Kosten-Trade-off: Local (direkt auf dem Host), Docker (Container-Isolation), SSH (entfernte Maschine), Singularity (HPC-Container), Modal (Cloud-Sandbox (sandbox), direct oder managed), Daytona (Cloud-Dev-Umgebung). Container-Härtung + Namespace-Isolation; gefährliche Befehle werden zudem von Approval-Callbacks abgefangen.
Designmotiv
Warum 6 statt 1? Die Ausführungsszenarien des Agenten spannen zu weit — bei der Entwicklung will man nur schnell lokal laufen, in Produktion für Batch brauchen wir Container-Isolation, HPC-Nutzer müssen in Singularity arbeiten (Docker geht nicht), in der Cloud unbeaufsichtigt braucht man einen sekundengenau abrechnenden Cloud-Sandkasten wie Modal. Kein einzelnes Backend deckt alles. Hermes abstrahiert «Ausführung» als einheitliche Schnittstelle; Backends werden nach Szenario zusammengesetzt; der Approval-Callback für gefährliche Befehle ist backend-übergreifend gleich. Das Docker-Backend prüft explizit, ob ein Bind Mount Host-Pfade freilegt, weil die Sicherheitsannahme der Container-Isolation lautet «Container sieht nicht nach draußen»; sobald docker run -v /:/host dabeistehn, ist das gebrochen. Modal unterscheidet direct (Credentials des Nutzers) und managed (vom Gateway (gateway) verwaltet), denn jenes läuft über die Nutzerrechnung, dieses über das Nous-Portal-Kontingent — Berechtigungs- und Abrechnungspfad sind völlig verschieden. Diese Prüflogik liegt nicht im Backend, sondern in tool_backend_helpers.py, sodass Backend-Wahl und Sicherheitsbewertung entkoppelt sind.
Schlüsseldateien
terminal_tool Kopfdoku:1-15— listet die sechs Backends local / docker / modal / ssh / singularity / daytona aufDocker Host-Zugriff-Detektion:260-290—_docker_volume_uses_host_path/_docker_has_host_access(erkennt, ob ein Bind Mount Host-Pfade freilegt)Singularity / Modal-Import:65-90—_get_scratch_dir(aus environments/singularity)environments/singularity.py— Singularity-Scratch-Verzeichnis und SIF-CacheModal-Modus-Auflösung:77-137—coerce_modal_mode/has_direct_modal_credentials/resolve_modal_backend_state(direct vs. managed)_DEFAULT_BROWSER_PROVIDER:12— Default-Provider (provider) ist localSubagenten-Approval-Callback:75-103—_subagent_auto_deny/_subagent_auto_approve(Kontrolle gefährlicher Befehle in der Sandbox)tools/daemon_pool.py— residenter Thread-Pool für Subagenten (trägt die Sandbox-Ausführung)tools/-Verzeichnis— terminal / close_terminal / read_terminal / tool_backend_helpers / environments/
Datenfluss
- Der Agent will einen Befehl ausführen →
tool_call terminal→tools/terminal_tool.py. - Das Backend wird gemäß Job-Konfiguration gewählt (local/docker/modal/ssh/singularity/daytona):
local: direkte Ausführung auf dem Host (schnellster Standard)docker:tools/terminal_tool.py:260prüft, ob ein Bind Mount Host-Pfade freilegt, und entscheidet darüber die Berechtigungenmodal:tools/tool_backend_helpers.py:102löst auf, ob direct (eigene Modal-Anmeldedaten des Nutzers) oder managed (vom Gateway verwaltet)singularity:tools/environments/singularity.pyrichtet Scratch und SIF einssh/daytona: Remote-/Cloud-Umgebung
- Gefährliche Befehle werden von Approval-Callbacks abgefangen – die Hauptschleife nutzt interaktive Approval; Subagenten sind per Default auto-deny (
tools/delegate_tool.py:75); Cron/Batch kann opt-in auto-approve aktivieren. - Die Befehlsausgabe wird in die Hauptschleife zurückgefüllt; lange Tasks können in den Hintergrund ausgelagert werden (
tools/daemon_pool.py).
Die Backend-Liste ist schlicht deklariert; im Kopf von terminal_tool.py steht ein Kommentar: «A terminal tool that executes commands in local, Docker, Modal, SSH, Singularity, and Daytona environments.»
Das Docker-Backend prüft, ob ein Bind Mount ein Host-Pfad ist, anhand des Spec-Präfix: beginnt er mit /, ~, ./, ../, oder ist (wie bei Windows-Laufwerken C:\) das zweite Zeichen ein Doppelpunkt, gilt er als Host-Pfad, has_host_access=True schlägt an und die Berechtigungen gehen in den strengeren Zweig:
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-Auflösung hat drei Stufen: direct (eigene Modal-Credentials), managed (Nous-Portal-verwaltet), auto (Default; fällt auf direct zurück, wenn managed nicht verfügbar). resolve_modal_backend_state zentralisiert das an einer Stelle; wählt es kein Backend, gibt es None zurück. has_direct_modal_credentials schaut gleichzeitig auf die Umgebungsvariablen MODAL_TOKEN_ID/MODAL_TOKEN_SECRET und auf die Datei ~/.modal.toml; fehlen beide, ist direct nicht verfügbar. managed_nous_tools_enabled geht über eine Nous-Portal-Kontoabfrage und fail-closed bei Fehler; ein Abfragefehler schaltet nicht versehentlich den managed-Pfad frei.
Singularity ist das HPC-Sonderexistenz — Nutzer können Docker nicht installieren, nur Apptainer/Singularity. _find_singularity_executable sucht zuerst apptainer, dann singularity; fehlen beide, wird mit Installationshinweis gemeldet. Das Scratch-Verzeichnis bevorzugt /scratch (Standard-Mount auf HPC-Clustern), sonst fällt es auf get_sandbox_dir() / "singularity" zurück. Der Container läuft mit --containall --no-home plus Capability-Dropping — auf einem geteilten HPC-Cluster zwingend, sonst sähe man in die Heimatverzeichnisse anderer Nutzer.
Grenzen und Fehler
- Docker-Bind-Mount-Fehlurteil: Die Prüffunktion wertet Spec-Anfänge mit
/,~,./,../als Host-Pfad; ein Named Volume (z. B.mydata:/data) wird jedoch durchgewunken. Hat das Named Volume einen Custom-Driver, der einen Host-Pfad bindet, sieht_docker_has_host_accessdas nicht und stuft es fälschlich als niedriges Risiko ein. - Modal-managed-Fehlschlag und Fallback (fallback): Im
auto-Modus fälltmanaged-nicht-verfügbar aufdirectzurück; hat der Nutzer aber nurmanagedund keinedirect-Credentials, wirdselected_backendzuNone; die Backend-Wahl scheitert erst zur Laufzeit, beim Anlegen des Terminals ist der Fehler möglicherweise unklar. - Singularity-SIF-Cache wächst: SIF-Images sind oft mehrere 100 MB bis GB; ohne Rotationsstrategie für das Scratch-Verzeichnis sprengt es die Platte;
/scratchauf HPC-Knoten hat meist eine clusterweite Aufräum-Strategie; passen beide nicht zusammen, kann der Cache vom Cluster weggeräumt werden, während der Agent noch denkt, das SIF sei da. - Remote-Backend-Netzschwankung: SSH-/Daytona-Befehl-Ausführung geht über das Netz; bricht die Verbindung, ist die Ausgabe des laufenden Kindprozesses weg. Die Rückfüllung nimmt eine einzelne synchrone Rückgabe an; ein Reconnect mit Fortsetzung gibt es nicht — lange Tasks sind auf Remote-Backends am riskantesten.
- Timeout und OOM: local/docker haben
script_timeout, aber Remote-Backends müssen sich selbst killen; der lokale Scheduler wartet auf eine Antwort, hängt oder wird vom Lease-TTL von_run_job_script_with_claim_heartbeatfälschlich als tot eingestuft.
Zusammenfassung
Die Sandbox-Schicht ist «Ausführungs-Isolation + Kontrolle gefährlicher Befehle»: 6 Backends decken Host bis HPC bis Cloud ab; Docker prüft Bind-Mounts gegen Rechteausweitung; Modal unterscheidet direct-/managed-Anmeldedaten; gefährliche Befehle laufen einheitlich über Approval-Callbacks. Subagenten und Cron laufen über den nicht-interaktiven Pfad auto-deny/auto-approve.