Aller au contenu principal

Auditer et vérifier une exécution

Ce guide couvre le processus de responsabilisation autour d'une exécution d'agent : lire ce qui a été enregistré et vérifier son intégrité. Il suppose que vous avez déjà exécuté au moins une tâche ou une session de chat face à un daemon.

Le runtime tient un seul registre : le journal chaîné par hachage. Ses entrées sont rattachées à une exécution, chaînées deux fois (à l'entrée précédente de leur exécution et à l'entrée précédente de n'importe quelle exécution), signées au moment de l'ajout, et la chaîne a une ancre de tête exportable. Chaque commande ci-dessous le lit : audit journal, audit export, audit show, audit verify, audit anchor et audit replay.

Pour comprendre la logique de ce modèle et son lien avec les exigences réglementaires, voir Le modèle de responsabilisation.

Une commande de cette liste fait moins que son nom ne le laisse croire. audit replay redérive une exécution depuis sa propre trace capturée, en consommant les réponses du modèle et les sorties d'outils enregistrées dans l'ordre de step_ordinal ; elle ne rejoue pas l'agent réel, parce que le journal n'a jamais capturé la question d'origine. Elle répond à la question de savoir si une trace est complète et cohérente avec elle-même, pas si le code d'aujourd'hui se comporterait pareil.

Lire le journal d'audit​

Pour lire les entrées d'une exécution donnée dans le journal chaîné par hachage, y compris les complétions du modèle capturées, résolvez-la par un identifiant d'exécution ou par un identifiant de tâche qui s'y rattache :

apollia-os audit show <run-or-task-id>

Cette lecture demande d'avoir un identifiant d'exécution sous la main. Pour parcourir le journal sans en connaître un, de l'entrée la plus récente à la plus ancienne et toutes exécutions confondues :

apollia-os audit journal --limit 20
apollia-os audit journal --limit 20 --offset 20

C'est la seule lecture du journal chaîné qui ne nomme pas une exécution à l'avance. Elle affiche une ligne par entrée : quand, quel agent, ce qui s'est passé, avec quel outil, sur quoi et avec quel résultat, puis l'exécution de l'entrée, sa position dans la chaîne de celle-ci, et si l'entrée porte une signature. La ligne est un résumé et ne contient aucun contenu. D'un appel d'outil, elle garde la cible, c'est-à-dire le seul argument que l'outil désigne comme tel (un chemin, un destinataire, l'hôte d'une adresse) et aucun autre ; un outil MCP n'en désigne aucun. Un même appel d'outil apparaît en deux entrées, une au démarrage et une à la fin, parce que le journal enregistre des événements et non des invocations.

Les arguments et la sortie d'un appel se lisent à la demande, depuis les entrées que l'exécution a gardées pour la relecture : audit show affiche l'exécution entière, et par l'API GET /api/v1/audit/journal/<run>/entries/<seq> rend une entrée avec les arguments et la sortie de son appel. Un appel fait par le code d'un agent, sans qu'un modèle l'ait demandé, n'a pas d'arguments capturés à montrer.

Le journal tient toutes les exécutions de l'instance. Cinq filtres resserrent une page, et ils se cumulent ; --limit et --offset comptent alors les entrées qui les passent :

FiltreGardePar l'API
--agentles entrées qui nomment l'agent, et les exécutions de ses tâchesagents=
--familyune famille d'action : read, read_personal, write_or_delete, send_external, execute_codefamily=
--natureaction (un appel, un consentement, un refus), access_change (un accès, une connexion, un secret, une installation, une règle), policy, ou system (ce que le journal garde pour la relecture)nature=
--resultsuccess, failure ou refusedresult=
--since, --untilune période, en jours lus en UTC ou en instants RFC 3339, la fin excluesince=, until=

Chaque filtre se répète en ligne de commande et prend des valeurs séparées par des virgules par l'API, comme dans GET /api/v1/audit/journal?agents=<agent>,<agent>&result=refused :

apollia-os audit journal --agent flux-quotes --agent flux-invoices --limit 20
apollia-os audit journal --nature access_change --since 2026-10-01
apollia-os audit journal --family send_external --result refused

Exporter le journal​

Pour remettre le journal à un audit ou le conserver pour une revue, exportez-le dans un fichier, en CSV ou en JSON. --format et --output sont tous deux obligatoires, et les cinq filtres de audit journal restreignent l'export de la même façon :

apollia-os audit export --format csv --output journal.csv
apollia-os audit export --format json --since 2026-10-01 --until 2026-11-01 --output october.json
apollia-os audit export --format csv --agent flux-quotes --result refused --detail --output refused.csv

Le fichier contient toutes les entrées qui passent les filtres, de la plus récente à la plus ancienne. L'export n'est pas paginé : il n'y a pas de --limit, donc un fichier n'est jamais la partie la plus récente du journal sous un nom qui annonce le tout.

Sans --detail, un export ne contient aucun contenu. Chaque entrée est le résumé qu'affiche audit journal (ts, agent, nature, family, what, tool, target, outcome) et sa place dans la chaîne (run_id, seq, kind, hash). --detail ajoute la charge utile de chaque entrée et, pour un appel d'outil, ses arguments, sa sortie et son statut : en CSV sous la forme de quatre colonnes de plus (payload, arguments, output, status, les trois premières contenant du JSON), en JSON sous la forme d'un objet detail sur chaque entrée. C'est là que des données personnelles peuvent se trouver, et la commande le dit sur stderr quand elle écrit un tel fichier.

Le runtime répond le contenu du fichier et la commande l'écrit, donc un export va vers un chemin de cette machine et nulle part ailleurs. Chaque export est lui-même inscrit au journal, sous l'exécution governance:journal:export, avec son format, ses filtres, le nombre d'entrées et la présence du détail. Sans runtime, la commande sort avec le code 2 et n'écrit rien.

Le même export est disponible via l'API HTTP pour une intégration hôte, sous la forme GET /api/v1/audit/export?format=csv&detail=true&agents=<agent>,<agent> ; voir les opérations d'audit dans la référence de l'API HTTP.

Une chose mérite d'être connue avant de s'appuyer sur le journal. Un appel d'outil effectué dans une session de chat atteint le journal chaîné par hachage : la boucle de chat émet chaque sortie d'outil et chaque complétion du modèle comme un événement rattaché à une exécution, et le souscripteur du journal en fait des entrées chaînées. Lisez-les avec audit show sur l'exécution, ou avec audit journal. Les deux sont stockées telles quelles, ce qui rend un chat auditable et met aussi son contenu sur le disque. Et le journal est alimenté sans bloquer l'exécution : une entrée est jetée, avec un avertissement dans les logs, lorsque son canal est saturé.

Vérifier l'intégrité d'une exécution​

Le journal est une chaîne de hachage avec signatures, ce qui rend toute altération détectable. audit verify a deux formes, et elles ne contrôlent pas la même chose.

Avec un identifiant d'exécution, elle recalcule la chaîne propre à cette exécution et ses signatures :

apollia-os audit verify <run-id>

Une vérification réussie de cette forme indique que les entrées de cette exécution n'ont pas été modifiées, et qu'elles portent une signature valide lorsque le journal en est un signé. La signature n'est pas garantie : lorsque la clef HMAC ne peut être ni lue ni écrite, le runtime ouvre le journal non signé et journalise audit.journal.unsigned_fallback. La vérification n'exige alors aucune signature, et une chaîne non signée rend quand même un résultat correct. Cherchez cet événement dans les logs du runtime, ou la colonne de signature d'audit journal, avant de lire une vérification verte comme une preuve d'origine. La signature est un HMAC-SHA256, symétrique : elle montre que la chaîne a été écrite par un détenteur de la clef sur cette machine, et ce n'est pas quelque chose qu'un tiers peut contrôler de son côté.

La vérification ne dit pas non plus que l'exécution est complète : une troncature qui retire les dernières entrées laisse une chaîne plus courte qui se vérifie toujours.

Sans argument, elle parcourt la chaîne globale de toutes les exécutions et compare la tête terminale à l'ancre persistée, ce qui détecte une suppression intérieure, une exécution entière supprimée, et une queue tronquée :

apollia-os audit verify

Utilisez la forme sans argument quand la question est de savoir si quelque chose manque, et la forme par exécution quand la question est l'authenticité des entrées d'une exécution. Aucune des deux ne protège d'un détenteur de la clef de signature qui resignerait une chaîne plus courte : c'est l'ancre exportée et conservée hors machine qui couvre ce cas, et audit anchor l'affiche.

Ajoutez --json à audit journal, audit show et audit verify pour obtenir une sortie exploitable par une machine. audit export écrit le format que nomme --format. --json est une option globale, donc audit export --json est accepté aussi, mais cela ne change rien au fichier : cela modifie ce que la commande affiche une fois le fichier écrit, et la forme d'un message d'erreur.

Conservation et politique d'organisation​

Le journal n'a aucune fonction d'effacement : ni commande, ni route, ni écran ne retire une entrée. Ce qui existe, c'est la conservation, et seule une politique d'organisation signée la fixe. Sa section [journal] tient quatre clés :

CléEffet
required = trueSans journal ouvert, le runtime refuse toute action sensible et toute nouvelle connexion. La lecture continue, et la fenêtre dit pourquoi.
retention_days = NAu démarrage, puis une fois par jour, les entrées de plus de N jours sont retirées.
export.detail = "forbid" ou "require"Le détail d'un export n'est plus un choix : audit export demandé dans l'autre sens est refusé avec la raison.
export.daily_summary_dir = "<dossier>"Un fichier de résumé par jour UTC, journal-AAAA-MM-JJ.json, est écrit dans ce dossier du poste, avec l'ancre de tête de la chaîne.

Un retrait par conservation est lui-même une entrée, journal_purged, chaînée et signée comme les autres. Elle porte l'ancre de ce qui a été retiré : audit verify sans argument repart d'elle, reste vert, et dit d'où il est parti :

OK whole journal 412 entries across 18 runs verified
verified from global_seq=9051 (2026-07-02T06:14:09Z): earlier entries were removed by the retention of the organisation policy on 2026-09-30T00:01:00Z

Avec --json, la même information est l'objet start du rapport. Une entrée altérée avant ou après l'entrée de purge reste signalée, de même qu'un journal qui commence plus tard que ne le dit sa dernière entrée de purge.

L'export quotidien passe avant la conservation, et rattrape au démarrage les jours où le runtime ne tournait pas : un jour atteint le dossier avant que ses entrées soient retirées. Un dossier inutilisable ne refuse rien : la fenêtre le dit, et le journal enregistre journal_export_failed. Sans politique, rien n'est exporté à heure fixe et rien n'est jamais retiré.

Ce que cela ne fait pas, c'est empêcher : le journal et sa clé sont dans votre propre dossier, une modification est donc détectée par audit verify, jamais bloquée. Un journal coupé à la fin n'est détecté que face à une ancre conservée hors du poste, celle que les fichiers quotidiens, ou audit anchor, vous donnent à garder.

En pratique​

Un passage type de responsabilisation consiste à utiliser audit show pour lire ce qu'une exécution a fait, puis audit verify sans argument pour confirmer que rien n'a été retiré, puis audit verify <run-id> sur l'exécution en question. Les trois commandes sont en lecture seule : aucune ne modifie quoi que ce soit sur le disque.

Voir aussi​