Wenn ein Symptom nach einem Bug aussieht, sind es fünf
Das erste echte End-to-End-Dogfooding der Nest-Pipeline zeigte exakt ein Verhalten: Notifications alle 15 Sekunden, troop leer, igor unsichtbar. Es waren fünf voneinander unabhängige Ursachen, jede mit einem anderen Symptom — nur die Verkettung erzeugte das, was ich sah. Und die tiefste war kein Bug, sondern ein Layer, das weg musste.
Beim ersten Mal, dass die ganze Nest-Pipeline end-to-end lief, sah ich genau drei Dinge: Mein Telegram bekam alle 15 Sekunden eine Approval-Notification, die troop-Liste auf der SP-Oberfläche war leer, und der Agent igor, den ich gerade gespawnt hatte, war nirgends sichtbar. Drei Beobachtungen, die sich anfühlten wie ein einziges kaputtes Ding. So liest man das ja: ein Symptom, ein Bug, eine Ursache, ein Fix.
Es waren fünf Ursachen. Jede für sich hätte ein anderes Symptom erzeugt — oder gar keins. Nur die Verkettung produzierte exakt das, was ich sah.
Wie es vorher war
Die Pipeline war über mehrere Phasen gewachsen, jedes Stück für sich getestet. Der IdP, der Agenten-Identitäten ausstellt. Die apes-CLI, die spawnt. Eine YOLO-Auto-Approval-Schicht, die bestimmte Kommandos ohne Prompt durchlässt. Ein Nest-Daemon, der die Bridges der Agenten startet und am Leben hält. Und darunter launchd-Plists, die Prozesse auf macOS verwalten.
Jedes Teil hatte seine eigenen Tests, jedes Teil war grün. Nur hatte sie noch nie jemand zusammen in einem echten Durchlauf gegen die Produktiv-Identität laufen lassen. An dem Tag das erste Mal.
Warum ein Symptom nicht eine Ursache ist
Ich fange beim auffälligsten Teil an: der Approval-Flood. Alle 15 Sekunden ein Prompt auf dem Handy für ein Kommando, das eigentlich YOLO-approved sein sollte. Mein erster Reflex war, die CLI-Logs zu lesen. Die sagten: pending. Also: Auto-Approval greift nicht, irgendwas mit dem Pattern.
Dann habe ich aufgehört, den Logs zu glauben, und den tatsächlichen Grant-Request im IdP inspiziert:
$ apes grant inspect <id>
status: approved
auto_approval_kind: yolo
Der Grant war approved. Mit auto_approval_kind: yolo. Die CLI sagte trotzdem pending und schickte mir einen Prompt. Das war der Moment, an dem die Annahme kippte: Das Symptom „Flood von Prompts" hatte gar nichts mit fehlender Approval zu tun.
Es war ein Dreifach-Bug. Das YOLO-Pattern matchte den apes run-Wrapper statt das innere Kommando — also wurde der Wrapper approved, das innere blieb pending. Der Supervisor rief apes run ohne --wait auf, sah keinen Erfolg, und startete neu. Und der Registry-Pfad war doppelt verschachtelt, weil homedir() im Daemon-Kontext das HOME des Daemons auflöste, nicht das des Agenten — deshalb war igor im Registry unsichtbar und troop leer. Jeder dieser drei hätte für sich allein „kein Prompt" produziert, jeweils ein anderer Failure-Modus. Erst das Zusammenspiel ergab „Flood von Prompts plus leere Liste".
Dazu kamen zwei, die mit dem sichtbaren Symptom gar nichts zu tun hatten und nur deshalb auffielen, weil ich sowieso schon im Maschinenraum stand: Die Owner-Attribution im IdP war rekursiv falsch, wenn ein Agent einen Agenten enrollt. Und beim setuid-Übergang über den escapes-Helper wurde der PATH nicht vererbt.
Fünf Drifts, alle gleichzeitig sichtbar geworden, weil nie zuvor jemand alle Schichten zusammen ausgeführt hatte. Das ist kein Pech. Das ist die Eigenschaft. Erstes echtes Dogfooding einer mehrschichtigen Pipeline findet die Drift jeder Schicht auf einmal — weil „getestet" pro Schicht etwas anderes heißt als „lief zusammen".
Der Fix, der kein Fix war
Bleibt der Supervisor. Der lief parallel zu den launchd-Plists, startete dieselben Bridges, die launchd auch startete, sah sie crashen, startete sie neu, launchd auch — Crashloop. Der naheliegende Fix wäre: Supervisor und launchd koordinieren, einer gewinnt.
Der richtige Fix war, den Supervisor zu löschen.
Process-Lifecycle auf macOS ist ein gelöstes Problem. launchd macht das seit Jahren, korrekt, mit Restart-Policy, mit System-Domain-Plists, mit allem. Der Nest-Supervisor war eine zweite Instanz von etwas, das das OS schon ist. Ich hatte ihn gebaut, weil ich beim Bauen nicht daran gedacht habe, dass die Verantwortung schon woanders liegt. Die billigste Lösung war nicht, die zwei Supervisoren zu versöhnen — sie war, dass es nur einen gibt.
Wie es jetzt aussieht
Der Nest-Daemon ist heute ein reiner Registry-Watcher. Er entscheidet, welche Agenten laufen sollen, schreibt das in eine Registry, und reconciled launchd-Plists in der System-Domain. Den Prozess-Lifecycle — Start, Restart nach Crash, Boot-Persistenz — besitzt launchd allein. Single Source of Truth für „läuft der Prozess".
Was weggefallen ist: der Supervisor. Und kurz danach der HTTP-Intent-Channel, über den der Nest früher mit den Bridges sprach — auch der war eine Schicht, die UNIX-Permissions auf einem intents/-Verzeichnis sauberer erledigen. Weniger Code heißt hier konkret: weniger Schichten, die zu einer Kette werden können, deren Symptom über die Anzahl ihrer Ursachen lügt.
Die Pointe
Ein Symptom ist keine Bug-Zählung. „Notifications alle 15 Sekunden, troop leer, igor unsichtbar" liest sich wie ein Defekt und war fünf, jeder mit seinem eigenen, anderen Symptom, die nur durch Verkettung zu dem einen kollabierten, das ich sah. Wer das Symptom nach Ursachen abklopft, zählt falsch.
Was hängen geblieben ist: Manchmal ist der Bug nicht in der Schicht. Manchmal ist die Schicht der Bug — und der ehrlichste Fix ist, sie zu löschen, nicht zu patchen. Den Supervisor habe ich nicht repariert. Ich habe ihn entfernt, weil launchd den Job längst tat.