Zum Inhalt springen
agenticbuilders

Zurück zur Übersicht

Sechs Stufen, bevor ein Agent schreibt

Ein Schreibpfad in ein Fremdsystem hat sechs Stufen. Der teuerste Fehler: das Wort Gate meint zwei kategorisch verschiedene Dinge.

Von Bernhard Götzendorfer

Veröffentlicht am Kategorie: Architektur

Jeder belastbare Schreibpfad in ein Fremdsystem läuft durch sechs Stufen: Evidence, Proposal, Gate, Apply, Audit, Verify. Ich lass die Namen englisch, weil sie im Code so heißen. Die meisten Integrationen haben fünf davon, und das fällt nie auf, solange ein Mensch beim Schreiben zusieht. Der teuerste Fehler liegt woanders: „Gate" bezeichnet zwei kategorisch verschiedene Dinge, und wer die zusammenwirft, baut ein Gate für ein CLI und schaltet dahinter einen Agenten scharf. Unten stehen die sechs Stufen, die Unterscheidung und eine Prüfliste mit 13 Punkten, gegen die du deinen eigenen Schreibpfad halten kannst.

Warum sechs Stufen und nicht fünf

Ich habe zwei Integrationen verglichen, die ich unabhängig voneinander gebaut habe. Die eine schreibt Arbeitszeiten in ein Zeiterfassungs-System, unbeaufsichtigt, per Cron ausgelöst. Die andere schreibt Datensätze in ein CRM, als persönliches Kommandozeilen-Werkzeug plus Server für ein Sprachmodell. Verschiedene Domänen, verschiedene Jahre, kein gemeinsamer Code.

Beide sind bei fast derselben Architektur gelandet. Dry-run als Default, genau eine Gate-Funktion, ein Audit-Log mit Vorher-Zustand, eine Allowlist der erlaubten Ziele. Diese Konvergenz allein ist noch kein Muster, sondern eine Beobachtung. Interessant ist, wo die beiden abweichen, denn jede Abweichung hat sich als begründet herausgestellt.

Meine Ausgangshypothese hatte fünf Stufen. Vier haben sich bestätigt, eine hat sich aufgeteilt, und eine fehlte komplett: Verify. Ohne diese Stufe weiß ein autonomer Lauf nicht, ob sein Write angekommen ist. Er weiß nur, was ihm das Zielsystem geantwortet hat, und das ist etwas anderes.

Die sechs Stufen

Evidence

Evidence sammelt maschinell, was die Grundlage der Aktion ist, getrennt vom Schreiben erhoben. Beim Zeiterfassungs-Pfad sind das Kalenderfenster und Repository-Aktivität, und die Antwort des Menschen zählt als eigene Evidenzklasse. Beim CRM-Pfad ist es der Live-Datensatz selbst, per GET geholt, bevor überhaupt etwas verglichen wird.

Die Regel dahinter ist unspektakulär: was du schreiben willst, leitest du aus etwas ab, das du vorher gelesen hast. Wenn die Evidenz erst beim Schreiben entsteht, kannst du sie nicht prüfen.

Proposal

Der geplante Effekt als inertes Datenobjekt. Ein Objekt, das per Default nichts tut, ist auch nach einem Refactoring inert. Ein Codepfad mit einem if davor ist das nur so lange, wie niemand das if verschiebt.

Hier liegen die beiden Systeme auf verschiedenen Niveaus, und der Unterschied ist größer, als er klingt. Der CRM-Plan lebt in einer Prozessausführung: das Kommando berechnet den Plan neu und wendet ihn direkt an, der Dry-run-Zweig wird dabei übersprungen. Das Dry-run ist damit eine Möglichkeit, kein erzwungener Vorschritt. Das Proposal im Zeiterfassungs-Pfad überlebt dagegen die Prozessgrenze: Change offen, Provenance-Record geschrieben, Anker eingefroren, und ohne offene Change-Id wirft das Gate.

Gate

Die Entscheidung, ob geschrieben werden darf. Davon gibt es zwei Arten, und sie beantworten verschiedene Fragen. An dieser Unterscheidung hängt der Rest, deshalb steht sie unten in einem eigenen Abschnitt.

Apply

