Backends de sandbox
Responsabilidad
El agent necesita ejecutar comandos y correr código, pero no puede permitir que comandos arbitrarios caigan directamente en la máquina host. La herramienta (tool) de terminal de Hermes soporta 6 backends de ejecución, elegidos según el balance seguridad/aislamiento/coste: Local (directo en host), Docker (aislamiento por contenedor), SSH (máquina remota), Singularity (contenedor HPC), Modal (sandbox en la nube, direct o managed), Daytona (entorno de desarrollo en la nube). Endurecimiento de contenedor + aislamiento por namespace; los comandos peligrosos, además, los intercepta un callback de aprobación.
Motivo de diseño
¿Por qué 6 backends en lugar de 1? Los escenarios de ejecución del agent son demasiado dispares: en desarrollo solo se quiere correr rápido en local; en producción batch hace falta aislamiento por contenedor; los usuarios de HPC están obligados a Singularity (no tienen Docker); el cloud sin supervisión necesita un sandbox por segundos como Modal. Un único backend no lo cubre todo. Hermes abstrae «ejecución» a una interfaz común y deja que los backends se monten según el escenario; el callback de aprobación de comandos peligrosos se comparte entre todos. El backend de Docker detecta específicamente si un bind mount expone rutas del host — la premisa de seguridad del contenedor es que «el contenedor no ve fuera», y en cuanto hay un docker run -v /:/host se rompe. Modal distingue direct (credenciales del propio usuario) y managed (gestionadas por el gateway), porque el primero va a la factura del usuario y el segundo al cupo de Nous Portal; permisos y rutas de facturación son completamente distintos. Esa lógica de detección no se mete dentro de cada backend, sino que se abstrae a tool_backend_helpers.py, desacoplando la elección de backend del juicio del nivel de seguridad.
Archivos clave
cabecera de terminal_tool:1-15— enumera los seis backends local / docker / modal / ssh / singularity / daytonadetección de acceso al host en Docker:260-290—_docker_volume_uses_host_path/_docker_has_host_access(detecta si un bind mount expone rutas del host)import de Singularity / Modal:65-90—_get_scratch_dir(de environments/singularity)environments/singularity.py— directorio scratch de Singularity y caché SIFresolución de modo Modal:77-137—coerce_modal_mode/has_direct_modal_credentials/resolve_modal_backend_state(direct vs managed)_DEFAULT_BROWSER_PROVIDER:12— el provider por defecto es localcallback de aprobación del subagente:75-103—_subagent_auto_deny/_subagent_auto_approve(control de comandos peligrosos ejecutados dentro del sandbox)tools/daemon_pool.py— pool de hilos residente del subagente (aloja la ejecución en sandbox)directorio tools/— terminal / close_terminal / read_terminal / tool_backend_helpers / environments/
Flujo de datos
- El agent decide ejecutar un comando →
tool_call terminal→tools/terminal_tool.py. - Se elige el backend según la configuración del job (local/docker/modal/ssh/singularity/daytona):
local: ejecución directa en host (lo más rápido, por defecto)docker:tools/terminal_tool.py:260detecta si un bind mount expone rutas del host y decide los permisos en consecuenciamodal: víatools/tool_backend_helpers.py:102se resuelve si es direct (credenciales propias del usuario) o managed (gestionadas por el gateway)singularity: víatools/environments/singularity.pyprepara scratch y SIFssh/daytona: entorno remoto / en la nube
- Los comandos peligrosos los intercepta un callback de aprobación: el bucle principal usa aprobación interactiva; los subagentes por defecto auto-deny (
tools/delegate_tool.py:75); cron/batch puede opt-in a auto-approve. - La salida del comando se devuelve al bucle principal; las tareas largas se pueden pasar a background (
tools/daemon_pool.py).
La declaración de la lista de backends es minimalista; la cabecera de terminal_tool.py lleva solo un comentario: «A terminal tool that executes commands in local, Docker, Modal, SSH, Singularity, and Daytona environments.».
El backend de Docker detecta si un bind mount apunta a una ruta del host mirando el prefijo del spec: si empieza por /, ~, ./, ../, o el segundo carácter es : (letra de unidad Windows tipo C:\), se considera ruta del host y se activa has_host_access=True, ramificando después hacia permisos más estrictos:
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 ("/", "\\"))
)La resolución de Modal tiene tres modos: direct (credenciales propias del usuario), managed (Nous Portal gestionado) y auto (por defecto; si managed no está disponible, cae a direct). resolve_modal_backend_state centraliza la decisión en un único sitio y devuelve None si no hay backend. has_direct_modal_credentials mira a la vez las variables de entorno MODAL_TOKEN_ID/MODAL_TOKEN_SECRET y el fichero ~/.modal.toml; solo si faltan ambos, direct no está disponible. managed_nous_tools_enabled consulta la cuenta de Nous Portal y, si falla, fail closed — una consulta rota no abre por error la ruta managed.
Singularity existe por el escenario HPC — el usuario no puede instalar Docker, solo tiene Apptainer/Singularity. _find_singularity_executable busca primero apptainer y luego singularity; si no encuentra ninguno, lanza un error con la dirección de instalación. El directorio scratch prefiere /scratch (mount estándar en clústeres HPC); si no existe, cae a get_sandbox_dir() / "singularity". El contenedor se arranca con --containall --no-home y capability dropping — en un clúster HPC compartido hay que correr así, si no se ven directorios home de otros usuarios.
Límites y fallos
- Fallo al juzgar bind mount en Docker: la función de detección trata como ruta host lo que empieza por
/,~,./,../, pero un named volume del estilomydata:/datase admite. Si ese named volume está respaldado por una ruta del host (driver custom),_docker_has_host_accessno lo ve y lo marca como bajo riesgo por error. - Fallback de Modal managed: en modo
auto, si managed no está disponible cae a direct; pero si el usuario solo configuró managed y no tiene credenciales direct,selected_backendqueda aNoney la selección falla solo en runtime, sin un error claro en el momento de crear el terminal. - Caché SIF de Singularity hinchada: una imagen SIF suele ocupar de cientos de MB a varios GB; sin rotación en scratch el disco revienta. El
/scratchde los nodos HPC suele tener política de limpieza a nivel de clúster; si ambas políticas no están alineadas, aparece la situación «el clúster borró la caché, el agent sigue creyendo que el SIF está». - Temblores de red en backends remotos: la ejecución por SSH/Daytona va por red; si la conexión cae, la salida del subproceso en marcha se pierde. El rellenado de salida asume una sola respuesta síncrona, sin reanudación; las tareas largas en backends remotos corren el mayor riesgo.
- Timeouts y OOM: local/docker tienen
script_timeout, pero el timeout de los backends remotos depende de que el proceso remoto se mate a sí mismo; si el planificador local no recibe respuesta, se cuelga o el lease TTL de_run_job_script_with_claim_heartbeatlo marca como muerto por error.
Resumen
La capa de sandbox es «aislamiento de ejecución + control de comandos peligrosos»: 6 backends cubren desde host hasta HPC hasta nube; Docker detecta bind mounts para evitar escalado; Modal distingue credenciales direct/managed; los comandos peligrosos pasan todos por un callback de aprobación. Los subagentes y el cron siguen la ruta no interactiva de auto-deny/auto-approve.