Skip to content

Programación cron

源码版本v2026.7.20

Responsabilidad

Permite al agent «arrancarse solo a la hora fijada»: informes, backups, resúmenes, etc. sin que haya nadie presente. El subsistema cron/ ofrece planificador, almacenamiento de jobs, registro de ejecuciones, guardián de ciclo de vida, catálogo de blueprints/sugerencias y aislamiento independiente de toolsets (los jobs programados pueden desactivar herramientas (tool) para evitar acciones peligrosas sin supervisión). El gateway impulsa el planificador con un latido de 60s.

Motivo de diseño

Sin supervisión es el problema central de este subsistema. Si el agent se levanta solo a las tres de la madrugada, ¿quién lo vigila? La respuesta de Hermes es «degradar privilegios en cada capa de la cadena de planificación»: los toolsets cortan por defecto las herramientas interactivas y la cronjob (que podría abrir más cron); lifecycle_guard bloquea comandos del estilo systemctl restart hermes-gateway antes de que el script se planifique; la tabla executions deja rastro auditable de cada corrida y, si hay crash, marca unknown en lugar de fingir éxito. Que «el agent corra solo» sea auditable y reversible.

Archivos clave

Flujo de datos

  1. El gateway arranca _start_cron_ticker:22335, que dispara el planificador cada 60s (en la práctica se migró a cron/scheduler_provider.py::InProcessCronScheduler; el símbolo viejo sigue como shim).
  2. El planificador lee el almacenamiento de jobs (cron/jobs.py:106) y, según la expresión cron, selecciona los jobs vencidos.
  3. Para cada job vencido:
  4. Si cae a mitad de la ejecución, la próxima vez recover_interrupted_executions (cron/executions.py:156) recupera la marca.
  5. El resultado se entrega vía el gateway.

El planificador recorta tres toolsets hardcoded, sobre los que se suma el agent.disabled_toolsets del config.yaml del usuario:

python
def _resolve_cron_disabled_toolsets(cfg: dict) -> list[str]:
    """Toolsets a cron-spawned agent must never receive."""
    disabled = ["cronjob", "messaging", "clarify"]
    agent_cfg = (cfg or {}).get("agent") or {}
    user_disabled = agent_cfg.get("disabled_toolsets") or []
    for name in user_disabled:
        name = str(name).strip()
        if name and name not in disabled:
            disabled.append(name)
    return disabled

cronjob se desactiva para que cron no pueda abrir más cron (un tropiezo podría encadenar un montón de jobs); messaging/clarify son interactivos y, sin nadie, se quedarían colgados esperando. Los jobs tipo script van por _run_job_script con la ruta confinada a HERMES_HOME/scripts/ para evitar path traversal.

La máquina de estados de ejecución vive en executions.py; los estados son solo claimed → running → completed/failed/unknown. El estado terminal se escribe una sola vez (el UPDATE de finish_execution lleva WHERE status IN ('claimed','running'); escribir dos veces devuelve rowcount=0 y None). La recuperación tras crash solo marca unknown las ejecuciones cuyo proceso ya no existe — no finge éxito, ni relanza automáticamente: el subproceso puede haber escrito efectos secundarios o no; el operador debe investigarlo, en lugar de asumir que un failed significa «relanzar y listo». lifecycle_guard compila systemctl restart hermes-gateway o pkill hermes gateway en regexes y escanea dos veces (al crear y antes de ejecutar), uniendo prompt y script en un solo escaneo — para tapar el agujero de «decirlo en el prompt, ejecutarlo en el script».

Límites y fallos

  • Deriva de segundos y daño colateral: el latido de 60s implica que el disparo puede desfasarse hasta un minuto. mark_running_jobs_interrupted no distingue qué PID se mató; cualquier job que estuviera corriendo en el instante del kill se interrumpe, lo que puede afectar a un job a punto de terminar.
  • Efectos secundarios del estado unknown: tras un crash se marca unknown y no se relanza, pero el script puede haber enviado mensajes o escrito archivos. Lo de abajo no puede asumir «no completado».
  • Latido de lease atascado: _run_job_script_with_claim_heartbeat renueva el lease en un hilo aparte; si ese hilo se queda colgado (p. ej. un GIL retenido mucho rato), otro proceso de planificación puede planificar el mismo one-shot por segunda vez y provocar efectos secundarios duplicados.
  • Zona ciega de prompt injection: el escaneo del assembled prompt (con contenido de skill) se hace en runtime; una skill maliciosa solo se detecta al dispararse el job, no al crearlo.

Resumen

El énfasis del subsistema cron está en la «seguridad sin supervisión»: aislamiento de toolsets, lifecycle_guard contra modificaciones accidentales, y máquina de estados de ejecución para recuperación tras crash. La planificación la impulsa el latido del propio gateway, sin necesidad de un demonio cron externo.

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