Apply führt genau das geprüfte Proposal aus, ohne es neu zu erfinden. Klingt trivial, ist es nicht. Der Apply-Rand ist die letzte Stelle, an der du prüfen kannst, ob die Nutzlast nach der Prüfung noch dieselbe ist. Im Zeiterfassungs-Pfad wird dort der Content-Hash der geprüften Nutzlast erneut verglichen, als Sicherung gegen einen Aufrufer, der die Nutzlast zwischendurch austauscht.

Audit

Durable Aufzeichnung mit Vorher und Nachher. Das Vorher ist die Rollback-Grundlage und muss vor dem Write erhoben werden, weil es danach weg ist.

Ein Detail, das ich anfangs falsch hatte: ob ein fehlgeschlagener Audit-Write die Aktion verhindern muss, hängt an seiner Rolle. Ist er reine Nachschau, darf er einen bereits erfolgten Write nicht als Fehlschlag darstellen. Ist er die Steuergrundlage des nächsten Laufs, muss sein Fehlschlag die Aktion abbrechen. Beide Politiken sind richtig, aber nicht im selben System.

Verify

Verify liest das Ergebnis unabhängig zurück und vergleicht. Die Response des Zielsystems ist die Behauptung des Zielsystems, mehr nicht. Zwei Regeln, die ich mir teuer erarbeitet habe: ein fehlendes Rücklesen zählt als Mismatch, nicht als Erfolg. Und ein Mismatch führt nicht zu einem automatischen Retry, sondern lässt den Vorgang offen für einen Menschen. Ein Blind-Rewrite auf einen unklaren Zustand verdoppelt im Zweifel den Schaden.

Der CRM-Pfad hat diese Stufe nicht. Für interaktiven Gebrauch ist das in Ordnung, der Mensch sieht ja hin. Für jeden nicht-interaktiven Aufruf ist es die fehlende sechste Stufe.

Der wichtigste Befund: „Gate" heißt zwei verschiedene Dinge

Beide Systeme benutzen dasselbe Wort für zwei kategorisch verschiedene Prüfungen. Genau das ist die Stelle, an der die meisten Integrationen unsicher werden.

Autorisierungs-Gate

Es beantwortet: darf dieser Aufrufer diese Aktion? Das ist das Flag auf der Kommandozeile, der Rechte-Precheck gegen die Live-Berechtigung, die Allowlist der erlaubten Ziele. Wer diese Ebene sauber hat, hat ein gutes CLI.

Integritäts-Gate

Es beantwortet eine andere Frage: wird genau der geprüfte Inhalt geschrieben, und nur der? Dazu gehören ein Content-Hash-Recheck am Apply-Rand, ein rekursiv eingefrorener Payload, dessen Hash aus dem eingefrorenen Wert abgeleitet wird, zwei unabhängige Anker für Proposal und Zustimmung, und ein Intent-Vergleich, nachdem die Zustimmung beansprucht wurde.

Ein Autorisierungs-Gate sagt „du darfst schreiben". Es sagt nichts darüber, was geschrieben wird.

Wo der Unterschied in meinem eigenen Code sitzt

An einer Stelle in meinem eigenen Bestand wird das konkret. Der Apply-Handler für den Sprachmodell-Server nimmt einen vom Modell gelieferten Plan und macht daraus ein ausführbares Objekt. Es gibt keinen Plan-Hash, keine Plan-Id und keinen Vergleich gegen das, was das Planungs-Werkzeug tatsächlich ausgegeben hat. Die Validierung prüft die Form des Plans, nicht seine Herkunft. Der Zeiterfassungs-Pfad würde denselben Fall ablehnen, weil dort die Nutzlast gegen zwei eingefrorene Anker gehalten wird.

Für ein persönliches CLI ist das unkritisch. Der Mensch hat den Plan selbst berechnet und selbst das Flag gesetzt. Für jedes agentisch angesteuerte System ist es der teuerste Punkt der ganzen Liste, weil zwischen „Plan berechnen" und „Plan ausführen" ein Sprachmodell sitzt, das den Plan als Text durch seinen Kontext getragen hat.

Was das im Betrieb heißt: der Handler prüft, ob ein Feld den richtigen Typ hat. Ob es denselben Wert hat wie im geprüften Plan, prüft er nicht. Ein Feld, das sich zwischen Planung und Ausführung geändert hat, sieht für ihn aus wie ein Feld, das immer so aussah.

Der MCP-Fall sieht harmlos aus und ist es nicht

