Série : Apprendre à coder avec l’IA

Le chercher-remplacer qui a corrompu 300 fichiers

Ce qu'une passe d'audit approfondie a révélé, et à quelle vitesse ce fut corrigé.

Deux constats, un audit Quand un balayage correspond à plus qu'il ne devrait 300 fichiers corrompus process_event→ charabié on_success()→ charabié has_access→ charabié pas de délimitation de mot compilait au vert, se parsait bien correctif : restaurer depuis git secret codé en dur secret = "●●●●●●●●●●●" ressemblait à un placeholder est devenu une vraie valeur correctif : rotation + variable d'env correctif : scan de secrets en CI
Les dégâts étaient dans la couche humaine du code, là où le compilateur ne regarde jamais.

Les bases de code pilotées par agents avancent vite — et échouent de façons qu'un relecteur humain aurait repérées d'un coup d'œil. Voici deux échecs, en public, et les garde-fous qui les arrêtent net désormais.

Le jour où vous cherchez les ennuis est généralement le jour où vous en trouvez. Ce samedi, j’avais programmé une passe d’audit approfondie sur la base de code. Le plan était de rattraper la dette technique : blocs except nus, constantes codées en dur, dérive des conventions, le genre d’amas qui s’accumule dans tout projet qui avance vite.

J’ai trouvé deux choses que je n’ai pas appréciées. Les deux sont maintenant corrigées. Les deux sont le genre de constat dont je veux parler publiquement, parce qu’elles expliquent quelque chose de réel sur la façon dont une base de code pilotée par agents échoue et sur la façon dont la discipline de faire un audit attrape les échecs.

Constat un : le chercher-remplacer qui a dévoré la base de code

Plus tôt dans le projet, un agent avait reçu pour tâche de faire un balayage de style marketing à travers les docstrings et commentaires. L’intention était raisonnable : renommer une phrase spécifique qui apparaissait dans quelques chaînes destinées au public. L’exécution n’était pas assez prudente concernant les délimitations de mots.

La phrase que l’agent était censé échanger était un slogan marketing particulier qui contenait les mots « process », « success » et « access » comme parties de phrases plus longues. L’opération de chercher-remplacer a fait correspondre ces sous-chaînes à des endroits où elles n’étaient pas censées correspondre. Noms de variables. Noms de fonctions. Descriptions de tests. Commentaires en ligne. Partout où ces trois sous-chaînes apparaissaient, la chaîne de remplacement de l’agent était substituée.

Le résultat fut trois cents fichiers avec des identifiants et de la prose subtilement charabiés. Des variables nommées process_event sont devenues quelque chose avec « Raku Game Engine Milestone » intégré au milieu du jeton. Les descriptions de fonctions se lisaient comme du non-sens. Les descriptions de tests prétendaient tester des choses qui n’existaient pas. La base de code compilait parce que les identifiants cassés étaient cohérents au sein de leurs fichiers, mais la couche lisible par l’humain de la base de code était vandalisée subtilement un peu partout.

Je veux être précis sur la façon dont ce genre d’échec se produit, car c’est une classe d’échec piloté par agents que d’autres équipes rencontreront.

La recherche était délimitée trop largement. L’agent avait pour instruction de trouver une phrase et de la remplacer. La phrase se trouvait être une sous-chaîne de mots anglais courants. La bonne façon de délimiter cette recherche est sur les frontières de mots (\bmot\b en regex), avec une sensibilité à la casse explicite, avec une liste blanche explicite d’extensions de fichiers, avec une liste noire explicite de contextes d’identifiants. L’instruction que l’agent a reçue n’avait aucune de ces contraintes.

L’agent n’a pas signalé l’ampleur. Trois cents fichiers, c’est beaucoup de fichiers. Un agent qui livre une PR touchant trois cents fichiers pour une petite retouche marketing aurait dû signaler l’ampleur au moment de l’ouverture de la PR. L’agent ne l’a pas fait. Le titre de la PR disait quelque chose comme « mise à jour du texte marketing dans les docstrings ». Le corps de la PR listait le nombre de fichiers comme un chiffre, pas comme une préoccupation.

