Es sagte success: true, und der Anhang ging nie raus: Hermes hat beide Enden der MEDIA-Zustellung geschlossen


Du bittest den Agenten, den gerade erzeugten Report an Telegram zu schicken. Er antwortet, er sei gesendet, Status success. Du leitest die Nachricht an einen Kollegen weiter, und fünf Minuten später kommt zurück: „welche Datei?“ Du scrollst hoch — die Nachricht ist reiner Text. Der Report ist nie aus der Sandbox herausgekommen, denn die Datei liegt in der Sandbox des Agenten, und „success“ bedeutete nur, dass der Agent selbst keine Exception geworfen hat.

Die andere Richtung ist das Spiegelbild und heimtückischer: Das Gateway erkennt Anhänge an einem MEDIA:-Marker im Text und kann einen Dateinamen nicht von einer Direktive unterscheiden. Jedes Tool, das einen vom Aufrufer gelieferten Pfad in seinen Ergebnis-Text zurückspiegelt, lässt sich also dazu bringen, eine zusätzliche Delivery-Direktive in die Ausgabe einzuschleusen.

Zwei am 20. September gemergte Änderungen (#116842, #116826) schließen beide Enden: laut sagen, wenn etwas verworfen wurde, und Direktiven ablehnen, die konstruiert statt geschrieben wurden.

1. success: true heißt nicht, dass der Anhang angekommen ist

Fangen wir mit der fehlenden Hälfte an. Vor dem Senden filtert das Gateway Anhangspfade — auf dem Host, wo eine Datei, die in der Sandbox existiert, überhaupt nicht vorhanden sein muss. filter_media_delivery_paths gab nur die Überlebenden zurück, und niemand verglich je „wie viele angefragt wurden“ mit „wie viele zugestellt wurden“. Die einzige Spur war eine Warnung auf der Host-Seite:

Skipping MEDIA directive path (not found on this host)

Die CLI und die Tool-Ebene sehen diese Zeile nie, also geben sie wie gewohnt Erfolg zurück.

PR #116842 lässt den Filter sammeln, was er verworfen hat ({path, reason}, wobei reason der String ist, den die Warnung schon berechnet hat: not found on this host oder denied by the delivery policy), und das Send-Tool trägt das ins Ergebnis:

Szenario Vorher Nachher
success in hermes send --json true false, mit partial_success: true
Feld media_dropped fehlt [{"path": "/data/report.html", "reason": "not found on this host"}]
CLI-Exit-Code 0 1
Kontrolle: reiner Text, kein Anhang success / Exit 0 success / Exit 0 (unverändert)

Zwei Dinge sollte man im Kopf behalten. Erstens: Dieses Signal existiert derzeit nur bei hermes send (und beim modellseitigen send_message-Tool) — andere Aufrufer (Cron, der Gateway-Turn, Weixin) übergeben keinen Collector, ihr Verhalten ist also unverändert, und ein Anhangsproblem bleibt dort weiterhin nur diese Log-Zeile. Zweitens: Da sich der Exit-Code jetzt ändert, prüfe den Exit-Code, statt Logs zu greppen:

if ! hermes send --to telegram --json "report attached MEDIA:/data/report.html"; then
  echo "attachment did not go out — inspect media_dropped" >&2
fi

2. Gefälschte Delivery-Direktiven werden von vornherein abgelehnt

Die andere Hälfte ist „zu viel gesendet“ — keine zusätzliche Nachricht, sondern zusätzliche Delivery-Direktiven. Der Matcher des Gateways sucht MEDIA:-Segmente im Text; er kann unmöglich wissen, ob dieser String aus einem Dateinamen oder aus dem Inhalt selbst stammt.

Nehmen wir text_to_speech. Sein output_path kommt vom Aufrufer, und der Pfad wird an drei Stellen zurückgespiegelt: im generierten Media-Tag, im Feld file_path des Ergebnisses und im Fehlertext zu Traversal beziehungsweise geschützten Pfaden. Setzt man Steuerzeichen oder ein verankertes MEDIA:-Segment in den Pfad, wird jede dieser drei Stellen zu einem Ausgang für Fälschungen.

Das gemessene Vorher/Nachher aus PR #116826 (echtes Tool, echter Collector):

Übergebener output_path Gefälschte Direktiven vorher Jetzt
a.mp3 + Zeilenumbruch + MEDIA:/tmp/evil.txt 4 (und es meldete Erfolg) 0, abgelehnt: Steuerzeichen
a.mp3 + U+2028 + MEDIA:/tmp/evil.txt 4 (automatisch angehängt, das Modell hat es nicht einmal gespiegelt) 0, abgelehnt
../MEDIA:/tmp/evil.txt 2 (Leck aus dem Traversal-Fehlertext) 0, abgelehnt: Media-Direktive
decoy/MEDIA:/tmp/evil.txt (sauberer Pfad) 4 (Leck aus dem file_path-Echo) 0, abgelehnt
Kontrolle: schlichtes ok.mp3 1 legitimer Tag 1 legitimer Tag (unverändert)

Die Implementierung besteht aus zwei Schritten. Eine neue Zeichenprüfung has_unsafe_path_chars lehnt C0-Steuerzeichen, DEL, NEL und U+2028 / U+2029 ab — dieselbe Klasse, die das gateway-interne _LOG_UNSAFE_CHARS absichert —, und _resolve_output_base lehnt sowohl diese Pfade als auch verankerte MEDIA:-Teilstrings zuerst ab, bevor irgendein Zweig laufen kann, der den Pfad spiegelt. Eine Grenze, die man kennen sollte: Ein bloßes media: ohne Pfad-Anker ist weiterhin ein legaler Dateiname und wird nicht erfasst.

Das Review fand noch ein Loch: Die zitierte Form (../MEDIA:"rel/evil.txt") rutschte anfangs durch den Test auf Verankerung, deshalb behandelt die Regex jetzt auch ein Anführungszeichen nach media: als Direktive.

3. Warum diese beiden zusammengehören

Das eine ist „zu wenig zugestellt und Erfolg gemeldet“; das andere ist „zu viel zugestellt und niemandem etwas gesagt“. Beide laufen über denselben Kanal: den MEDIA:-Marker im Text. Das Design ist bequem — das Modell schreibt einfach MEDIA:/path in seine Antwort und die Datei kommt mit —, bringt aber Inhalt und Steueranweisungen auf dieselbe Ebene, weshalb sich beide Enden selbst verteidigen müssen:

  • Der Produzent muss verifizieren. Jede Automatisierung, die einen Anhang als zugestellt betrachtet, sollte das strukturierte Ergebnis und den Exit-Code lesen, nicht ob eine Log-Zeile böse aussieht.
  • Der Consumer muss validieren. Jedes Tool, das einen von außen gelieferten Pfad in den Ergebnis-Text spiegelt, muss die Zeichenprüfung an der frühesten Stelle durchführen, statt zu hoffen, dass etwas weiter unten einen Dateinamen von einer Direktive unterscheiden kann.

Kann ich das heute nutzen?

Klare Antwort: noch kein Release. Beide Änderungen wurden am 20. September 2026 gemergt; der neueste Release ist v0.21.3 (Tag v2026.9.14), sie sind also nur auf main. Heute liefert ein verworfener Anhang weiterhin success: true.

Das Muster

Keine der beiden Änderungen ist groß — die eine macht aus einer Warnung, die niemand las, ein strukturiertes Fehlersignal, die andere verschiebt die Validierung an den frühestmöglichen Punkt —, aber beide beheben dasselbe: das System ehrlich darüber halten, was tatsächlich passiert ist. Ist der Anhang nicht rausgegangen, sollte der Exit-Code Fehler sagen; trägt ein Pfad Steuerzeichen, sollte er an der äußersten Kante der Loop gestoppt werden. Der praktischste Takeaway: Wenn du Anhänge aus einem Skript oder einem Cron-Job zustellst, verzweige nach dem Exit-Code, nicht nach dem „done“ im Satz.

Verwandte Lektüre: Media-Zustellung aus einer Sandbox erklärt, wie die Delivery-Policy mit Sandbox-Pfaden zusammenspielt, die Befehlsreferenz zu hermes send listet jeden Flag und jede Ausgabeform auf, und wenn du Approval- und Sicherheitsgrenzen justierst, siehe den Leitfaden zu unbeaufsichtigten Approvals.