Ein Server für ein Sprachmodell liegt zwischen den beiden Fällen oben, und das ist die gefährlichste Lage: die Ausführung wirkt interaktiv, aber der Aufrufer ist ein Modell. Die Spezifikation des Model Context Protocol vom 2025-06-18 sagt dazu unter Tools:

„For trust & safety and security, there SHOULD always be a human in the loop with the ability to deny tool invocations."

Quelle: modelcontextprotocol.io, Spezifikation 2025-06-18, Abschnitt Tools, abgerufen am 2026-07-26. Im selben Dokument steht, dass Clients Tool-Annotationen als nicht vertrauenswürdig behandeln müssen, sofern sie nicht von einem vertrauenswürdigen Server stammen.

Das „SHOULD" ist dabei kein Höflichkeitswort. Die Spezifikation bindet ihre Schlüsselwörter ausdrücklich an BCP 14 und RFC 2119, und dort heißt SHOULD, dass es im Einzelfall gute Gründe geben kann, den Punkt zu ignorieren, die vollen Folgen aber verstanden und abgewogen sein müssen.

Das heißt: die menschliche Freigabe liegt beim Client, nicht in deinem Server. Deine Serverseite behält nur ihre Autorisierungs-Gates. Das Integritäts-Gate, das die Herkunft des Plans prüfen würde, fehlt. Der Weg ist spezifikationskonform und trotzdem der, auf dem die Bindung von Plan zu Apply offen bleibt. Für einen Server, der auch ohne interaktiven Client laufen könnte, ist die Delegation an den Client keine Absicherung.

Wann welche Stufe Pflicht wird

Die Trennlinie ist nicht die Größe des Systems. Sie ist die Frage, ob ein Mensch beim Write zusieht.

Für ein persönliches CLI reichen die Grundlagen aus der Liste unten, die ersten acht Punkte. Verify und Content-Bindung kosten hier Aufwand, den niemand einlöst.

Sobald ein Cron, ein Scheduler oder ein Agent den Write auslöst, kippt das. Bei mir hat ein Pfad dreizehn Läufe hintereinander abgelehnt, und gesehen hat es niemand, weil die Ablehnung nirgends beobachtbar war. Ohne Content-Bindung führt der Lauf aus, was ihm zuletzt gereicht wurde. Ohne Verify weiß er nicht, ob er es getan hat. Ohne serverseitige Re-Verifikation der Zustimmung reicht ein selbstgeschriebenes Flag als Freigabe. Und ohne Single-Use-Claim gilt eine einmalige Freigabe für immer. Was ein Agent über einen Chat-Kanal überhaupt auslösen darf, und wie Token-Auth und Least Privilege dafür aussehen, ist der Stoff im Kurs 24/7-Assistent.

Terraform hat für dieses Problem zwei Modi, und keiner ist gratis. Der gespeicherte Plan ist die Content-Bindung, aber er kostet die Rückfrage:

„When you pass a saved plan file to terraform apply, Terraform performs the operations in the saved plan without prompting you for confirmation."

Quelle: HashiCorp, Command: apply, abgerufen am 2026-07-26. Im Gegenstück zu plan steht der Preis der anderen Seite: Änderungen am Zielsystem in der Zwischenzeit können den Effekt eines gespeicherten Plans verändern, deshalb soll der endgültige Plan vor dem Apply erneut geprüft werden. Wer neu berechnet und die Bestätigung überspringt, hat beides aufgegeben: die Bindung an einen geprüften Plan und den Blick eines Menschen darauf. Wenn du an der Verify-Seite ansetzen willst: read-only zuerst, Autorisierung danach ist das Grundmuster aus dem Kurs Loop Engineering.

Ein Punkt noch zur Einmaligkeit einer Zustimmung. Echte Idempotenz nach dem Muster von Stripes Idempotency Keys ist eine serverseitige Fähigkeit: der Server erkennt den Retry am Key und gibt die gespeicherte erste Antwort zurück. Wenn dein Zielsystem das nicht anbietet, kannst du es nicht einfach benutzen. Was du bauen kannst, ist ein clientseitiger Ersatz: eine Zustimmung, die genau einmal einlösbar ist, plus eine Zustandsabfrage vor dem Write. Das schützt gegen doppelte Ausführung im eigenen Haus, nicht gegen einen Retry auf Transportebene.

Die 13 Punkte für dein System

