# Verwaiste Agenten-Subprozesse können Zugangsdaten aktiv halten

Ein KI-Agent, dessen Zeitlimit abgelaufen ist, wurde nicht unbedingt beendet. Wenn sein Shell-Wrapper, Compiler, HTTP-Helfer oder SSH-Befehl vor Ablauf des Timeouts einen Kindprozess erzeugt hat, kann dieser nach dem Verschwinden der Eltern-PID weiterlaufen. Hat er eine verwendbare Zugangsdaten oder eine bereits authentifizierte Verbindung geerbt, wurde die Berechtigung, deren Widerruf du angenommen hast, durch das Timeout nicht beendet.

Ich habe Incident-Notizen gesehen, die mit «Der Agent wurde um 14:03 beendet» beginnen und mit einem API-Aufruf um 14:11 enden. Meist hat niemand einen raffinierten Exploit gefunden. Der Runner beendete einen Prozess, während die eigentliche Arbeit bereits in einen anderen verschoben worden war. Prozessbereinigung klingt nach bloßer Infrastruktur, bis ein autonomer Agent Produktionszugangsdaten verwenden kann. Dann gehört auch diese Infrastruktur zur Sicherheitsgrenze.

## Der Tod des Elternprozesses beendet die Arbeit nicht

Ein Kindprozess kann seinen Elternprozess überleben, weil der Kernel Prozesse unabhängig voneinander verwaltet und nicht als wegwerfbare Erweiterungen eines Shell-Befehls. Wenn ein Elternprozess endet, weist das Betriebssystem seine Kinder einem System-Reaper oder Supervisor zu. Das Kind behält seine eigene PID, seinen Speicher, seine Dateideskriptoren, Sockets, sein aktuelles Verzeichnis und oft auch seine Umgebung.

Dieses Verhalten ist legitim. Build-Systeme nutzen es. Terminal-Multiplexer nutzen es. Dienstmanager verlassen sich darauf. Der Fehler besteht darin, die PID des Elternprozesses als Ausführungsgrenze eines Agentenlaufs zu behandeln.

Betrachte eine vertraute Kette:

```text
agent-runner (PID 4102)
  shell tool wrapper (PID 4131)
    deployment script (PID 4140)
      ssh helper (PID 4144)
```

Der Runner erreicht seine Frist und sendet ein Signal an PID 4102. Wenn die Shell oder das Deployment-Skript nicht mit ihm beendet wurde, kann die übrige Kette weiterlaufen. Schwieriger wird es, wenn das Skript mit `&` einen Hintergrundjob startet, `nohup` verwendet, einen Dienstmanager aufruft oder einen entfernten Rechner anweist, Arbeit zu beginnen. Der Elternprozess kann sauber enden, während das Kind sich bereits unabhängig gemacht hat.

Deshalb ist ein verwaister Agenten-Subprozess mehr als ein übrig gebliebener CPU-Verbraucher. Er führt eine Entscheidung unbemerkt fort, von der ein Mensch oder Scheduler glaubte, sie sei beendet. Der Schaden hängt davon ab, worauf der Prozess noch zugreifen kann. Der Fehler beginnt jedoch früher: Der Runner hat die falsche Einheit zum Beenden ausgewählt.

POSIX beschreibt die Grundlagen dieses Verhaltens in seinen Schnittstellen für Prozesse und Jobsteuerung. Eine Prozessgruppe ist eine Gruppe verwandter Prozesse mit einer gemeinsamen Prozessgruppen-ID. Signale können an die Gruppe statt an ein einzelnes Mitglied gesendet werden. Eine Sitzung umfasst eine oder mehrere Prozessgruppen und steht normalerweise in einer Beziehung zu einem steuernden Terminal. Das sind unterschiedliche Kernel-Konzepte. Wer die Begriffe gleichsetzt, schreibt schwache Bereinigungslogik.

## Ein Timeout muss eine Isolierungseinheit beenden

