Cron-Zeitsteuerung
Verantwortung
Den Agenten «zur richtigen Zeit automatisch laufen» lassen: Berichte, Backups, Briefings ohne menschliche Anwesenheit. Das cron/-Subsystem stellt Scheduler, Job-Speicher, Ausführungsprotokoll, Lifecycle-Guard, Blaupausen-/Vorschlagskatalog sowie eine eigene Toolset-Isolation bereit (geplante Jobs können Werkzeuge (tools) teilweise deaktivieren, um unbeaufsichtigte gefährliche Aktionen zu verhindern). Das Gateway (gateway) treibt den Scheduler mit einem 60s-Herzschlag.
Designmotiv
Unbeaufsichtigt ist das Kernproblem dieses Subsystems. Der Agent steht um drei Uhr morgens selbst auf — wer passt auf? Hermes antwortet mit «auf der Scheduler-Kette Stufe für Stufe Rechte abbauen»: Toolsets schneiden interaktive und cron-eröffnende Werkzeuge (cronjob) standardmäßig ab; lifecycle_guard fängt Befehle wie systemctl restart hermes-gateway im Skript ab, bevor es zur Ausführung kommt; die executions-Tabelle hinterlässt für jeden Lauf eine prüfbare Spur und markiert nach Crash unknown statt erfolgreich vorzutäuschen. «Der Agent läuft selbst» soll auditierbar und rollbackfähig sein.
Schlüsseldateien
def run_job:2659— Kern der Ausführung eines einzelnen Jobs_run_job_script / _run_job_script_with_claim_heartbeat:2113-2247— Skript-Job-Ausführung + Lease-HeartbeatLaufzustand und Abbruch:352-445—get_running_job_ids/mark_running_jobs_interrupted/_consume_interrupted_flagToolset-Auflösung:143-210—_resolve_cron_disabled_toolsets/_resolve_cron_enabled_toolsets(Werkzeug-Isolation für geplante Jobs)Thread-Pool:501-547—_get_parallel_pool/_get_sequential_pool_CronStorePaths / _current_cron_store:106-157— Job-SpeicherpfadeSperren und Output-Verzeichnis:251-360—_jobs_lock_file/_job_output_dir_normalize_job_record:426-461— Job-Datensatz-NormalisierungAusführungs-Zustandsautomat:99-156—create_execution/mark_execution_running/finish_executionrecover_interrupted_executions:156-213— Crash-Recovery +list_executions/latest_executioncheck_gateway_lifecycle:69-112— verhindert, dass Cron-Skripte versehentlich den Gateway-Lifecycle verändernblueprint_catalog— Job-Blaupausen-Katalogsuggestion_catalog/suggestions— Cron-Vorschlagserzeugung_start_cron_ticker:22335— 60s-Herzschlag im Gateway treibt den Scheduler
Datenfluss
- Das Gateway startet
_start_cron_ticker:22335, der den Scheduler alle 60s triggert (tatsächlich nachcron/scheduler_provider.py::InProcessCronSchedulerverschoben; das alte Symbol ist ein Shim). - Der Scheduler liest den Job-Speicher (
cron/jobs.py:106) und ermittelt nach Cron-Ausdrücken die fälligen Jobs. - Jeder fällige Job:
check_gateway_lifecycle:112scannt das Skript und blockt versehentliche Lifecycle-Änderungen am Gateway- Auflösung der Toolsets des Jobs (
cron/scheduler.py:210), meist werden gefährliche Werkzeuge deaktiviert (siehe denselben Auto-Deny-Gedanken bei Subagenten) create_execution(cron/executions.py:99) verbucht eine Ausführungrun_job(cron/scheduler.py:2659) führt tatsächlich aus: Skript-Jobs über_run_job_script:2113, Sitzungs-Jobs (session jobs) werden anrun_conversation:588gefüttert
- Crash während der Ausführung → beim nächsten Start stellt
recover_interrupted_executions(cron/executions.py:156) die Marker wieder her. - Das Ergebnis wird über das Gateway zugestellt (oder zu einer Plattform gespiegelt).
Der Scheduler beschneidet Toolsets hartkodiert um drei Einträge, darüber gelegt kommt agent.disabled_toolsets aus der config.yaml des Users:
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 ist verboten, damit Cron sich nicht selbst neuen Cron erzeugt (Versehen kaskadiert sonst eine Lawine von Jobs); messaging/clarify sind interaktiv und würden unbeaufsichtigt nur blocken. Skript-Jobs laufen über _run_job_script, deren Pfade in HERMES_HOME/scripts/ eingesperrt sind, um Traversal zu verhindern.
Der Ausführungs-Zustandsautomat liegt in executions.py; Zustände sind nur claimed → running → completed/failed/unknown, ein Endzustand wird nur einmal geschrieben (finish_execution schützt mit UPDATE ... WHERE status IN ('claimed','running'); doppelte Schreibversuche liefern rowcount=0 und geben None zurück). Crash-Recovery markiert nur Ausführungen, deren Prozess nicht mehr existiert, als unknown — sie tut nicht so, als wäre sie erfolgreich, und startet auch nicht automatisch neu; der Subprozess hat vielleicht schon Nebenwirkungen geschrieben, vielleicht auch nicht — das wird dem Operator überlassen zu prüfen, statt failed zu raten und ihn glauben zu lassen, ein Re-Run reiche. lifecycle_guard kompiliert systemctl restart hermes-gateway, pkill hermes gateway als Regex und scannt sowohl bei Erstellung als auch vor der Ausführung; Prompt (prompt) und Skript werden zusammengespannt gescannt, um die Split-Umgehung «im Prompt sagen, im Skript machen» zu verstopfen.
Grenzen und Fehler
- Sekunden-Drift und Kollateralschaden: Der 60s-Herzschlag bedeutet, dass der Trigger bis zu einer Minute abweichen kann.
mark_running_jobs_interruptedunterscheidet nicht, welcher PID getötet wurde — jeder zum Kill-Zeitpunkt laufende Job wird abgebrochen, was einen kurz vor Abschluss stehenden Job erwischen kann. - Nebenwirkungen im
unknown-Zustand: Hat die Crash-Recoveryunknownmarkiert, läuft nichts neu; das Skript hat vielleicht schon eine Nachricht gesendet oder Datei geschrieben. Downstream darf nicht «nicht abgeschlossen» annehmen. - Lease-Heartbeat verklemmt:
_run_job_script_with_claim_heartbeatverlängert das Lease über einen eigenen Thread; hängt dieser Thread selbst fest (z. B. langer GIL-Halt), kann ein anderer Scheduler-Prozess denselben One-Shot-Job ein zweites Mal auslösen — doppelte Nebenwirkung. - Prompt-Injection blinder Fleck: Der assembled Prompt (inklusive Skill-Inhalt) wird erst zur Laufzeit gescannt; ein bösartiger Skill wird erst zum Trigger-Zeitpunkt abgefangen, bei der Job-Erstellung ist er nicht sichtbar.
Zusammenfassung
Im Cron-Subsystem steht «Sicherheit bei Unbeaufsichtigtheit» im Vordergrund: Toolset-Isolation, lifecycle_guard gegen Fehlbedienung, Ausführungs-Zustandsautomat für Crash-Recovery. Das Scheduling selbst wird vom Gateway-Heartbeat getrieben und braucht keinen externen Cron-Daemon.