Halte deinen eigenen Schreibpfad dagegen. Jeder Punkt ist eine Regel plus ihr Grund. Die ersten acht gelten, sobald überhaupt jemand schreibt. Die letzten fünf gelten erst, wenn niemand mehr zusieht.

Immer

  1. Das Proposal ist ein Datenobjekt mit fail-safe Default, kein Codepfad mit Bremse. Ein Objekt, das per Default nichts tut, bleibt auch nach einem Refactoring inert.
  2. Genau eine Gate-Funktion, mehrfach aufgerufen, niemals kopiert. Eine Kopie divergiert. Bei mir war der konkrete Bruch, dass eine in einem zweiten Modul neu deklarierte Fehlerklasse bei instanceof still false ergibt und die Exit-Code-Aufteilung zerlegt.
  3. Das Gate sitzt im Seam, nicht im Aufrufer. Der Aufrufer wechselt, der Seam bleibt.
  4. Strukturierter Diff über alle angefragten Felder, nicht nur die geänderten. „Keine Änderung" ist eine Information; ein Diff ohne unveränderte Felder ist nicht von einem Diff zu unterscheiden, der sie vergessen hat.
  5. Prüfe den Zielzustand vor dem Write, nicht nur die Berechtigung. Berechtigung sagt „du darfst", nicht „es ist noch offen" und nicht „es kollidiert nicht". Bei Mehrdeutigkeit ablehnen, nicht raten.
  6. Audit mit Vorher und Nachher, und der Compiler erzwingt die Vollständigkeit. Eine handgepflegte Liste der Operationen fällt hinter die Realität zurück. Die Fatalität des Audit-Writes muss zu seiner Rolle passen.
  7. Destruktive Operationen bekommen eine eigene, engere Allowlist. Schreibbar heißt nicht löschbar. Neue Entitäten sind Opt-out, und die zweite Bestätigung läuft vor dem ersten Netzwerkkontakt.
  8. Identität und Least Privilege über Profile statt geteilter Superrechte, plus Precheck gegen die Live-Berechtigung. Und dokumentiere die Grenzen des Prechecks, sonst wird er für eine Garantie gehalten.

Zusätzlich, sobald kein Mensch beim Write zusieht

  1. Binde den Apply kryptografisch an den geprüften Plan. Sonst sind „der Plan wurde geprüft" und „dieser Plan wird ausgeführt" zwei verschiedene Aussagen.
  2. Verify nach dem Write, unabhängig gelesen. Ein Mismatch bleibt offen, ohne Blind-Rewrite.
  3. Ablehnungen sind beobachtbar, nicht nur Rückgabewerte. Ein Pfad, der lautlos ablehnt, ist von einem Pfad, der nichts zu tun hatte, nicht zu unterscheiden.
  4. Autorisierung serverseitig gegen die Quelle re-verifizieren, mit Absender-Allowlist und Alterslimit. Bei mir sind es 72 Stunden, damit eine Freigabe vom Freitagabend nicht am Montag noch schreibt. Lokal geschriebene Zustimmung ist niemals Beweis: wer sie selbst schreiben kann, hat sie nicht bewiesen.
  5. Die Zustimmung wird beansprucht, nicht gelesen, also genau einmal einlösbar. Eine Zustimmung, die man beliebig oft vorzeigen kann, ist ein Dauerticket.

Punkt 11 hat bei mir einen echten Betriebsschaden als Beleg: 13 aufeinanderfolgende abgelehnte Läufe ohne ein einziges datierbares Artefakt. Von außen sah das exakt aus wie „es gab nichts zu tun". Die Antwort war ein Ablehnungs-Trace mit einer Bremse gegen Alarm-Müdigkeit, der nur schreibt, wenn sich der Ablehnungsgrund ändert.

Zeilennummern driften, Symbolnamen nicht

Die Notiz, aus der dieser Artikel stammt, enthält keine einzige Code-Zeilennummer. Jedes Code-Zitat nennt stattdessen den Symbolnamen und dahinter in Klammern ein lauffähiges Suchkommando, das genau diese Stelle findet. Das Kommando ist die Prüfung: wer es nachläuft, sieht sofort, ob die Aussage noch gilt.