Ein Timeout ist nur dann sicher, wenn es auf eine Isolierungseinheit zielt, die vor dem Start des Agenten eingerichtet wurde. Für einen lokalen Befehlsbaum ist das normalerweise eine eigene Prozessgruppe. Der Runner speichert die Gruppen-ID sofort und sendet bei Ablauf der Frist Signale an diese Gruppe.

Die Reihenfolge ist wichtig. Warte mit der Suche nach den Kindern nicht bis zur Bereinigung. Bis dahin kann der ursprüngliche Elternprozess verschwunden sein, die PPID-Beziehung bereits irreführend sein und ein weiterer Lauf begonnen haben.

Ein praktischer Timeout-Ablauf umfasst fünf Schritte:

1. Erstelle vor dem Start des Agenten eine eigene Gruppe oder einen von einem Supervisor verwalteten Job.
2. Speichere Lauf-ID, PID, PGID, Startzeit, Befehl und Frist in einem gemeinsamen Datensatz.
3. Markiere den Lauf bei einem Timeout als abgelaufen, bevor du ein Signal sendest, damit neue Aktionen nicht mit genehmigter Arbeit verwechselt werden.
4. Sende `TERM` an die gespeicherte Isolierungseinheit, warte eine kurze festgelegte Kulanzzeit und sende anschließend `KILL` nur an die verbleibenden Prozesse.
5. Sichere das Ergebnis und speichere die Signalzeitpunkte, den Exit-Status und die noch laufenden PIDs.

Der Datensatz kommt zuerst, weil die Bereinigung mit einem schnellen Prozessende konkurrieren kann. Wenn dein Protokoll nur «Agent beendet» sagt, kann es nicht beantworten, welchen Prozess du beendet hast, welche Kinder zu seiner Gruppe gehörten oder ob ein Kind vor dem Signal entkommen ist.

Prüfe auf macOS die Kennungen, statt einer Darstellung des Prozessbaums blind zu vertrauen. Dieser Befehl zeigt die Felder, die die meisten Timeout-Fehler erklären:

```sh
ps -axo pid,ppid,pgid,sid,lstart,etime,command
```

Ein sauber abgelaufener Lauf könnte vor der Bereinigung ungefähr so aussehen:

```text
  PID  PPID  PGID   SID  STARTED                  ELAPSED COMMAND
 4102  3988  4102  4102  Thu Jul 24 14:00:02 2026   00:31 agent-runner ...
 4131  4102  4102  4102  Thu Jul 24 14:00:02 2026   00:31 /bin/sh -c ...
 4144  4131  4102  4102  Thu Jul 24 14:00:04 2026   00:29 ssh ...
```

Die Werte dienen nur zur Veranschaulichung und dürfen nicht fest einprogrammiert werden. Entscheidend ist, dass der Lauf eine bekannte PGID hat, hier 4102. Die Bereinigung sendet das Signal an diese Gruppe und nicht nur an PID 4102. Ein negatives Ziel weist `kill` an, ein Signal an eine Prozessgruppe zu senden:

```sh
kill -TERM -4102
```

Prüfe die Syntax für die von dir verwendete Laufzeitumgebung und das Betriebssystem. Manche Wrapper verarbeiten negative Zahlen fehlerhaft oder behandeln ein zusätzliches Argument als Befehlsoption. Baue keine Sicherheitsgrenze auf einer Befehlszeile auf, die du nicht unter einem echten Timeout getestet hast.

Ein Gruppensignal hat trotzdem Grenzen. Ein Kind kann `setsid` aufrufen, eine neue Prozessgruppe erstellen, Arbeit an einen anderen Dienst übergeben oder einen Prozess auf einem entfernten Rechner starten. Die Disziplin der Prozessgruppe schließt den gewöhnlichen lokalen Ausweg, aber nicht jede mögliche Flucht. Dein Runner muss solche Übergänge als ausdrückliche Übergaben mit eigener Abbruch- und Audit-Logik behandeln.

## Prozessgruppen, Sitzungen und Zugangsdaten lösen unterschiedliche Probleme

