Kostenlos testen
Handbuch

writeTenantConfig — Content-Modi (content / source_file / patch)

Drei Wege, den Datei-Inhalt an writeTenantConfig zu uebergeben. Wann welcher Modus? Faustregeln, Token-Kosten, Truncation-Bugs.

writeTenantConfig — drei Modi

writeTenantConfig ist das Werkzeug, mit dem der Builder-Bot (und andere Meta-Assistants) Config-Dateien schreibt. Es akzeptiert den Datei-Inhalt auf drei verschiedenen Wegen. Der falsche Modus verbrennt Tokens oder produziert stumme Daten-Verluste — daher die Faustregeln unten.

Die drei Modi im Ueberblick

Modus Wann Token-Kosten Truncation-Risiko
content Neue Files, kleine Aenderungen (<10 KB) Full-File-Groesse Hoch bei >10 KB
source_file Full-Rewrite grosser Files (>10 KB) Nur der Filename Kein Risiko
patch Punktuelle Aenderungen an bestehendem File Nur die geaenderten Zeilen Kein Risiko

Genau EINEN der drei Wege nutzen — patch und content zusammen sind ein Fehler.

Modus 1: content — Voller Datei-Inhalt

{
  "path": "assistants/my_bot.php",
  "content": "<?php\nreturn [...]"
}

Der klassische Weg. Der komplette Datei-Inhalt geht als String durch tool_call.arguments.

Faustregel: unter 10 KB ok. Ueber 10 KB fangen LLM-Provider (Anthropic vor allem) an, grosse String-Args auf Stubs zu reduzieren — die Datei landet unvollstaendig auf Platte. Wurzel des 2026-08-03 bloomify/apis.php-Bugs (162 → 66 Zeilen still verschwunden).

Der Shrink-Guard im Tool blockt >30% Verkleinerung + >20 Zeilen Drop, aber nicht jede Truncation triggert den Guard.

Modus 2: source_file — Aus dem Sandbox

{
  "path": "assistants/my_bot.php",
  "source_file": "my_bot_new.php"
}

Der Runner liest die Datei aus dem chat-lokalen runPython-Sandbox-Ordner. source_file ist nur der Basename (kein Pfad) — die Datei muss vorher via runPython in die Sandbox geschrieben worden sein.

# runPython
with open('my_bot_new.php', 'w') as f:
    f.write(neuer_inhalt)

Umgeht LLM-Truncation komplett, weil das grosse Text-Blob nie durch die Tool-Args-Serialisierung geht. Empfohlen bei allen Full-Rewrites uber 10 KB.

Erlaubt sind auch: source_path, file_path, filepath (Aliasse, alle mit derselben Basename-Semantik).

Modus 3: patch — Unified-Diff

{
  "path": "assistants/my_bot.php",
  "patch": "@@ -42,3 +42,3 @@\n context line\n-old line\n+new line\n context line"
}

Ein Unified-Diff wird gegen die aktuelle Datei angewendet. Format:

  • @@ -N,M +N,M @@ — Hunk-Header (nur die alte Startzeile wird gelesen, die Laengen werden ignoriert; Kontext-Match entscheidet)
  • (Leerzeichen-Prefix) — Kontextzeile, muss exakt matchen
  • - — Zeile aus Original entfernen
  • + — Zeile neu einfuegen

Bevorzugt fuer punktuelle Aenderungen — Token-Ersparnis vs Full-Rewrite ist bei einer 100-Zeilen-Aenderung an einem 2000-Zeilen-File ~20x.

Strikter Kontext-Match, tolerante Position

Der Applier ist beim Inhalt strict:

  • Kontext- und --Zeilen muessen exakt matchen (auch Whitespace, auch Anfuehrungszeichen).
  • Kein Inhalts-Fuzz, keine Whitespace-Toleranz.
  • Bei Mismatch: Fehler mit Zeilennummer und der erwarteten/gefundenen Zeile.

Bei der Position ist er tolerant wie patch (seit 2026-09-25): stimmt die Startzeile im Hunk-Header nicht (LLMs verzaehlen sich oft um ein paar Zeilen), sucht er dieselbe Zeilenfolge (Kontext + --Zeilen) im Umkreis von ±100 Zeilen, nie vor dem Ende des vorigen Hunks. Uebernommen wird nur ein eindeutiger Treffer; bei mehreren Treffern Fehler „stehen mehrfach im Umkreis … mehr Kontext mitgeben“. Verschobene Hunks stehen im Ergebnis unter patch_offsets (Hunk-Nr => Zeilen). Ein abschliessender Zeilenumbruch des Patches ist keine Kontext-Leerzeile.

Grund: still falsche Dateien schreiben ist der schlimmste Failure-Mode. Ein klarer Fehler mit Zeilennummer erlaubt dem Bot, die Datei neu zu lesen und den Diff frisch zu bauen.

