Skip to content

Backends de sandbox

源码版本v2026.7.20

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

Flujo de datos

  1. El agent decide ejecutar un comando → tool_call terminaltools/terminal_tool.py.
  2. Se elige el backend según la configuración del job (local/docker/modal/ssh/singularity/daytona):
  3. 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.
  4. 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:

python
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 estilo mydata:/data se admite. Si ese named volume está respaldado por una ruta del host (driver custom), _docker_has_host_access no 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_backend queda a None y 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 /scratch de 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_heartbeat lo 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.

Sitio de aprendizaje comunitario no oficial. Basado en el código fuente de NousResearch/hermes-agent (licencia MIT).