Eine Prozessgruppe gibt dir ein lokales Signalziel. Eine Sitzung bietet eine umfassendere Grenze und kann die Jobsteuerung des Terminals isolieren. Keine von beiden widerruft Zugangsdaten, die ein Prozess in den Speicher kopiert oder auf die Festplatte geschrieben hat. Das sind getrennte Kontrollen, für die du getrennte Nachweise brauchst.

Diese Unterscheidung verschwimmt, weil der Fehler wie ein einziges Ereignis aussieht: Ein Agent überschreitet sein Zeitlimit und ein späterer Aufruf ist erfolgreich. Die Bereinigung fragt: «Welche lokalen Prozesse sollten beendet werden?» Der Umgang mit Zugangsdaten fragt: «Welcher Prozess kann diese externe Aktion noch autorisieren?» Das Audit fragt: «Können wir nachweisen, welche Aktion nach dem Ende der Freigabe stattgefunden hat?» Eine Prozessgruppe beantwortet nur die erste Frage.

Umgebungsvariablen sind die klassische schlechte Übergabe. Wenn ein Runner `API_TOKEN` exportiert, erhält jeder Nachkomme das Token, sofern ein späterer Prozess die Umgebung nicht bereinigt. Ein Kind kann das Token kopieren, an ein anderes Programm weitergeben oder eine authentifizierte Verbindung offen halten. Das Ersetzen einer Umgebungsvariable, nachdem der Elternprozess gestartet wurde, zieht die bereits geerbten Bytes nicht zurück.

Dateien und Sockets sind ebenso wichtig. Ein Prozess kann einen offenen Dateideskriptor erben, der auf eine Zugangsdaten-Datei, einen verbundenen HTTPS-Socket mit Sitzungsstatus oder einen SSH-Agent-Socket zeigt. Ein Hintergrundprozess kann den Deskriptor weiter verwenden, nachdem sein Elternprozess beendet wurde. Close-on-exec verhindert die versehentliche Vererbung bei einem neuen `exec`, hilft aber nicht, wenn das Kind den Deskriptor bereits besitzt oder das Programm ihn absichtlich weitergibt.

SSH erfordert besondere Vorsicht. Ein in einen lokalen Helfer geladener privater Schlüssel, ein SSH-Agent-Socket, eine multiplexte Steuerverbindung und ein entfernter Befehl haben vier unterschiedliche Lebenszyklen. Das Beenden des lokalen Elternprozesses kann eine bestehende Verbindung oder einen entfernten Befehl weiterlaufen lassen. Wenn der Agent beliebige SSH-Aufrufe ausführen kann, muss der Runner wissen, ob er den Aufruf eines lokalen Helfers, einen authentifizierten Kanal oder einen entfernten Job freigegeben hat. Alle drei einfach «SSH-Befehl» zu nennen, verschleiert die Grenze, die du kontrollieren musst.

Das sicherere Muster übergibt eine Aktionsanfrage statt des rohen Geheimnisses. Der Prozess bittet eine Autoritätskomponente, eine bestimmte HTTP-Anfrage oder SSH-Aktion auszuführen. Diese Komponente hält das Geheimnis, entscheidet, ob der aktuelle Lauf es verwenden darf, führt die Aktion aus und gibt das Ergebnis zurück. Ein entkommenes Kind kann Anfragen trotzdem wiederholen, solange sein Lauf autorisiert bleibt. Deshalb muss die Autoritätskomponente auch Ablauf oder Widerruf verstehen.

Sallyport folgt dieser Trennung für seine unterstützten HTTP- und SSH-Kanäle: Der Agent fragt über seinen MCP-Shim an, während die App das API- oder SSH-Material aufbewahrt und die Aktion ausführt, statt das Geheimnis in den Agentenprozess zu geben. Das ersetzt die Prozessbereinigung nicht, beseitigt aber die einfachste Form der Zugangsdatenvererbung.

## Prozesse im Hintergrund sind ein Ausweg, kein harmloses Shell-Detail