Voraussetzungen

  • Datei muss existieren. Fuer neue Files → content oder source_file.
  • Nur @@-Hunk-Header + Body-Zeilen. diff --git/---/+++-Header werden still ueberlesen.

Beispiel: Regel in Prompt-File aendern

@@ -288,3 +288,4 @@
 ## `readTenantConfig` liefert nur ~2 KB pro File
+
 Wenn du den vollen Code eines Handlers brauchst und die Preview abgeschnitten ist:

Diff = 4 Zeilen. Ohne patch-Modus muesste der Bot die ganze Prompt-Datei (1200+ Zeilen) neu schreiben.

Downstream-Pipeline bleibt gleich

Egal welcher Modus — nach dem Content-Assembly greifen dieselben Guards:

  1. Bei .php: php -l-Syntaxcheck (Fehler = abort)
  2. ToolCodeAudit::analyze + analyzeSchema (verbotene Funktionen, OpenAI-Style-Schema)
  3. Exposure-Guard: Literal-Verstoesse (=> !0, berechnete Schluessel …) blockieren hart. Neu gesetzte Flags (public_api & Co., allow_write unter remote_databases) blockieren nicht separat, sondern stehen als Tragweite oben in der Gate-Vorschau (Felder exposure, tragweite); dafuer zaehlt nur das Ja auf genau diese Rueckfrage — keine direkte Anweisung, keine Auto-Freigabe. EINE Bestaetigung statt zwei; confirm_exposure ist nicht mehr noetig.
  4. Shrink-Guard (>30% + >20 Zeilen = abort, Bypass via force=true)
  5. Freigabe-Gate (siehe unten) — ohne Freigabe wird nichts geschrieben
  6. Backup nach _backups/<file>.<ts>.bak
  7. Write + opcache_invalidate
  8. In-process reloadTenantConfig (Tool ist im gleichen Turn nutzbar)

Freigabe-Gate (seit 2026-09-25)

writeTenantConfig, deleteTenantConfig und — im Builder — runTool mit writeFile/deleteFile/renameFile schreiben erst nach einer Freigabe im Chat. Durchgesetzt im Server (App\Tooling\ApprovalGate, fail-closed), nicht nur per Prompt. Muster wie contaoSetPublished.

Ablauf:

  1. Erster Aufruf → nichts wird geschrieben. Antwort {success:false, needs_confirmation:true, error:"freigabe_fehlt", preview:"<Diff>", suggestions:["Ja, speichern","Nein, verwerfen"]}. Das Gate merkt sich den Vorschlag (Pfad + Fingerprint des exakten Inhalts + Anzahl Nutzer-Nachrichten) und setzt die Buttons selbst.
  2. Der Bot zeigt die Änderung kurz und wartet.
  3. Nächste Nutzer-Nachricht ist ein klares, kurzes Ja → derselbe Aufruf mit identischem Inhalt und confirm: true schreibt. Bei runTool steht confirm am runTool-Aufruf.

Was als Ja gilt (App\Tooling\UserApproval::isClearYes; derselbe Mechanismus steckt hinter confirmChatAction — contaoSetPublished, restoreDbBackup, deleteDbBackup, rollbackWrite): Button-Text oder eine kurze Nachricht, die mit einem Ja-Wort beginnt („ja“, „ok“, „passt“, „mach das“, „sieht gut aus“, 👍 …), höchstens ein paar Wörter danach, keine Frage, keine Einschränkung („aber“, „noch“, „anders“, „nicht“, „erst“, „auch“ …). Ein 🎤-Präfix (WhatsApp-Sprachnachricht) wird ignoriert.

Was blockiert (fail-closed):

  • Keine neue Nutzer-Nachricht seit dem Vorschlag — das Modell kann sich nicht selbst bestätigen.
  • Das Ja ist nicht die unmittelbar nächste Nachricht (dazwischen „nein, anders“ → alter Vorschlag verfällt).
  • Inhalt anders als beim Vorschlag (neuer Fingerprint = neuer Vorschlag).
  • Vorschlag älter als 24 h.
  • Kein Chat (Scheduled Task, CLI, chat_id leer) → freigabe_nicht_moeglich.
  • getChatMessages schlägt fehl oder State nicht lesbar → freigabe_pruefung_fehlgeschlagen bzw. freigabe_nicht_moeglich.

Ausnahmen:

