Los backends de terminal de plugin ya no se quedan atascados en /root: default_cwd y supports_streams

Conectas un backend de terminal de plugin a Hermes — Cua Spaces, una VM de QEMU o el servicio de ejecución interno de tu empresa. El plugin carga, terminal.backend apunta a él y el primer comando aterriza en /root, aunque el proyecto viva en /home/acme/project. Le pides al agente que haga cd hasta allí y el siguiente comando vuelve a estar en /root. Lo raro es que read_file sí resuelve la ruta del proyecto. Pruebas a meter Bot Desktop dentro de ese sandbox y se niega, sin una explicación clara.
Nada de eso era culpa de tu plugin. Lo provocaban tres suposiciones hardcodeadas en el core, y el PR #142225, mergeado el 20 de octubre de 2026, las convirtió en opciones que el provider puede declarar. Aquí tienes el mecanismo, la API y los dos escollos con los que conviene diseñar.
Las tres suposiciones hardcodeadas
Una: el directorio de arranque era /root o nada. Una tabla _DEFAULT_CWD_BY_BACKEND cubría solo ssh y vercel_sandbox. Cualquier otro backend caía en /root, y un plugin no tenía forma de decir dónde debía empezar.
Dos: el guard de contenedor tiraba a la basura las rutas del home del sandbox. El directorio de trabajo de cada comando sale del cwd registrado en la session, o si no del valor por defecto de la config (_resolve_command_cwd), y execute() prefiere ese valor frente al env.cwd del propio entorno. Así que los comandos seguían yendo a /root, y cuando hacías cd a /home/acme/project, el guard clasificaba /home/... como ruta del host y la descartaba en el comando siguiente. Las herramientas de archivos sí conservaban la ruta, y de ahí que comandos y herramientas acabaran en planetas distintos.
Tres: los backends con capacidad de stream eran una tupla fija. exec_prefix() en tools/environments/streams.py reconocía las clases de entorno Docker, SSH y Singularity, y tools/bot_desktop/placement.py hardcodeaba _STREAM_BACKENDS = ("docker", "ssh", "singularity"). Un plugin solo podía sumarse editando el core y añadiendo su propio nombre.
Atributo uno: default_cwd
El provider añade una propiedad de solo lectura. Devuelve una ruta absoluta dentro del sandbox, o "~" para decir “el home de cada sandbox”:
from agent.terminal_env_provider import TerminalEnvironmentProvider
class AcmeBoxProvider(TerminalEnvironmentProvider):
name = "acmebox"
is_container = True
@property
def default_cwd(self):
return "/home/acme" # home conocido: basta con una ruta absoluta
Para sandboxes cuyo home solo se conoce después de conectar — el caso normal en un producto de espacios alojados — devuelve "~":
@property
def default_cwd(self):
return "~"
Hermes nunca expande esa tilde en el host. Pasa cwd="~" a create_environment() tal cual, y es tu entorno quien la resuelve al home que realmente le tocó:
class AcmeBoxEnvironment(BaseEnvironment):
def __init__(self, *, cwd, timeout, sandbox):
if cwd == "~":
cwd = sandbox.home() # p. ej. /home/acme
super().__init__(cwd=cwd, timeout=timeout)
Después Hermes graba esa ruta resuelta en el objeto de entorno, de modo que cualquier cd dentro del home del sandbox sobrevive a los comandos posteriores y las herramientas de archivos anclan las rutas relativas en el mismo sitio: la división entre el cwd de los comandos y el de las herramientas desaparece.
Tres detalles importan en la práctica. default_cwd se usa cuando terminal.cwd no está definido, o cuando el guard de contenedor descarta un terminal.cwd que parece del host. Se lee en cada comando, así que mantenlo barato: nada de llamadas de red. Las rutas relativas se ignoran, porque Hermes las anclaría en el host.
Atributo dos: supports_streams y exec_prefix()
La pantalla, computer_use y el navegador siguen al terminal dentro del sandbox lanzando allí procesos de larga vida con stdin y stdout abiertos. Docker, SSH y Singularity siempre lo han hecho así. Ahora un backend de plugin puede apuntarse:
- Pon
supports_streams = Trueen el provider. Con eso,bot_desktop.placement: autotrata tu backend como destino válido en lugar de rechazar todo backend de plugin. - Dale al entorno un método
exec_prefix(*, user=None, interactive=True). Devuelve un argv local que ejecuta lo que venga después dentro del sandbox — la forma dedocker exec -i <container>— oNonecuando ahora mismo no puede.
class AcmeBoxEnvironment:
def exec_prefix(self, *, user=None, interactive=True):
return ["acmebox", "exec", "--stdin", self.sandbox_id, "--"]
Tres convenciones: Hermes añade bash -c <script> como palabras separadas después de tu argv, así que el cliente debe pasarlas sin tocar y el sandbox necesita bash; devuelve una lista, nunca una única cadena de comando; user nombra la cuenta con la que ejecutar, así que ignóralo si tu backend no sabe cambiar de usuario.
El lado de la imagen también tiene requisitos: Xvnc, Xfce, Chromium y cua-driver deben estar en el sandbox — nousresearch/hermes-sandbox:desktop los incluye. Bot Desktop y el navegador guardan sus archivos temporales bajo el get_temp_dir() del entorno.
Ambos atributos son opcionales y vienen desactivados. Un plugin que no define ninguno se comporta exactamente como antes.
Dos escollos
El modo de aislamiento se come las dos funciones. Tanto exec_prefix() como la resolución del home con ~ leen el propio objeto de entorno, así que solo funcionan con plugins cargados en proceso. Con plugins.isolation: host, el entorno llega a Hermes como un placeholder opaco: Bot Desktop rechaza un backend con supports_streams con un mensaje que nombra el ajuste, y default_cwd = "~" se queda en una tilde literal. Si tu backend va a alojar la pantalla, no ejecutes ese plugin aislado.
No le pases rutas del host. default_cwd acepta una ruta absoluta dentro del sandbox o "~". Si le pasas una ruta del host como /Users/you/project no salta ningún error: se considera inutilizable y se cae al valor por defecto. Del mismo modo, el guard solo conserva un cd que acabe bajo el home que declaraste, así que un home de sandbox que simplemente se parezca a /home/... sigue necesitando default_cwd.
¿Puedes usarlo hoy?
El cambio está en main (mergeado el 20 de octubre de 2026) pero no en ninguna release: la última versión publicada es la v0.22.2, anterior a esta interfaz. Actualiza cuando salga el siguiente patch, o sigue main. Para el panorama general, lee la guía del plugin de backend de terminal; los comandos de instalación, activación y aislamiento de plugins están en la referencia de hermes plugins; y el ciclo de vida de los plugins se cubre en el post sobre plugin lifecycle hooks.
La versión corta
Un backend de terminal de plugin ya puede responder dos preguntas por sí mismo: dónde arrancan los comandos (default_cwd, una ruta absoluta o "~") y si la pantalla puede vivir dentro de él (supports_streams más exec_prefix()). Los dos son opcionales y los valores por defecto no cambian. Y lo único que hay que planificar es el modo de aislamiento, donde ambos se degradan a un placeholder.