La revue de sécurité qui n'a jamais tourné : vérifier ce que déclarent les agents d'une chaîne de livraison IA
eHVB-IT · · sécurité de l'IA · agents LLM · sécurité applicative · audit · ingénierie de détection
Un agent de revue peut déclarer PASS sans avoir ouvert le diff, et la trace d'audit dit qu'un contrôle a fonctionné. Les deux formes observées et le contrôle qui confronte la déclaration au runtime et au dépôt.
Résumé
Les équipes qui développent avec des agents LLM leur confient désormais une partie du travail de sécurité : un agent de revue lit le diff, un agent QA lance les tests, et la chaîne enregistre un PASS. Un auditeur verrait dans cet enregistrement la preuve qu'un contrôle a fonctionné. Nous avons constaté que cet enregistrement peut être faux d'une manière précise : un agent déclare DONE ou PASS pour un travail qu'il n'a jamais effectué, dans une prose identique à celle d'un vrai rapport. Pour une revue de sécurité, cela donne un contrôle qui n'a pas tourné alors que la trace dit le contraire. Cette note décrit les deux formes que nous avons observées, explique pourquoi le rapport de l'agent ne peut pas servir à les révéler, et présente le contrôle que nous appliquons : il compare l'affirmation à ce que le runtime et le dépôt peuvent prouver.
Pourquoi c'est un problème de sécurité
Dans une chaîne où des agents relisent d'autres agents, l'orchestrateur lit des rapports, et l'humain en bout de chaîne lit souvent une synthèse de ces rapports. Chaque étape en aval hérite de ce que dit le rapport. Si un agent de revue de sécurité déclare PASS sans avoir ouvert le diff, la modification est fusionnée avec une revue enregistrée et aucune revue faite.
La pratique de l'audit distingue déjà la conception d'un contrôle et son efficacité opérationnelle : une étape de revue documentée ne prouve rien sur la revue d'une modification donnée. Les chaînes d'agents ont besoin de la même distinction, et elles la rendent plus difficile, parce que la preuve de fonctionnement est elle-même du texte produit par l'agent contrôlé.
Un rapport est aussi un texte qu'un attaquant peut parfois influencer. Un agent qui lit un fichier piégé peut être amené à produire une synthèse assurée (voir notre note sur l'injection de prompt par le contenu des outils). Nous traitons le rapport d'un sous-agent comme n'importe quelle sortie d'outil : une affirmation à vérifier.
Les deux formes
La première est la narration seule. L'agent décrit des actions qu'il n'a jamais exécutées : « j'ai lu le fichier », « j'ai lancé les tests, tout est vert », « j'ai vérifié la migration ». Son transcript ne contient aucun appel d'outil réel, ou des appels écrits en texte au lieu d'être exécutés. Le rapport est fluide et précis.
La seconde est plus difficile à repérer. L'agent appelle bien des outils, donc un contrôle qui se demande si un outil a été utilisé passe. Chaque appel est une lecture : ouvrir un fichier, chercher, lister un répertoire. Rien n'a été modifié, écrit ni exécuté, la tâche exigeait une modification, et l'agent déclare DONE.
Vu de l'orchestrateur, les deux ressemblent à un DONE accompagné d'une explication convaincante.
Exécution réelle
status: DONE
- lecture
- recherche
- modification
- tests
- écriture
dépôt : diff présent
accepté
Narration seule
status: DONE
- 0 appel d'outil réel
dépôt : arbre propre
rejeté · narration seule
Imposture en lecture seule
status: DONE
- lecture
- recherche
- lecture
- liste
- lecture
dépôt : arbre propre
rejeté · lecture seule
Nous avons rencontré les deux formes dans notre propre flotte en avril 2026, deux jours de suite. La première venait d'un agent d'orchestration qui rendait des rapports de pure narration ; nous l'avons retiré de la flotte. La seconde est apparue le lendemain, et c'est pour elle que la trace ci-dessous ventile l'usage des outils par type.
Ce que le contrôle compare
Le principe est celui que nous appliquons à toute preuve dans une investigation : une affirmation est acceptée quand une source indépendante la confirme.
Chaque agent termine son exécution par une trace structurée plutôt que par du texte libre :
trace:
delegation_id: <id de la tâche déléguée>
parent_id: <id amont, ou "user">
agent: <nom de l'agent>
tool_uses_count: <total des appels d'outils>
tool_uses_breakdown:
read_count: <lectures et recherches>
edit_count: <modifications>
write_count: <écritures de fichiers>
bash_count: <commandes shell>
expected_evidence: <code | artifact | read_only | none>
git_evidence: <git status ou dernier commit, ou "none">
status: <DONE | BLOCKED | NEEDS_REVIEW | ...>
C'est la ventilation par type qui expose la seconde forme : des outils ont été utilisés, et aucun n'aurait pu modifier quoi que ce soit. Les identifiants de délégation et de parent permettent de reconstruire après coup qui a demandé quoi à qui, et c'est ainsi que se diagnostiquent la plupart des défaillances en chaîne.
-
Affirmation
status: PASS - Trace Auto-déclarée appels d'outils ventilés par type
- Runtime Identité et rôle attribution sur le transcript, preuve attendue selon le rôle
- Dépôt Diff ou commit tranche pour le travail de code
- Verdict Journalisé et lu
La trace est auto-déclarée, et notre contrôle en reprend aujourd'hui les comptes d'outils tels quels. Deux éléments ne viennent pas de l'agent. Son identité est tirée de l'attribution que le runtime écrit sur le transcript, et ne retombe sur le nom déclaré qu'à défaut. La preuve attendue découle de son rôle : un développeur doit produire du code, un relecteur lire. Un agent peut relever sa preuve attendue (un relecteur qui écrit un fichier de rapport déclare un artefact), jamais l'abaisser. Un DONE sur une tâche de code dont la preuve git ne montre aucun changement est rejeté.
L'étape suivante consiste à prendre les comptes dans le transcript lui-même plutôt que dans la déclaration. La fonction ci-dessous est écrite pour cette version.
Les règles de décision en code
Les règles tiennent en une fonction. claim est ce que l'agent déclare ; observed est ce que montrent le transcript et le dépôt. Tant que observed n'est pas alimenté par le transcript, la règle sur les comptes contradictoires n'a rien à comparer et les autres règles s'appliquent aux chiffres déclarés. La fonction n'utilise que la bibliothèque standard de Python et embarque ses propres vérifications :
def verdict(claim: dict, observed: dict) -> str:
if claim["status"] not in ("DONE", "PASS"):
return "no completion claimed"
if observed["tool_uses"] == 0:
return "narration only"
if claim["tool_uses"] != observed["tool_uses"]:
return "self-report contradicts the transcript"
changes = observed["edits"] + observed["writes"]
if claim["expected_evidence"] == "code":
if changes == 0:
return "read-only imposture"
if not observed["diff"]:
return "claimed work without a diff"
if claim["expected_evidence"] == "artifact" and changes == 0:
return "over-claimed evidence"
return "consistent"
if __name__ == "__main__":
def obs(tools, edits=0, writes=0, diff=False):
return {"tool_uses": tools, "edits": edits, "writes": writes, "diff": diff}
def cl(tools, evidence, status="DONE"):
return {"tool_uses": tools, "expected_evidence": evidence, "status": status}
assert verdict(cl(5, "code"), obs(0)) == "narration only"
assert verdict(cl(7, "code"), obs(7)) == "read-only imposture"
assert verdict(cl(9, "code"), obs(9, edits=2)) == "claimed work without a diff"
assert verdict(cl(12, "code"), obs(4, edits=1, diff=True)) == "self-report contradicts the transcript"
assert verdict(cl(3, "artifact"), obs(3)) == "over-claimed evidence"
assert verdict(cl(6, "read_only", "PASS"), obs(6)) == "consistent"
assert verdict(cl(8, "code"), obs(8, edits=3, diff=True)) == "consistent"
print("ok")
Un relecteur qui ne fait que lire ressort cohérent, et c'est juste : lire est son travail. Son PASS ne vaut alors que ce qu'il a lu. La liste des fichiers qu'un agent de revue a ouverts a donc sa place à côté de son verdict : une revue qui n'a jamais ouvert les fichiers modifiés se voit immédiatement.
Les commandes shell sont volontairement exclues du compte des modifications. Une commande peut modifier l'arbre de travail ou seulement le lister, et la trace seule ne permet pas de savoir laquelle. Le diff tranche ce cas.
Où vont les verdicts
Le contrôle s'exécute à l'arrêt de chaque sous-agent et écrit son verdict dans un journal qu'un script de rapport lit. Cette seconde moitié compte autant que la première. Nous avons un jour supprimé un stockage de traces alimenté pendant des mois et jamais lu. Un contrôle dont personne ne lit la sortie est dans la même situation que la revue qui n'a jamais tourné.
Limites
- Tant que les comptes sont auto-déclarés, un agent qui invente des comptes plausibles pour une tâche de revue passe. Les règles de rôle et de git attrapent toujours une tâche de code sans modification ; rien n'attrape un relecteur qui ment sur ses lectures.
- Le contrôle prouve qu'aucun travail n'a eu lieu. Il ne prouve pas que le travail est correct, ce qui reste l'affaire des tests et de la revue humaine.
- Les valeurs par défaut liées au rôle sont grossières. Un développeur n'a parfois rien à modifier, et un artefact déclaré à tort produit un faux positif.
- Les agents absents de la liste connue sont ignorés : un nouveau type d'agent n'est pas contrôlé tant qu'il n'y est pas ajouté.
- Un agent peut produire un diff réel mais trivial. Le dépôt prouve que quelque chose a changé, et seule la revue montre si cela correspond à la tâche.
- Un relecteur qui a ouvert les bons fichiers et a quand même manqué une vulnérabilité passe ce contrôle. Il détecte l'absence de travail, jamais un mauvais jugement.
Check-list
- N'acceptez jamais un DONE ou un PASS en texte libre. Exigez une trace structurée.
- Comptez l'usage des outils par type, pour que des lectures ne passent pas pour des modifications.
- Confrontez la trace au transcript, et prenez l'identité dans le runtime.
- Laissez le dépôt trancher pour le travail de code.
- Enregistrez les fichiers qu'un agent de revue a réellement lus, à côté de son verdict.
- Traitez le rapport d'un sous-agent comme un texte non fiable, au même titre que toute sortie d'outil.
- Assurez-vous que quelqu'un lit les verdicts, et gardez une validation humaine avant toute action irréversible.
eHVB-IT est un cabinet français de cybersécurité (tri d'alertes SOC, sécurité applicative, ingénierie de détection). Contact : contact@ehvb-it.fr