Beide Ausnahmen gelten nur für reine Wert-Änderungen (ApprovalGate::classifyChange → change: "values"): bestehende PHP-Datei, Token-Folge bis auf String-/Zahl-Literale unverändert (Whitespace/Kommentare egal), kein geändertes Literal ist ein Array-Schlüssel, steht in einer Closure/Arrow-Function oder unter handler. Neue Tokens (function, fn, $ctx, =>, use …), geänderte Bezeichner/Flags (true→false), Nicht-PHP-Dateien, neue Dateien und Exposure-Flags sind code bzw. new → immer Vorschlag + Ja. Grund: Text aus gelesenen Inhalten (Mails, Webseiten, Tool-Results) darf nie über einen „Auftrag“ zu Code werden (Prompt-Injection). Das Tool-Result nennt change; bei Rückfrage erklärt message, warum Direktauftrag/Auto-Freigabe nicht gegriffen haben.

  • Direkte Anweisung (UserApproval::isDirectOrder): die letzte Nutzer-Nachricht ist selbst eine eindeutige Anweisung in einem Satz („ändere beim supportBot das Icon in 🤖“, „setz im Kundenservice die Temperatur auf 0.2“), ohne Frage/Hedging/Verneinung, und nennt die Datei, das Tool oder den Assistant (Basename ohne Endung oder ein in der Datei definierter tools.<name>/assistants.<name>-Schlüssel, mind. 3 Zeichen). Gilt nur für eine kleine Wert-Änderung (≤ 12 geänderte Zeilen) an einer bestehenden Datei und einmal pro Nachricht. Löschen per Anweisung nur, wenn die Nachricht die Datei benennt („lösch das Tool altesTool“).
  • Auto-Freigabe (setAutoConfirm({on:true})): nur wenn die letzte Nutzer-Nachricht das ausdrücklich verlangt („frag nicht mehr“, „ohne Rückfrage“) — das Tool prüft das selbst. Gilt 12 h für diesen Chat und nur für Wert-Änderungen per writeTenantConfig — nie für deleteTenantConfig, Dateitools, neuen Code/Handler, neue Dateien, Exposure-Flags oder Connector-Tools. Ergebnis mit Restlaufzeit (auto_confirm_remaining, z. B. „11 h 42 min“). „frag wieder“ bzw. on:false schaltet zurück.

Manipulationsschutz: Der Vorschlags-State liegt im ToolState-Namespace _approval_gate; Namespaces mit führendem _ kann stateSet/stateDelete/stateClear nicht beschreiben.

Tests: tests/TestCase/Tooling/UserApprovalTest.php (Beispielsätze), ApprovalGateTest.php, BuilderApprovalFlowTest.php (echter ToolRunner).

Maskierte Zugangsdaten (seit 2026-10-04)

readTenantConfig, readSharedTool, describeTool, readLogs und writeTenantConfig tragen mask_secrets => true. Der ToolRunner schickt ihr Ergebnis durch App\Tooling\ConfigSecretMasker::maskResult(), bevor es ans Sprachmodell, in chat_messages und in task_executions geht. Anlass: Betriebsanalyse 04.10. #1, apis.php mit Trello-Key/-Token im Klartext.

  • Maskiert wird nur der Wert, die Struktur bleibt: 'token' => '***masked(len=64)***'. Betroffen sind Werte unter Secret-Namen (token, secret, password, api_key, client_secret, private_key, signing …; key/*_key ab 12 Zeichen), Werte, die wie Schlüssel aussehen (sk-, xox, EAA…, JWT, lange Hex-/Base64-Strings), key=/token= in URLs und Credentials nach Bearer/Token. In .env ist jeder Wert maskiert.
  • {{secret:…}}-Platzhalter, Bezeichner (meta_long_lived_token), Zahlen und ID-Felder (spreadsheetId, folder_id) bleiben lesbar.
  • Zurückschreiben: Enthält content, source_file oder patch Masken, setzt writeTenantConfig die echten Werte aus der aktuellen Datei wieder ein. Ein patch mit maskierten Kontextzeilen wird dafür gegen die maskierte Fassung angewendet. Jede Zeile mit Maske muss unverändert sein. Sonst kommt error: masked_secret und es wird nichts geschrieben. Das gilt auch für neue Dateien mit Maske.
  • Die Diff-Vorschau in der Freigabe-Rückfrage wird aus den maskierten Fassungen gebaut.
  • Neue oder geänderte Zugangsdaten gehören nicht in writeTenantConfig. Der Builder trägt {{secret:NAME}} ein, und den Wert setzt der Nutzer im Secrets-Bereich.

Tests: tests/TestCase/Tooling/ConfigSecretMaskerTest.php, BuilderApprovalFlowTest::testReadMasksSecretsAndWriteKeepsThem.

Faustregeln fuer den Builder-Bot

  • Neue Datei → content (klein) oder source_file (gross)
  • Existierende Datei, weniger als 10 Zeilen aendern → patch
  • Existierende Datei, mehr als 30% neu → source_file (Full-Rewrite via Sandbox)
  • Existierende Datei, unklar → readTenantConfig erst, dann entscheiden

Verwandt

Zuletzt aktualisiert: 2026-09-25