Der häufigste Fehler bei der Bereinigung beginnt mit einer Shell-Bequemlichkeit. Jemand schreibt `command &`, startet eine Pipe, verwendet `nohup` oder startet eine Laufzeitumgebung, die Worker erzeugt. Der Elternbefehl sieht beendet aus oder läuft in ein Timeout, während der Worker weitermacht.

Eine Shell-Pipeline verdient besondere Aufmerksamkeit. Ein Runner kann `/bin/sh -c 'generator | uploader'` ausführen und die Shell-PID speichern. Die Shell hat für beide Seiten der Pipeline eigene Kinder. Je nachdem, wie der Runner die Gruppe erstellt, kann das Beenden nur der Shell den Generator oder Uploader weiterlaufen lassen. Wenn der Uploader Zugangsdaten und eine Netzwerkverbindung besitzt, ist das genau der Prozess, den du beenden musst.

`nohup` ist schlimmer, als viele zugeben. Es verhindert, dass ein Prozess ein Hangup-Signal empfängt. Es macht einen Prozess weder vertrauenswürdig noch meldet es ihn deinem Runner. In der Automatisierung bedeutet es normalerweise, dass jemand möchte, dass Arbeit ein Terminal- oder Elternprozessende überlebt. Das kann für einen verwalteten Dienst sinnvoll sein, muss dann aber in einen Supervisor verschoben werden, der den Job besitzt und seinen Lebenszyklus erfasst. Es sollte nicht versehentlich innerhalb eines Agenten-Tool-Aufrufs sitzen.

Abgekoppelte Kinder erzeugen eine eigene Problemklasse. In Node.js gibt `spawn` mit `detached: true` dem Kind auf Unix-ähnlichen Systemen absichtlich eine neue Prozessgruppe und Sitzung. Das kann für eine Desktop-Anwendung sinnvoll sein, die ihren Starter überleben muss. In einem Agenten-Executor ist es eine schlechte Voreinstellung, weil es das normale Beenden der Gruppe durch den Executor verhindert.

```js
import { spawn } from "node:child_process";

const child = spawn("/bin/sh", ["-c", "sleep 600"], {
  detached: true,
  stdio: "ignore"
});
child.unref();
```

Dieses Snippet dient als Testfall für die Bereinigung und ist kein Ausführungsmuster, das du in einen Agenten-Runner übernehmen solltest. Es erzeugt ein Kind, das die normale Prozessgruppe des Elternprozesses nicht mehr teilt. Wenn ein Tool-Framework solchen Code ausführen kann, reicht die Bereinigung der Prozessgruppe allein nicht für eine vollständige Garantie. Beschränke abgekoppelte Ausführung, fange sie an der Tool-Grenze ab oder führe das Tool in einer stärkeren Betriebssystem-Isolierung aus.

Auch die beliebte Empfehlung, «einfach den Prozessbaum zu beenden», scheitert, wenn damit ein rekursiver PPID-Durchlauf gemeint ist. PPIDs beschreiben einen Zeitpunkt, keine dauerhafte Mitgliedschaftsgrenze. Eine Kindprozess kann zwischen deinem Durchlauf und dem Signal einen Fork ausführen. Ein Nachkomme kann den Baum verlassen. Speichere beim Start eine Gruppen- oder Jobidentität und mache jeden Austritt aus dieser Identität zu einem bewussten, auditierbaren Vorgang.

## Bereinige nicht nach Befehlsnamen

`pkill` und eine breite Namenssuche wirken attraktiv, weil sie kurz sind. Für parallele Agentenläufe passen sie jedoch schlecht. `pkill ssh` kann eine unabhängige interaktive Verbindung beenden. `pkill python` kann das lokale Tool eines Entwicklers entfernen. Eine Suche nach einer Argumentzeichenfolge kann einen Prozess übersehen, der seine Argumente geändert hat, oder einen falschen Treffer in einem anderen Lauf liefern.

Verwende einen Befehlsnamen nur als Hinweis für die Untersuchung. Für die Durchsetzung brauchst du eine Besitzgrenze.

