Zum Inhalt springen
covey

Mehrere Agenten arbeiten, und bei einem von ihnen ist gerade etwas schiefgegangen. Ohne einen Schalter dafür hieße das: jede laufende Sitzung suchen, den Container dazu finden, ihn abräumen, die angefangene Aufgabe von Hand wieder auf offen setzen. Dann noch den übersehen, der kurz darauf aus seinem Zeitplan heraus aufwacht.

Ausgedacht ist der Fall nicht: am 8. Mai 2026 stellte ein Nutzer auf Hacker News das Log seines Coding-Agenten ein, Item 48069567. Der Agent hatte rm -rf ./* im falschen Arbeitsverzeichnis ausgeführt und damit sämtliche lokalen Git-Repositories gelöscht. Über einen Timeshift-Schnappschuss ließ sich der Schaden zum größten Teil rückgängig machen, zum Preis von ein bis zwei Stunden. Im selben Log steht die Nachricht des Agenten an das Team: „I will stop all further actions immediately."

Das ist der gute Fall. Der Agent hat gemerkt, was er angerichtet hatte, und sich selbst angehalten. Knapp vier Monate früher hatte ein anderer Nutzer auf Hacker News nach dem schlechten gefragt, Item 46601809: ob IAM-Regeln, Freigabe-Workflows und Monitoring reichen oder ob da eine Lücke klafft. In der covey-Dokumentation steht dieselbe Frage als die, die in jeder Sicherheitsabnahme kommt: „Und wenn wir das alles stoppen müssen?"

Der Schalter am Agenten setzt eine Fahne und bricht die Sitzung ab

POST /api/v1/agents/{id}/kill arbeitet eine feste Reihenfolge ab: die Fahne killed des Agenten geht in der Datenbank auf wahr, der Daemon in der Sandbox bekommt eine Nachricht vom Typ kill mit dem Grund kill-switch, und die laufende Sitzung wird abgebrochen. Eine geparkte warme Sandbox wird sofort von evictWarm geräumt, denn ein gestoppter Agent soll keinen Container weiterlaufen lassen.

Der Schritt danach ist der, an dem Arbeit verloren gehen könnte. Ein UPDATE backlog_tasks SET state='open' holt jede Aufgabe des Agenten aus in_progress zurück in den Backlog, sodass der abgebrochene Lauf endet und die begonnene Aufgabe im Backlog stehen bleibt. Danach bekommt die Aufzeichnung einen Lebenszyklus-Eintrag mit dem Status killed.

Auf der Gegenseite der Verbindung geht es hart zu. Der Daemon bricht den laufenden Lauf ab und gibt ErrKilled zurück. coveyd schreibt daraufhin die Zeile „kill switch" mit dem Zusatz „daemon terminates immediately" ins Log und beendet sich mit Exit-Code 3, während der gewöhnliche Fehlerweg mit Exit-Code 1 endet.

Der zweite Schalter steht auf der Organisation

POST /api/v1/fleet/kill setzt zuerst eine einzige Spalte: UPDATE organizations SET fleet_killed=$2 WHERE id=$1. Danach ruft es für jeden Agenten der Organisation denselben Kill auf. Wer das darf, ist eng gefasst: org_admin oder security. Im Dashboard sieht nur diese Rolle den roten Knopf, und ob er schon gedrückt wurde, beantwortet GET /api/v1/fleet mit dem Feld fleet_killed. Schon diese eine Spalte hält die ganze Belegschaft an. Bevor ein Agent geweckt wird, liest die Control Plane erst seine eigene Fahne und dann die der Organisation; steht eine von beiden, kehrt sie zurück, ohne eine Sandbox zu starten.

Dasselbe Paar steht in der Abfrage, mit der der Zeitplan seine fälligen Heartbeats einsammelt: WHERE NOT a.killed AND NOT org.fleet_killed. Wer einen Heartbeat stattdessen in der Oberfläche von Hand auslöst, bekommt 409 zurück, mit dem Satz „Agent or fleet is stopped" und der Aufforderung „resume it first." Ein Trigger von außen über POST /api/trigger/{token} prallt ebenfalls ab, dort allerdings an der eigenen Fahne des Agenten, die KillFleet jedem einzeln gesetzt hat.

Dass eine neue Aufgabe den gestoppten Agenten nicht mehr weckt, prüft TestNotausDerFlotte in internal/integration/notaus_test.go. Während der Notaus steht, wird eine Aufgabe angelegt; nach zwei Sekunden liegt sie unverändert im Zustand open. Der Kommentar über dem Test nennt den Grund, aus dem er geschrieben wurde: der flottenweite Notaus hatte vorher keinen einzigen, „of all things the lever you need exactly once and that has to work then".

Lösen muss so weit reichen wie Stoppen

An dieser Stelle lag ein Fehler, und er steht heute als Kommentar über ResumeFleet in internal/orchestrator/orchestrator.go. Das Zurücknehmen löste anfangs nur die Fahne der Organisation. KillFleet hatte aber jeden Agenten einzeln gestoppt, und dessen Fahne blieb stehen. Die Oberfläche zeigte danach keinen Notaus mehr an, und die Belegschaft stand trotzdem still.

Heute räumt ResumeFleet beides ab: die Fahne der Organisation, danach die jedes einzelnen Agenten. Wer offene Arbeit hat, wird dabei sofort geweckt statt beim nächsten Tick.

In die andere Richtung gilt etwas anderes. Einen einzelnen Agenten freizugeben, während der Flottenschalter steht, setzt zwar seine eigene Fahne zurück; die Prüfung der Organisationsfahne steht danach immer noch davor.

Was der Notaus kostet

Der Preis steht auf der Doku-Seite zu den Guard-Rails im selben Satz wie das Versprechen: beide Schalter wirken sofort, und laufende Sandboxen werden beendet. Was im Home der Sandbox liegt, ist damit der Stand des letzten Syncs. Der läuft laut spec/16-runner.md beim echten Einschlafen, und eine geparkte warme Sandbox wird im Hintergrund gesichert, höchstens alle fünf Minuten.

Grob ist der Flottenschalter außerdem, und zwar mit Absicht; das ist der zweite Preis. KillFleet holt sich die Agenten mit SELECT id FROM agents WHERE org_id=$1, also jeden, der zur Organisation gehört.

Der Verdacht, gegen den das Werkzeug gebaut ist, ist ein systemischer: eine Injektion etwa, die mehr als einen Agenten getroffen hat. Wer einen einzelnen im Verdacht hat, nimmt den Schalter am Agenten.

Der dritte Punkt betrifft, wer davon erfährt. Der Flottenschalter löst eine Benachrichtigung der Klasse cost und der Art fleet_killed aus, „The emergency stop was pulled: every agent of this organisation is paused", verlinkt auf /administration. Adressaten sind nach der Spezifikation der Org-Admin und das Controlling.

Der Agent aus dem Log vom 8. Mai hat sich selbst angehalten und danach um Hilfe gebeten. Ein Schalter außerhalb der Runtime beantwortet den anderen Fall: den, in dem die Meldung ausbleibt.

Zurück zur Übersicht