Mon processus de relecture ne l’a pas attrapé. Le diff de la PR était trois cents fichiers de petits changements de deux lignes qui ressemblaient tous à la même modification. Le diff se lit, à première vue, comme un balayage propre. La corruption n’apparaît que si vous lisez le contenu réel modifié d’un fichier à un moment où la substitution de l’agent produisait du non-sens. Je ne l’ai pas fait. J’ai fusionné.

La CI ne l’a pas attrapé parce que les noms se parsaient toujours. Les identifiants corrompus étaient syntaxiquement valides. Les compilateurs se moquent de savoir si votre variable porte un nom qui ressemble à un slogan marketing. Le build était vert. Les tests s’exécutaient toujours. Les dégâts étaient dans la couche humaine du code, pas dans la couche machine.

Comment je l’ai corrigé ce samedi

Un script. Le script fait trois choses.

Un : redériver les noms d’identifiants canoniques. À partir de l’historique git avant que le mauvais chercher-remplacer n’atterrisse, le script reconstruit comment chaque identifiant était censé s’appeler. La reconstruction est mécanique : pour chaque fichier touché par la mauvaise PR, comparer la version pré-PR à la version post-PR, et pour chaque jeton substitué, proposer une restauration vers le nom pré-PR. La plupart des fichiers se restaurent proprement. Un petit nombre nécessite une relecture manuelle parce qu’ils avaient des changements légitimes superposés à la corruption.

Deux : une passe de bon sens pilotée par grep. Même après restauration, certains des identifiants corrompus avaient été référencés depuis du nouveau code écrit après l’atterrissage de la mauvaise PR. Ces références avaient été écrites contre les noms corrompus. La passe grep trouve chaque référence à un identifiant de style corrompu dans le code écrit après l’atterrissage de la mauvaise PR, et signale chacune pour une décision manuelle : ce nouveau code était-il censé utiliser le nom corrompu (rare), ou utilisait-il simplement le nom qui existait à l’époque (la plupart des cas) ?

Trois : un garde-fou pour l’avenir. Chaque opération de chercher-remplacer qu’un agent fait maintenant doit spécifier (a) une délimitation par frontière de mot, (b) la sensibilité à la casse, (c) une liste blanche d’extensions de fichiers, (d) un seuil maximal de nombre de fichiers au-delà duquel l’agent doit signaler et demander une relecture explicite, et (e) un échantillon de trois correspondances aléatoires que l’agent doit montrer avant d’appliquer le remplacement complet. Le garde-fou est dans le Guide Copilot et fait maintenant partie de chaque cadrage de tâche de chercher-remplacer.

La corruption est maintenant réparée. Le script d’audit qui a fait la réparation est dans le dépôt, exécutable à tout moment, avec les sorties de diff sauvegardées comme preuve. La leçon est dans le Guide Copilot.

Constat deux : le secret HMAC codé en dur

La passe d’audit approfondie a aussi révélé quelque chose que j’aurais dû attraper plus tôt. La couche de licence du runtime utilise HMAC-SHA-256 pour vérifier les jetons de licence. Le secret HMAC était codé en dur dans un fichier source. Le fichier source était dans le dépôt public. Le secret était un vrai secret utilisé par un vrai chemin de vérification de production.

C’est le constat le plus embarrassant de la journée. Je veux être honnête à ce sujet parce que c’est exactement le genre de chose qui arrive dans les bases de code pilotées par agents qui avancent vite, et la discussion publique sur comment l’attraper est plus précieuse que la discussion privée.

Le chemin qu’il a pris pour atterrir : Une première version de la couche de licence a été prototypée avec une valeur de secret placeholder, destinée à être remplacée avant que la couche ne soit livrée à qui que ce soit. Le prototype a atterri dans une PR avec un placeholder de dev à l’aspect évident. Au fil du temps, une vraie logique de vérification a été ajoutée par-dessus le placeholder. Le placeholder a cessé de ressembler à un placeholder une fois enveloppé dans du code de validation à l’aspect réel. Le temps que quelqu’un le remarque, le secret était utilisé dans des flux de style production et le fichier était dans le dépôt public.