Ein Runner, der eine Prozessgruppe kennt, kann zunächst ihre Mitglieder prüfen. Filtere auf macOS die Prozesstabelle mit der gespeicherten Nummer und nicht mit einem geratenen ausführbaren Namen:

```sh
ps -axo pid,ppid,pgid,sid,etime,command | awk '$3 == 4102 || NR == 1'
```

Die erste Spalte ist bei diesem Befehlsaufbau die PID, die zweite die PPID und die dritte die PGID. Vermeide in Produktionscode das Parsen menschenorientierter Textausgaben, wenn deine Sprache native Prozess-APIs aufrufen kann. Für ein Operator-Terminal liefert diese Ausgabe eine schnelle, verständliche Prüfung, bevor du ein Signal auswählst.

Sende anschließend das Signal an die Gruppe und prüfe erneut:

```sh
kill -TERM -4102
sleep 2
ps -axo pid,ppid,pgid,sid,etime,command | awk '$3 == 4102 || NR == 1'
```

Wenn Mitglieder bleiben, finde zuerst den Grund heraus, bevor du reflexartig `KILL` sendest. Ein Prozess, der in nicht unterbrechbarer Kernel-Arbeit blockiert ist, erfordert eine andere Untersuchung als ein Prozess, der `TERM` ignoriert hat. Ein Prozess mit einer anderen PGID hat dein Signal nicht zufällig überlebt, sondern die Grenze überschritten. Das ist ein Hinweis auf eine Designentscheidung, ein Framework-Verhalten oder ein bösartiges Tool.

Nach der festgelegten Kulanzzeit ist `KILL` für lokale Prozesse angemessen, die weiterhin zur gespeicherten Gruppe gehören und nicht fertig werden sollen. Versprich keine schonende Bereinigung, wenn der Agent sein Berechtigungsfenster überschritten hat. Kulanz dient dazu, Dateien zu schließen und den Status zu erfassen, nicht dazu, externe Arbeit unbegrenzt fortzusetzen.

Container und Dienstmanager können eine bessere Besitzstruktur bieten als eine rohe Prozessgruppe, wenn du die Ausführungsumgebung kontrollierst. MacOS-Desktop-Workflows für Agenten erhalten nicht automatisch Linux-cgroups. Übertrage cgroup-Empfehlungen deshalb nicht unverändert. Auf einem Mac sind eine eigene Kindprozessgruppe, enge Tool-Berechtigungen und ein ausdrücklicher Vertrag für entfernte Jobs oft die Grundlage. Wenn du Ressourcengrenzen oder eine vollständige Begrenzung aller Nachkommen brauchst, verwende eine Ausführungsumgebung, die diese Kontrollen tatsächlich bereitstellt.

## Das Audit muss den Elternprozess überleben

Du kannst eine entkommene Aktion nicht aus dem abschließenden Text des Agenten rekonstruieren. Der Elternprozess kann gestorben sein, bevor er Protokolle geleert hat, und ein weiterlaufendes Kind hat keinen Grund, Bericht zu erstatten. Baue das Audit auf unveränderlichen Ereignissen auf, die außerhalb des Agentenprozesses beobachtet werden.

Erfasse für jeden Lauf mindestens die folgenden Felder, bevor du externe Arbeit zulässt:

```json
{
  "run_id": "run-7f3c",
  "started_at": "2026-07-24T14:00:02Z",
  "deadline_at": "2026-07-24T14:00:32Z",
  "parent_pid": 4102,
  "process_group": 4102,
  "approval_identity": "signed agent process identity",
  "state": "active"
}
```

Der Aktionsdatensatz muss Lauf-ID, eine Aktionssequenz, den Zeitpunkt der Annahme durch das Gateway, den Kanal, die Zielidentität und das Ergebnis enthalten. Protokolliere standardmäßig niemals rohe Bearer-Werte, private Schlüssel, Autorisierungs-Header oder vollständige Request-Bodies. Ein Datensatz, der die Zugangsdaten bei der Dokumentation ihrer Verwendung offenlegt, erzeugt einen zweiten Incident.