Der Anlass war unangenehm. Eine adversariale Nachprüfung, also ein Agent mit dem ausdrücklichen Auftrag, meine Belege zu widerlegen, hat 21 Zeilenzitate meiner Analyse als falsch belegt, drei davon unabhängig am committeten Stand nachgeprüft und bestätigt. Der schwerste Fall zeigte auf ein return { kind: 'ok' } und behauptete daneben einen Ablehnungspfad, war also inhaltlich invertiert. Drei Ursachen: die Zeilennummern sind während der Arbeit zweimal gedriftet, ein Teil der Treffer stammte aus einem uncommitteten Working Tree, und meine Verifikation war ein Selbstbericht statt einer Prüfung. Ich hatte einen Sweep gebaut, der Zeileninhalte ausgibt, und dessen Ausgabe als geprüft gemeldet, ohne sie gegen meine eigenen Behauptungen zu lesen. Eine Verifikationsbehauptung, die nicht gedeckt ist, ist schädlicher als eine fehlende.

Nach der Umstellung ist der Beleg messbar. Ein späterer Commit hat den Code unter der Notiz bewegt, und der Wiederholungslauf über alle 95 Suchkommandos ergab 93 Treffer, 2 gewollte Null-Treffer und 0 Fehlschläge. Für das zweite Repository lief währenddessen der Stand von einem Commit auf den nächsten weiter, und git diff --stat über diesen Sprung zeigt genau eine geänderte Datei. Eine Zeilennummer hätte das möglicherweise nicht überlebt, ein Symbolname hat es nachweislich.

Praktisch sieht das so aus:

# statt: siehe Zeile 214
grep -n "export function assertWriteGatesOpen" src/write-gate.ts

# Aussage über ein FEHLEN: das Kommando muss 0 Treffer liefern
grep -n -iE "re-read|readback|verify" src/writes.ts

Die übertragbare Regel: eine Referenz auf Code gehört an ein Symbol, nicht an eine Koordinate, außer die Koordinate ist an einen Commit gepinnt. Ein Suchkommando verschiebt die Beweislast dorthin, wo sie hingehört. Der Leser kann es nachlaufen, statt der Notiz glauben zu müssen.

Was dieses Muster nicht ist

Es ist eine Architektur-Landkarte für einen Schreibpfad. Es ist keine Bauanleitung für ein laufendes System. Wer das Muster verstanden hat, hat es noch nicht gebaut, und zwischen beidem liegt die ganze operative Arbeit: Server, Container-Isolation, Token, Kanal-Anbindung, Betrieb. Das geht sich in einem Artikel nicht aus, und ich tu auch nicht so.

Was du nach diesem Artikel entscheiden kannst: welche der 13 Punkte dein Schreibpfad braucht. Die Antwort hängt an einer einzigen Frage, und die kannst du dir selbst beantworten, nämlich ob ein Mensch beim Write zusieht.

Passend dazu, was passiert, wenn ein Agent schreibt, der es nicht sollte: Zwei Sessions, ein Arbeitsverzeichnis. Und wenn du das Modell selbst lokal fahren willst: Lokales LLM für Claude Code.

Häufige Fragen

Reicht ein Dry-run-Default nicht aus?

Nein, wenn er optional ist. Prüf bei dir, ob der Dry-run ein erzwungener Vorschritt ist oder nur eine Möglichkeit. Wenn dein Kommando den Plan bei gesetztem Apply-Flag einfach neu berechnet und direkt ausführt, hast du Terraforms Automatic Plan Mode ohne die Bestätigung.

Brauche ich Verify wirklich, wenn die API einen Erfolg zurückmeldet?

Solange du zusiehst, nicht. Sobald ein Cron oder ein Agent auslöst, ist die Antwort des Zielsystems nur dessen Behauptung. Und ein ausbleibendes Rücklesen ist kein Erfolg, sondern ein Mismatch.

Und wenn ich den Server, den ich aufrufe, nicht selbst gebaut habe?

Dann gehören dir die Stufen zwei bis sechs trotzdem, weil du sie einem fremden System nicht hinzufügen kannst. Das Proposal, das Gate, die Bindung von Apply an den geprüften Plan, dein eigenes Audit-Log und das unabhängige Rücklesen liegen alle bei dir. Verify besonders: das ist ein zweites GET gegen eine API, die du ohnehin schon aufrufst.

Handout und neue Beiträge per E-Mail

Kein fixer Rhythmus, keine Zweitverwertung. Es kommt eine E-Mail, wenn es etwas Brauchbares zu berichten gibt.

Mehr dazu in der Datenschutzerklärung.