Cron — planification
Responsabilité
Permettre à l'agent de « tourner tout seul à l'heure » : rapports programmés, sauvegardes, briefings sans présence humaine. Le sous-système cron/ fournit un scheduler, un stockage des jobs, un historique d'exécution, un garde-fou du cycle de vie, un catalogue de blueprints / suggestions, ainsi qu'une isolation par toolsets dédiée (les jobs planifiés peuvent désactiver certains outils (tools), pour éviter les actions dangereuses en mode sans surveillance). La passerelle (gateway) pilote le scheduler via un heartbeat de 60 s.
Mot de conception
Le mode sans surveillance est le problème central de ce sous-système. L'agent se lance tout seul à trois heures du matin — qui surveille ? La réponse d'Hermes est « réduire les privilèges à chaque maillon de la chaîne de scheduling » : les toolsets rognent par défaut les outils interactifs et le toolset cronjob qui pourrait re-créer des cron ; lifecycle_guard bloque avant scheduling les commandes du type systemctl restart hermes-gateway présentes dans le script ; la table executions laisse une trace vérifiable pour chaque exécution, et après un crash marque unknown plutôt que de prétendre au succès. L'objectif : « l'agent tourne tout seul » reste auditable et réversible.
Fichiers clés
def run_job:2659— cœur d'exécution d'un job_run_job_script / _run_job_script_with_claim_heartbeat:2113-2247— exécution de job script + heartbeat de bailétat d'exécution et interruption:352-445—get_running_job_ids/mark_running_jobs_interrupted/_consume_interrupted_flagrésolution toolsets:143-210—_resolve_cron_disabled_toolsets/_resolve_cron_enabled_toolsets(isolation d'outils pour les jobs planifiés)pool de threads:501-547—_get_parallel_pool/_get_sequential_pool_CronStorePaths / _current_cron_store:106-157— chemins de stockage des jobsverrous et répertoire de sortie:251-360—_jobs_lock_file/_job_output_dir_normalize_job_record:426-461— normalisation d'un enregistrement de jobmachine à états d'exécution:99-156—create_execution/mark_execution_running/finish_executionrecover_interrupted_executions:156-213— récupération post-crash +list_executions/latest_executioncheck_gateway_lifecycle:69-112— empêche les scripts cron de modifier par erreur le cycle de vie de la passerelleblueprint_catalog— catalogue de blueprints de jobssuggestion_catalog/suggestions— génération de suggestions cron_start_cron_ticker:22335— heartbeat 60 s dans la passerelle pilotant le scheduler
Flux de données
- La passerelle démarre
_start_cron_ticker:22335, qui déclenche le scheduler toutes les 60 s. - Le scheduler lit le stockage des jobs (
cron/jobs.py:106) et sélectionne les jobs dus selon l'expression cron. - Pour chaque job dû :
- passage par
check_gateway_lifecycle:112qui scanne le script, pour empêcher toute modification du cycle de vie de la passerelle - résolution des toolsets du job (
cron/scheduler.py:210), avec désactivation usuelle des outils dangereux (même approche que l'auto-deny du sous-agent) - enregistrement d'une exécution dans
create_execution(cron/executions.py:99) run_job(cron/scheduler.py:2659) exécute réellement : script →_run_job_script:2113, conversation → alimenterun_conversation:588
- passage par
- Crash en cours d'exécution → la prochaine exécution de
recover_interrupted_executions(cron/executions.py:156) récupère le marquage. - Le résultat est livré via la passerelle (ou mirroré vers une plateforme donnée).
Le rognage du toolset par le scheduler est codé en dur sur trois entrées, auquel s'ajoute agent.disabled_toolsets du config.yaml utilisateur :
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 disabledcronjob est désactivé pour empêcher cron de se re-créer du cron (un déclenchement par erreur pourrait cascadement ouvrir plein de jobs) ; messaging / clarify sont interactifs — en mode sans surveillance ils resteraient bloqués à attendre un humain. Les jobs script passent par _run_job_script, avec le chemin enfermé dans HERMES_HOME/scripts/ pour empêcher la traversée.
La machine à états d'exécution est dans executions.py ; les états sont uniquement claimed → running → completed/failed/unknown, l'état terminal ne s'écrit qu'une fois (le UPDATE de finish_execution est protégé par WHERE status IN ('claimed','running') ; une double écriture renvoie rowcount=0 et donc None). La récupération post-crash marque seulement les exécutions « dont le processus n'existe plus » en unknown — ni prétendre au succès, ni relancer automatiquement : le sous-processus a peut-être déjà écrit des effets de bord, ou pas ; on laisse l'opérateur enquêter plutôt que de deviner failed et faire croire qu'il suffit de relancer. lifecycle_guard compile systemctl restart hermes-gateway, pkill hermes gateway en regex, scannées à la création et avant exécution ; il concatène prompt et script pour le scan, afin de boucher la faille « dire un truc dans le prompt, faire l'autre dans le script ».
Limites et échecs
- Dérive seconde et dommage collatéral : le heartbeat 60 s signifie que le point de déclenchement peut être décalé d'une minute au maximum.
mark_running_jobs_interruptedne distingue pas quel PID a été tué — tout job en cours d'exécution au moment du kill est interrompu, ce qui peut faucher un job sur le point de finir. - Effets de bord de l'état
unknown: après crash, la récupération marqueunknownet ne relance pas ; mais le script a peut-être déjà envoyé un message ou écrit un fichier. Les consommateurs en aval ne doivent pas supposer « non terminé ». - Heartbeat de bail bloqué :
_run_job_script_with_claim_heartbeatutilise un thread séparé pour renouveler le bail ; si ce thread se coince lui-même (GIL tenu longtemps), un autre processus de scheduling peut réassigner le même job one-shot et produire des effets de bord en double. - Angle mort d'injection de prompt : le scan du prompt assemblé (qui inclut le contenu des skills) ne se fait qu'à l'exécution ; une skill malveillante n'est interceptée qu'au moment où le cron se déclenche, et reste invisible à la création du job.
Résumé
Le point clé du sous-système cron est « la sécurité en mode sans surveillance » : isolation des toolsets, lifecycle_guard contre les fausses manœuvres, machine à états d'exécution pour la récupération post-crash. Le scheduling lui-même est piloté par le heartbeat de la passerelle, sans daemon cron externe requis.