Wenn das Timeout eintritt, füge vor dem Senden irgendeines Signals einen Zustandsübergang hinzu:

```json
{
  "run_id": "run-7f3c",
  "event": "deadline_expired",
  "observed_at": "2026-07-24T14:00:32Z",
  "process_group": 4102,
  "signal": "TERM"
}
```

Damit hat deine Untersuchung eine klare Frage mit einer klaren Antwort: Begann eine externe Aktion nach `deadline_expired`? Wenn das Gateway eine Aktion nach diesem Ereignis annimmt, hat es entweder den Laufstatus nicht durchgesetzt oder der Aufrufer trug nicht die Identität, die das Gateway erwartet hat. Begann eine Aktion vor Ablauf und endete danach, sage das ausdrücklich. Start und Abschluss sind unterschiedliche Zeitpunkte. Wer sie zusammenfasst, lässt normale laufende Arbeit wie eine Flucht aussehen.

Bewahre die Wanduhrzeit für Menschen und einen monotonen verstrichenen Wert zur Reihenfolge von Ereignissen innerhalb einer Maschine auf. Wanduhren können sich durch Synchronisierung oder manuelle Anpassung ändern. Während eines Incidents brauchst du keine Vorlesung über Zeitmessung, sondern genug Daten, um nicht zu behaupten, eine Aktion sei nach dem Tod des Elternprozesses passiert, wenn die Uhren nicht übereinstimmen.

Sallyports Sitzungs- und Aktivitätsjournale trennen die Ansicht des Laufs von einzelnen Aufrufen. Sein verschlüsseltes, hashverkettetes Protokoll kann mit `sp audit verify` offline geprüft werden. Das ist nach dem Verschwinden eines Elternprozesses nützlich, weil die Audit-Spur nicht davon abhängt, dass der Agent seine eigene Historie freiwillig liefert.

Eine Audit-Spur ist ein Nachweis, keine Begrenzung. Sie zeigt, dass ein Aufruf erfolgt ist, und unterstützt die spätere Prüfung. Sie beendet keinen lokalen verwaisten Prozess, holt bereits an ihn übermittelte Daten nicht zurück und bricht keinen entfernten Befehl ab, für den es keinen Abbruchweg gibt. Behandle das als unterschiedliche Verantwortlichkeiten und mache jede davon im Laufdatensatz sichtbar.

## Rekonstruiere den Fehler in der richtigen Reihenfolge

Wenn du einen mutmaßlichen Waisenprozess findest, sichere die Fakten, bevor du die Situation bereinigst. Ein hastiges `kill -9` kann gerechtfertigt sein, um schädliche Arbeit zu stoppen, doch es kann die Prozessbeziehungen löschen, die du zur Behebung des Runners brauchst. Erstelle zuerst einen Prozess-Snapshot, sofern die Lage das zulässt.

Beginne mit dem Datensatz des abgelaufenen Laufs. Notiere seine Eltern-PID, PGID oder Jobidentität, Startzeit, Frist und jedes gesendete Signal. Erstelle dann den aktuellen Prozess-Snapshot. Vergleiche auf macOS PID, PPID, PGID, SID, verstrichene Zeit und Befehl. Ein Waisenprozess hat oft eine PPID von 1 oder einem Supervisor, aber mache das nicht zu deinem einzigen Test. Ein Prozess kann weiterhin ein aktives Risiko sein, während seine PPID noch auf etwas anderes zeigt.

Ordne anschließend ein, was entkommen ist:

- Ein Prozess in der ursprünglichen PGID hat das erwartete Signal überlebt.
- Ein Prozess hat eine neue PGID oder SID und sich daher lokal abgekoppelt.
- Ein lokaler Prozess hat eine Anfrage zum Starten entfernter Arbeit gestellt, und der entfernte Job lief weiter.
- Eine Zugangsdaten oder ein authentifizierter Kanal blieb nach Ablauf des Laufs verwendbar.