Ce que j’ai fait aujourd’hui :

  • Fait tourner le secret. La valeur compromise n’est plus la valeur de production. La nouvelle valeur est dans une variable d’environnement, avec un repli warnings.warn() pour les environnements de dev qui permet au travail de dev de continuer sans vrai secret mais qui crie bruyamment à ce sujet.
  • Retiré la valeur codée en dur du fichier source. Le remplacement est un getenv avec un message d’erreur clair si la variable d’environnement n’est pas définie dans un build de production.
  • Ajouté une vérification CI qui scanne les secrets codés en dur correspondant à des motifs courants (chaînes à haute entropie, jetons en forme de base64, tout ce qui ressemble à une clé). La vérification est le genre de petite infrastructure qui attrapera la prochaine tentative avant qu’elle n’atterrisse.
  • Déposé un suivi pour auditer le reste de la base de code à la recherche de motifs similaires. L’audit est le travail d’un autre week-end. Aujourd’hui, il s’agissait de clore le constat immédiat.

La couche de licence fonctionne toujours. Le nouveau chemin est plus sûr. Le secret compromis a été renouvelé dans les heures suivant sa découverte.

Ce que cela généralise

Quelques points honnêtes.

Le chercher-remplacer piloté par agents a besoin de règles de délimitation explicites. C’est la troisième fois dans l’histoire du projet qu’un balayage trop large me mord. Les deux premières fois étaient moins dommageables. Cette fois était assez grave pour mériter un garde-fou permanent. Le garde-fou est maintenant en place.

Les secrets codés en dur dans les fichiers source sont un échec de discipline, pas un échec d’outillage. Aucun outil ne sauvera une équipe qui laisse un vrai secret atterrir dans un fichier public. La discipline de « chaque commit est relu à la recherche d’identifiants codés en dur » est le vrai correctif. Le scan CI aide. La discipline est ce qui compte.

Les audits trouvent ce que la relecture a manqué. La discipline de faire tourner une passe d’audit programmée sur la base de code, en cherchant spécifiquement les modes d’échec que la relecture PR par PR a tendance à manquer, vaut le temps investi. L’audit d’aujourd’hui a attrapé deux choses que la relecture de PR avait laissées passer. Les futurs audits en attraperont d’autres. La cadence est le point.

Ce que partenaires et bâtisseurs devraient en retenir

Si vous évaluez un moteur pour un partenariat, demandez à l’équipe comment ils gèrent le mode d’échec « balayage trop large piloté par agent ». La bonne réponse implique des règles de délimitation explicites, un signalement obligatoire sur les changements importants, et des passes d’audit. La mauvaise réponse est « nous n’avons pas vu ce problème ».

Si vous exécutez vous-même un flux de travail piloté par agents et que vous n’avez pas fait d’audit de secrets codés en dur récemment, faites-en un. La probabilité que quelque chose se soit glissé n’est pas nulle. Le coût de le trouver maintenant est faible.

Si vous êtes un professionnel de la sécurité qui lit ceci et que vous avez des suggestions, je suis sincèrement intéressé. La classe d’échec contre laquelle je travaille à me défendre est « l’agent fait quelque chose qu’un relecteur humain aurait repéré d’un coup d’œil mais n’a pas été attrapé dans le schéma de relecture en masse qu’un flux de travail piloté par agents encourage ». Suggestions bienvenues.

Samedi après-midi. La base de code a subi un examen sérieux. Deux constats, tous deux corrigés. Le prochain audit est au calendrier.

Retour à la construction.

Un runtime construit pour être audité

RakuAI est le runtime spatial avec lequel les fabricants de LLM et de lunettes intelligentes construisent — discipliné par des audits, endurci par des leçons publiques. Voyez comment nous concevons pour une confiance digne d'un partenaire.

← Tous les articles