Un appel TTS échoué a supprimé le fichier d'un utilisateur : au cœur du correctif P0 du chemin de sortie d'Hermes

Vous demandez à Hermes de transformer le compte rendu d’une réunion en audio, et vous ajoutez une ligne au prompt : enregistre-le sous meeting-notes.mp3. Ce nom de fichier se trouve être l’enregistrement d’un appel client que vous avez passé ce matin. Quelques secondes plus tard, l’outil revient avec une erreur — le provider TTS était indisponible. Vous ouvrez le fichier. Il n’est pas corrompu. Il n’est pas vide. Il a disparu.
C’était un vrai P0 dans Hermes : un appel text-to-speech échoué supprimait le fichier existant vers lequel pointait output_path. Le bug a été corrigé le 9 octobre (PR #135531 — cinq lignes de code plus un fichier de test). Voici le mécanisme, le correctif, et ce que cela implique pour votre façon d’utiliser l’outil.
Pourquoi un échec pouvait emporter votre fichier avec lui
Commençons par la façon dont l’outil text_to_speech nettoie derrière lui.
Il prend en charge la synthèse de textes longs en découpant le texte en chunks, si bien que chaque appel tient un ensemble appelé generated_artifacts : chaque chemin touché par cet appel y est consigné. Une fois l’appel terminé — succès ou échec — un bloc finally parcourt l’ensemble et _remove_quietly() tout ce qui n’a pas été réellement livré. Cette conception est parfaitement raisonnable. Si le chunk 3 sur 5 échoue, les fichiers .chunk001.mp3 à moitié terminés ne doivent pas rester sur le disque.
Le problème se situait à l’étape d’enregistrement. Avant le correctif, le code ressemblait à ceci :
chunk_path = base_path # le chemin vers lequel output_path pointait
if len(chunks) > 1:
chunk_path = base_path.with_name(f"{base_path.stem}.chunk{index:03d}{base_path.suffix}")
generated_artifacts.add(str(chunk_path)) # ← le fichier de l'utilisateur était enregistré comme « sortie temporaire »
output_path est un argument d’outil que le modèle peut choisir. Quand il pointait vers un fichier qui existait déjà, l’ancien code enregistrait ce fichier comme quelque chose que cet appel avait produit. À partir de là :
- si la synthèse réussit → l’étape de livraison fait un
os.replace()du nouvel audio par-dessus la cible (l’écrasement attendu) ; - si la synthèse échoue → le balayage du
finallyrevendique le chemin et le supprime.
C’est dans la branche d’échec que vivait le P0 : le fichier de l’utilisateur n’avait jamais pris part à la synthèse, mais la logique de nettoyage le comptait comme une sortie résiduelle. Le bug a été mis au jour lors d’une revue du catalogue du plugin wyoming-voice-bridge.
Le correctif : écrire à côté, emménager seulement en cas de succès
La nouvelle fonction _synthesize_chunks vérifie l’existence d’un fichier avant d’écrire :
if os.path.lexists(chunk_path):
# Un fichier que nous n'avons pas créé : synthétiser à côté pour qu'une tentative échouée
# ne puisse ni le tronquer ni le balayer ; en cas de succès, l'étape de livraison fait un
# os.replace() de l'audio par-dessus (écrasement).
chunk_path = chunk_path.with_name(f".{chunk_path.stem}.{uuid.uuid4().hex}{chunk_path.suffix}")
Quand la cible existe déjà, la synthèse se fait vers un frère caché — .meeting-notes.9f3c….mp3. Les deux issues sont désormais propres :
- Échec : le balayage ne supprime que le frère. Votre fichier n’est jamais ouvert, tronqué ni supprimé.
- Succès : l’étape de livraison fait toujours un
os.replace()de l’audio sur la cible, donc le contrat d’écrasement reste inchangé.
Les nouveaux chemins — y compris le chemin de cache horodaté par défaut — empruntent exactement le même chemin de code qu’avant, et la sortie partielle d’une tentative échouée est toujours nettoyée.
Bon à savoir : l’outil disposait déjà d’un ensemble de filtres sur les chemins, mais ils gardent l’endroit où vous pouvez écrire, pas ce que vous pouvez écraser :
- une composante de traversée
..est rejetée (les chemins absolus sont acceptés ; les chemins relatifs ne peuvent pas remonter) ; - les caractères de contrôle ou les séparateurs de ligne dans le chemin sont rejetés ;
- une directive
MEDIA:à l’intérieur du chemin est rejetée, afin qu’une balise de livraison forgée ne puisse pas voyager en aval ; - les emplacements protégés d’identifiants/système (les
is_write_denied/is_write_approval_requireddefile_safety) sont rejetés.
Omettez output_path et l’outil écrit dans <audio cache>/tts_<timestamp>.<ext> — un nom horodaté très peu susceptible d’entrer en collision avec quelque chose qui vous appartient. C’est aussi pourquoi la plupart des gens n’ont jamais rencontré ce bug : le déclencher exige de pointer l’outil vers un fichier qui existe déjà.
Si cela vous est déjà arrivé
Soyons directs : un fichier supprimé ne va pas dans une corbeille, et sans sauvegarde il a disparu. Mais quelques habitudes réduisent le rayon d’impact :
- Checkpoints. Le système de checkpoints d’Hermes peut annuler les modifications de fichiers au sein d’une session ; les commandes
hermes checkpointsvous permettent de les inspecter et de les restaurer. Il est conçu pour les modifications faites par l’agent, donc il n’aide que si le fichier touché vit dans un workspace suivi. - Exports de session. Le texte qui a été synthétisé existe généralement encore dans la transcription, donc l’audio peut être régénéré. Un enregistrement, non — c’est toute la différence qui fait le risque.
- Gardez les fichiers irremplaçables hors des répertoires exposés aux outils. Nous avons documenté une classe similaire de perte de fichiers accidentelle dans l’incident de perte de données du worktree sale.
- Pour la surface complète des checkpoints, voir la référence hermes checkpoints.
Comment utiliser output_path en toute sécurité
Une seule règle suffit : ne pointez jamais output_path vers un fichier que vous ne pouvez pas vous permettre de perdre.
- Vous voulez une version parlée d’un texte ? Ne passez pas du tout
output_path— prenez le chemin de cache par défaut. - Vous archivez de l’audio ? Utilisez un nom de fichier neuf (
summaries/2026-10-10-daily.mp3), pas un nom existant. - Écraser délibérément un fichier de sortie fixe est désormais sûr par contrat (un succès écrase, un échec préserve), mais ne gardez à côté l’unique copie de rien.
Quand pourrez-vous l’obtenir
Le correctif a été fusionné le 2026-10-09, après la sortie de la v0.21.6 le 8 octobre, il est donc uniquement sur origin/main et ne figure encore dans aucune release taguée. hermes update le récupérera depuis main, ou vous pouvez attendre la prochaine release. Le statut des releases et le travail de stabilité plus large sont résumés sur notre page de release v0.21.6.
Le test de régression est arrivé avec lui (tests/tools/test_tts_failure_preserves_existing.py), épinglant le contrat en trois assertions : un fichier existant survit à une défaillance du provider sans laisser de fichier partiel derrière lui, un échec sur un nouveau chemin ne laisse rien derrière lui, et un succès écrase toujours. La leçon se généralise à quiconque écrit des outils : le code de nettoyage doit être certain de ce qu’il possède. Enregistrer le fichier de quelqu’un d’autre comme votre propre sortie temporaire coûte à l’utilisateur son enregistrement.