Jede Klasse erfordert eine andere Reparatur. Die erste deutet auf Signalverarbeitung oder den Zeitpunkt der Bereinigung hin. Die zweite deutet auf einen nicht genehmigten Mechanismus zum Abkoppeln hin. Die dritte erfordert eine Remote-Job-ID und ein Abbruchprotokoll. Die vierte erfordert, dass die Aktionsautorität Anfragen an einen aktuellen Lauf bindet, statt darauf zu vertrauen, dass sich der Elternprozess korrekt verhält.

Gleiche danach die Aktionsdatensätze mit dem Zeitablauf ab. Beginne nicht mit den Agenten-Transkripten. Suche nach nach dem Ablauf angenommenen Anfragen, Zielen, die von der ursprünglichen Anweisung abweichen, wiederholten Aufrufen zur Fortsetzung eines Batches und Aufrufen, die der Elternprozess nicht melden konnte, weil er bereits beendet war. Das Transkript kann später die Absicht erklären. Es kann nicht beweisen, dass eine Netzwerkanfrage stattgefunden hat oder nicht.

Entscheide schließlich, ob die Zugangsdaten rotiert oder widerrufen werden müssen. Wenn kein rohes Geheimnis in den Agenten gelangt ist und das Aktions-Gateway Aufrufe nach Ablauf abgewiesen hat, kann sich das verbleibende Risiko auf bereits abgeschlossene Daten oder Änderungen beschränken. Wenn das Kind ein Bearer-Token, einen privaten Schlüssel, SSH-Agent-Zugriff oder einen verbundenen Administrationskanal geerbt hat, musst du davon ausgehen, dass es weiter handeln kann, bis du diese Berechtigung deaktiviert hast. An diesem Punkt verlieren Teams Zeit mit der Frage, ob der Prozess «wahrscheinlich» etwas getan hat. Wenn du nicht feststellen kannst, dass er den Zugriff verloren hat, entziehe den Zugriff.

## Ein Fehlertest muss den Waisenprozess sichtbar machen

Ein Timeout-Testsystem braucht einen Test, der laut fehlschlägt, wenn ein Kind überlebt. Nur zu prüfen, ob der Elternprozess mit einem Timeout zurückkehrt, beweist fast nichts.

Erstelle ein Fixture, das einen Eltern- und einen Kindprozess in derselben gespeicherten Gruppe startet. Das Kind sollte lange genug warten, damit das Testsystem den Elternprozess mit einem Timeout beendet. Der Elternprozess sollte vor seiner Blockierung die PID und Gruppe des Kindes melden. Prüfe nach dem Timeout, dass keiner der beiden Prozesse existiert und dass das Testaktionsprotokoll nach dem Ablaufereignis keinen angenommenen Aufruf enthält.

Dieses Shell-Fixture zeigt die Form des Problems:

```sh
#!/bin/sh
(
  trap 'exit 0' TERM INT
  sleep 600 &
  child=$!
  printf 'parent=%s child=%s pgid=' "$$" "$child"
  ps -o pgid= -p "$$" | tr -d ' '
  wait "$child"
)
```

Führe es mit demselben Startcode aus, den dein Agenten-Executor verwendet, und nicht aus einem interaktiven Terminal, das unbemerkt ein anderes Verhalten bei der Jobsteuerung liefert. Erfasse die ausgegebenen IDs, erzwinge die Frist und prüfe die Prozesstabelle. Wenn das Kind bleibt, hast du etwas Konkretes gelernt: Entweder hat dein Start keine Gruppe erstellt, wie du angenommen hast, oder dein Timeout-Signal hat sie nicht erreicht.

Wiederhole den Test für die Tools, die deine Agenten tatsächlich aufrufen. Shell-Wrapper, Node-Kindprozess-APIs, Python-Subprozess-Aufrufe, Paketmanager, Compiler und SSH-Helfer treffen unterschiedliche Entscheidungen zu Gruppen, Deskriptoren und Signalweiterleitung. Du brauchst keine Hunderte Tests. Du brauchst einen destruktiven Test für jedes Startmuster, das Nachkommen erzeugen kann.

