Skip to content

Cron-Zeitsteuerung

源码版本v2026.7.20

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

Datenfluss

  1. Das Gateway startet _start_cron_ticker:22335, der den Scheduler alle 60s triggert (tatsächlich nach cron/scheduler_provider.py::InProcessCronScheduler verschoben; das alte Symbol ist ein Shim).
  2. Der Scheduler liest den Job-Speicher (cron/jobs.py:106) und ermittelt nach Cron-Ausdrücken die fälligen Jobs.
  3. Jeder fällige Job:
  4. Crash während der Ausführung → beim nächsten Start stellt recover_interrupted_executions (cron/executions.py:156) die Marker wieder her.
  5. 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:

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 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_interrupted unterscheidet 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-Recovery unknown markiert, 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_heartbeat verlä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.

Inoffizielle Community-Lernseite. Basiert auf dem MIT-lizenzierten NousResearch/hermes-agent-Quellcode.