Füge einen zweiten Fixture hinzu, das versucht, sich abzukoppeln. Der Test kann einen Aufruf zur Sitzungserstellung oder eine Laufzeitoption verwenden, die ein abgekoppeltes Kind erzeugt. Das erwartete Ergebnis sollte eine Ablehnung, die Übernahme durch einen Supervisor oder ein ausdrückliches Registrierungsevent sein. Ein stiller Erfolg ist ein Fehler, weil er dem Agenten zeigt, dass er Arbeit außerhalb der Timeout-Grenze verschieben kann.

Beschränke dich nicht auf lokale PIDs. Wenn dein Agent entfernte Arbeit starten kann, muss die entfernte Seite eine Jobkennung zurückgeben, und du musst den Abbruch nach dem lokalen Timeout testen. Der gewünschte Zustand lautet nicht «Der SSH-Client ist beendet». Der gewünschte Zustand lautet «Der von diesem Lauf identifizierte entfernte Job ist beendet, oder seine verbleibende Arbeit hat einen dokumentierten Verantwortlichen».

## Die Freigabe muss vor dem Prozess ablaufen

Die menschliche Freigabe eines Agentenlaufs bedeutet wenig, wenn ein Kind diese Freigabe nutzen kann, nachdem der Lauf für beendet erklärt wurde. Binde die Autorität für externe Aktionen an eine Laufidentität und einen Status, den die Autoritätskomponente bei jeder Anfrage prüft. Wenn der Runner den Lauf als abgelaufen markiert, müssen neue Aktionen dieses Laufs scheitern, selbst wenn ein lokaler Waisenprozess noch CPU-Zeit erhält.

Das bedeutet nicht, für jeden Shell-Befehl einen Menschen zu fragen. Es bedeutet, dass die Komponente, die Berechtigungen einsetzen kann, weiß, wann ein Lauf endete. Diese Komponente kann einem Lauf eine kurzlebige Kennung geben, sie nach dem Widerruf ablehnen und die Ablehnung protokollieren. Das Beenden der Prozessgruppe begrenzt dann den lokalen Schaden, während die Aktionsgrenze die Folgen eines Bereinigungsrennens begrenzt.

Behalte eine Freigabe pro Aktion für Vorgänge bei, die sie benötigen, aber verwechsle Freigabe nicht mit Überwachung. Ein Benutzer kann zu Beginn einer Sitzung einen signierten Agentenprozess freigeben. Wenn ein Nachkomme nach dem Ende des Agenten weiterläuft, hat er die genehmigte Ausführungsdauer überschritten, selbst wenn er denselben lokalen Kontext geerbt hat. Dein System muss diesen Zustand durchsetzen können, statt darauf zu hoffen, dass sich das Kind anständig verhält.

Der Runner sollte außerdem das Abbrechen eines Laufs vom Abbrechen eines externen Effekts unterscheiden. Eine abgebrochene Anfrage kann die API bereits erreicht haben. Ein beendeter SSH-Client kann bereits einen entfernten Befehl gestartet haben. Erfasse die Abbruchanfrage, das beobachtete lokale Ende und jede vom externen System zurückgegebene Bestätigung als getrennte Ereignisse. Diese Formulierung ist weniger beruhigend als «abgebrochen», zeigt den Operatoren aber, was sie tatsächlich wissen.

Die erste Reparatur, die ich an einem Agenten-Executor vornehmen würde, ist klein und unspektakulär: Erstelle und speichere beim Start eine eigene Prozessgruppe, beende diese Gruppe im Timeout-Pfad und prüfe das Ergebnis in einem Fehlertest. Kombiniere das mit einer Aktionsgrenze, die einen Lauf nach Ablauf ablehnt. Sobald diese beiden Fakten gelten, ist ein Waisenprozess ein Vorfall, den du eindämmen und erklären kannst, statt einer vagen Angst, dass möglicherweise noch etwas läuft.
