# Connecter ses journaux à Conforms depuis son agent

Guide destiné aux agents (Claude Code, Cursor, Hermes, n8n, applications) et à leurs utilisateurs.
Il décrit le contrat réel de l'API d'ingestion de Conforms et une recette par environnement.

## Principe

- **L'agent installe un export ; il n'envoie jamais lui-même le contenu des journaux** et ne le recopie jamais dans la conversation. Les capteurs de référence écrivent dans une file locale, puis `envoyer.mjs` la transmet.
- **Un envoi par jour.** Chaque envoi crée un upload dans Conforms et, si le système est en parcours automatique, une analyse : les capteurs remplissent la file en continu, et `envoyer.mjs --quotidien`, lancé toutes les heures, ne la transmet qu'une fois par jour. Un poste en veille à l'heure habituelle envoie à son réveil.
- Recettes écrites pour **Linux et macOS**. Windows n'est pas pris en charge (droits du fichier de clé, tâche planifiée).
- **La clé d'ingestion est créée par un administrateur dans Conforms** (Paramètres → Surveillance continue → Gérer les clés d'API) et posée **par un humain** dans le fichier `~/.conforms/cle-ingestion`, droits 600 : c'est le seul emplacement vu par une tâche planifiée, qui n'hérite pas des variables du shell. `envoyer.mjs` refuse un fichier lisible par d'autres comptes. Commande à taper par l'humain, sous bash ou zsh (coller la clé à l'invite « Clé : », puis Entrée ; rien ne s'affiche, et rien ne va dans l'historique du shell) :

```sh
( umask 077; mkdir -p ~/.conforms; printf 'Clé : ' >&2; read -rs CLE; echo >&2; printf '%s\n' "$CLE" > ~/.conforms/cle-ingestion )
```

  La variable `CONFORMS_INGEST_KEY` n'est utile que pour un lancement manuel. L'agent n'ouvre jamais ce fichier et ne lit, n'affiche ni ne recopie jamais la clé : il contrôle la clé et l'adresse avec `node envoyer.mjs --verifier` (sauf n8n, où la clé vit dans l'identifiant n8n). Ne jamais placer la clé dans un fichier versionné, une crontab ou le bloc `env` d'un fichier de réglages.
- **Adresse d'envoi** (variable `CONFORMS_INGEST_URL`) : reprendre **l'adresse exacte donnée par le prompt**, de la forme `https://conforms.fr/api/ingest?systeme=<identifiant>`. Le paramètre `systeme` est obligatoire dès que l'organisation déclare plusieurs systèmes IA ; l'identifiant figure dans Paramètres, bloc « Connecter votre agent ».

## Contrat de l'API

- `POST https://conforms.fr/api/ingest`, en-tête `Authorization: Bearer <clé>` (seul mode d'authentification). Aucune redirection n'est à suivre : `envoyer.mjs` les refuse.
- Corps : JSONL, un objet JSON **sur une seule ligne** par enregistrement, en UTF-8, sans compression. Un tableau JSON d'objets est aussi accepté.
- Bornes : 25 Mo par envoi, 4 Mo par ligne, 60 envois par heure et par organisation, **tous postes et connecteurs confondus** (fenêtre fixe d'une heure ; l'en-tête `Retry-After` n'est qu'indicatif). Chaque requête authentifiée compte, rejeux et `--verifier` compris : mettre en tampon, envoyer par lots, et répartir le budget entre les postes (un poste envoie une fois par jour ; après une interruption, un passage fait au plus 5 envois, variable `CONFORMS_MAX_ENVOIS`).
- Réponses :
  - `202` : envoi reçu, `{ uploadId, statut, dejaRecu: false, message }`. Reçu ne veut pas dire analysé : suivre l'envoi avec `GET https://conforms.fr/api/ingest/<uploadId>` (même clé).
  - `200` : corps identique, à l'octet près, à un envoi déjà reçu ; rien n'est recréé.
  - `400` sur le corps : refus motivé par un message `{ error }` (voir « Format »). Ne pas renvoyer tel quel.
  - `400` sur le système cible (« Paramètre systeme invalide », « Système IA introuvable », « Aucun système IA déclaré », « Plusieurs systèmes IA déclarés ») : c'est l'adresse qui est à corriger, pas les journaux.
  - `401` : clé manquante, invalide ou révoquée. `403` : organisation non abonnée ou compte à mettre en règle dans Conforms.
  - `409` : plafond du système atteint. `413` : corps trop volumineux. `429` : trop d'envois récents, réessayer plus tard.
- Statuts d'un envoi : `uploaded`, `parsing`, `parsed` (en attente de confirmation, ou lignes en échec), `validated`, `failed`.

## Format des enregistrements

Chaque ligne porte **exactement** ces clés, toujours présentes (`null` quand la valeur manque) :

| Clé | Contenu |
|---|---|
| `timestamp` | Date de l'événement, RFC 3339 en UTC (`2026-09-26T10:15:00.000Z`). Obligatoire. |
| `agentId` | Nom stable de l'agent (par exemple `claude-code`, `assistant-support`). |
| `sessionId` | Identifiant de la conversation ou de l'exécution. |
| `eventType` | `completion` pour un échange, `error` pour un échec. |
| `input` | Demande adressée à l'agent (texte). |
| `output` | Réponse de l'agent (texte). |
| `model` | Modèle utilisé, s'il est connu. |

- Valeurs : texte ou `null`, jamais un objet ni un tableau.
- **Toujours le même jeu de clés** : Conforms reconnaît la structure par ses clés. Une clé omise, ajoutée ou renommée crée une nouvelle structure, donc une nouvelle confirmation.
- Ne jamais placer à la racine `messages`, `choices`, `workflowId`, `jsonrpc`, `run_type` ni `request.headers` : l'envoi serait classé comme une enveloppe de fournisseur ou refusé en `400`.

```json
{"timestamp":"2026-09-26T10:15:00.000Z","agentId":"assistant-support","sessionId":"conv-4812","eventType":"completion","input":"Quels sont mes droits sur mes données ?","output":"Vous pouvez demander l'accès, la rectification ou l'effacement…","model":"claude-opus-5-5"}
```

## Mise en service et confirmation humaine

Dans cet ordre, pour qu'une seule confirmation suffise :

1. Contrôler la clé et l'adresse avec `node envoyer.mjs --verifier`, dans les conditions de la tâche planifiée (voir plus bas) ; en cas d'échec, ne rien installer. Pour n8n, sans fichier de clé, ce contrôle est l'exécution de test de l'étape 3.
2. Installer le capteur de la recette (hooks, plugin Hermes, nœuds ou module).
3. Après quelques échanges, vérifier que la file contient des lignes (`wc -l ~/.conforms/journal/en-attente.jsonl`, sans en afficher le contenu), puis faire **un premier envoi à la main** (`node envoyer.mjs`, ou une exécution de test du workflow n8n) et transmettre à l'utilisateur l'`uploadId` affiché.
4. Un membre de l'organisation ouvre `https://conforms.fr/uploads/<uploadId>` et clique « Confirmer la correspondance ».
5. **Seulement ensuite**, activer l'envoi quotidien (ou publier le workflow n8n). Si la mise en service n'aboutit pas, retirer le capteur installé.

Les envois suivants de même structure sont alors traités sans intervention, tant qu'aucune ligne n'échoue. Un envoi reçu **avant** la confirmation garde sa propre confirmation à faire, séparément. Ce format n'est jamais validé d'office : la confirmation humaine fait partie du contrôle.

## Scripts de référence

Sans dépendance (Node 18 ou plus ; Python 3.8 ou plus pour `journal_sdk.py`). Les télécharger par exemple dans `~/.conforms/bin/` (`curl -fsSLo <fichier> <adresse>`) et **les montrer à l'utilisateur avant de les activer**.

| Script | Adresse | Rôle |
|---|---|---|
| `envoyer.mjs` | https://conforms.fr/connexion-agent/envoyer.mjs | Transmet la file locale par lots ; `--quotidien` limite la tâche horaire à un passage abouti par jour ; `--verifier` contrôle la clé et l'adresse sans envoyer de journal. |
| `hook-agent.mjs` | https://conforms.fr/connexion-agent/hook-agent.mjs | Capteur de hooks pour Claude Code et Cursor. |
| `hermes_plugin.py` | https://conforms.fr/connexion-agent/hermes_plugin.py | Plugin Hermes (point d'accroche `post_llm_call`), à installer sous le nom `__init__.py`. |
| `hermes_plugin.yaml` | https://conforms.fr/connexion-agent/hermes_plugin.yaml | Manifeste du plugin Hermes, à installer sous le nom `plugin.yaml`. |
| `journal-sdk.mjs` | https://conforms.fr/connexion-agent/journal-sdk.mjs | Journal des appels au modèle dans une application Node. |
| `journal_sdk.py` | https://conforms.fr/connexion-agent/journal_sdk.py | Journal des appels au modèle dans une application Python. |
| `n8n-code.txt` | https://conforms.fr/connexion-agent/n8n-code.txt | Code du nœud Code de n8n. |

**Envoi quotidien** (Linux ou macOS) : une tâche planifiée **toutes les heures** lance `envoyer.mjs --quotidien`, qui ne fait qu'un passage abouti par jour (23 heures au moins entre deux) ; après un report (`429`, `5xx`, réseau), il réessaie à l'heure suivante. Une heure fixe ne convient pas : cron saute l'exécution d'un poste en veille. Chemins absolus obligatoires (`command -v node`), la tâche n'ayant ni le `PATH` ni les variables du shell. Relever d'abord la crontab actuelle (`crontab -l`), y **ajouter** la ligne sans jamais la remplacer, puis vérifier avec `crontab -l` que les lignes relevées sont toujours là :

```sh
( crontab -l 2>/dev/null; echo "17 * * * * CONFORMS_INGEST_URL='<adresse du prompt>' /chemin/absolu/node /chemin/absolu/.conforms/bin/envoyer.mjs --quotidien >> /chemin/absolu/.conforms/envoi.log 2>&1" ) | crontab -
```

Contrôle dans les conditions de la tâche planifiée, avant de l'activer :

```sh
env -i HOME="$HOME" CONFORMS_INGEST_URL='<adresse du prompt>' /chemin/absolu/node /chemin/absolu/.conforms/bin/envoyer.mjs --verifier
```

## Données conservées sur le poste

- La file `~/.conforms/journal/en-attente.jsonl` (variable `CONFORMS_SPOOL` pour déplacer le dossier) contient les demandes et les réponses **en clair**, jusqu'à leur envoi. Ce dossier est **réservé** à la file : n'y placer aucun autre fichier, ni la clé.
- `rejets/` garde les envois refusés en `400` avec leur motif ; `etat/` garde, par session, la dernière demande en attente de réponse.
- Une ligne qui n'a pas pu partir (quelle qu'en soit la raison) est supprimée 30 jours après son horodatage (variable `CONFORMS_CONSERVATION_JOURS`) ; une partie déjà tentée, qui repart toujours à l'identique pour que Conforms reconnaisse un envoi déjà reçu, l'est entière, la même durée après sa ligne la plus récente ; les rejets, les états de session et la file elle-même, si plus rien n'y a été écrit, après la même durée sans écriture. C'est `envoyer.mjs` qui applique cette conservation, à chaque passage, même quand la clé manque : la tâche horaire doit donc rester en place. Seuls les fichiers des scripts sont concernés.
- Le dossier contient aussi `.verrou` (passage en cours) et `.dernier-passage` (date du dernier passage abouti).

## Recette : Claude Code et Cursor

Les hooks ne fournissent aucun horodatage : `hook-agent.mjs` date chaque événement lui-même. Il n'accède jamais au réseau, n'écrit rien sur la sortie standard et sort toujours en code 0, pour ne jamais bloquer l'agent.

**Périmètre, à faire choisir par l'utilisateur avant l'installation.** Dans les réglages de l'utilisateur (`~/.claude/settings.json`), le hook capte **toutes les sessions de cet utilisateur sur la machine**, tous projets confondus, sans masquage local. Pour le limiter : la variable `CONFORMS_PROJETS` (dossiers autorisés, en chemins absolus, séparés par `:`), à placer dans la commande du hook elle-même (`CONFORMS_PROJETS=/chemin/absolu/projet /chemin/absolu/node …/hook-agent.mjs`), hors de laquelle rien n'est enregistré ; ou les réglages locaux d'un seul projet, `.claude/settings.local.json`, non versionnés.

**Claude Code** — même commande sur `SessionStart`, `PostModelSwitch`, `UserPromptSubmit`, `Stop`, `StopFailure` et `SessionEnd` :

```json
{
  "hooks": {
    "SessionStart": [{ "hooks": [{ "type": "command", "command": "/chemin/absolu/node /chemin/absolu/.conforms/bin/hook-agent.mjs" }] }],
    "PostModelSwitch": [{ "hooks": [{ "type": "command", "command": "/chemin/absolu/node /chemin/absolu/.conforms/bin/hook-agent.mjs" }] }],
    "UserPromptSubmit": [{ "hooks": [{ "type": "command", "command": "/chemin/absolu/node /chemin/absolu/.conforms/bin/hook-agent.mjs" }] }],
    "Stop": [{ "hooks": [{ "type": "command", "command": "/chemin/absolu/node /chemin/absolu/.conforms/bin/hook-agent.mjs" }] }],
    "StopFailure": [{ "hooks": [{ "type": "command", "command": "/chemin/absolu/node /chemin/absolu/.conforms/bin/hook-agent.mjs" }] }],
    "SessionEnd": [{ "hooks": [{ "type": "command", "command": "/chemin/absolu/node /chemin/absolu/.conforms/bin/hook-agent.mjs" }] }]
  }
}
```

**Cursor** — dans son propre fichier `~/.cursor/hooks.json`, sur `beforeSubmitPrompt`, `afterAgentResponse`, `stop` et `sessionEnd`. Il est indispensable : les hooks de Claude Code que Cursor sait charger ne transmettent pas le texte des réponses. Le script tolère d'être appelé deux fois pour un même événement.

```json
{
  "version": 1,
  "hooks": {
    "beforeSubmitPrompt": [{ "command": "/chemin/absolu/node /chemin/absolu/.conforms/bin/hook-agent.mjs" }],
    "afterAgentResponse": [{ "command": "/chemin/absolu/node /chemin/absolu/.conforms/bin/hook-agent.mjs" }],
    "stop": [{ "command": "/chemin/absolu/node /chemin/absolu/.conforms/bin/hook-agent.mjs" }],
    "sessionEnd": [{ "command": "/chemin/absolu/node /chemin/absolu/.conforms/bin/hook-agent.mjs" }]
  }
}
```

Puis, après la confirmation, activer l'envoi quotidien. Variable facultative : `CONFORMS_AGENT_ID` (défaut `claude-code` ou `cursor`).

## Recette : Hermes

Le point d'accroche documenté `post_llm_call` de Hermes se déclenche une fois par tour terminé avec une réponse, avec la demande, la réponse finale et le modèle. Le plugin de référence écrit chaque tour dans la file, en temps réel : rien n'est perdu à la compaction des sessions. Les tours interrompus ne sont pas captés. Limites connues (Hermes 0.20.6) :

- les relectures internes de Hermes (mémoire, compétences) et les passes du curator passent par le même point d'accroche : le plugin les reconnaît et les ignore ;
- **une partie seulement des échecs d'appel au modèle est captée**, avec le message de Hermes à la place de la réponse : ceux dont le texte est connu (réponse vide, « ⚠️ No reply: … », raisonnement sans réponse, erreur interne « I apologize, but I encountered … ») sont enregistrés en `error`. Ce classement repose sur le texte et reste **indicatif** : un autre échec peut apparaître en `completion`. Une erreur d'API renvoyée directement, un contexte dépassé ou une réponse tronquée ne passent pas par ce point d'accroche et ne sont pas enregistrés ;
- les tours des sous-agents sont enregistrés, leur consigne tenant lieu de demande ;
- avec le moteur facultatif `codex_app_server`, aucun tour n'est capté ;
- Hermes saute le point d'accroche d'un tour qui se termine pendant qu'il traite le précédent (tours simultanés de la passerelle) : rare, mais ce tour n'est pas enregistré.

Les fichiers `request_dump` de Hermes, eux, ne sont pas une source complète : ils ne sont écrits qu'en cas d'erreur d'API, sauf activation de `HERMES_DUMP_REQUESTS`.

Installation dans le dossier du profil Hermes à journaliser : **demander à l'humain lequel**. Le profil `default` est `~/.hermes` ; un profil nommé est `~/.hermes/profiles/<profil>` (le profil actif, s'il y en a un, figure dans `~/.hermes/active_profile`). Toujours préciser le profil avec `-p` : sans lui, la commande vise le profil actif.

```sh
P=default    # ou le nom du profil choisi
H=~/.hermes; [ "$P" = default ] || H=~/.hermes/profiles/$P
mkdir -p "$H/plugins/conforms-journal"
curl -fsSLo "$H/plugins/conforms-journal/__init__.py" https://conforms.fr/connexion-agent/hermes_plugin.py
curl -fsSLo "$H/plugins/conforms-journal/plugin.yaml" https://conforms.fr/connexion-agent/hermes_plugin.yaml
hermes -p "$P" plugins enable conforms-journal
```

Les plugins de Hermes sont désactivés par défaut : `hermes -p <profil> plugins enable conforms-journal` l'active (équivalent : ajouter `conforms-journal` à `plugins.enabled` dans `config.yaml` du profil). Le plugin n'est chargé qu'au démarrage : **l'humain redémarre ensuite Hermes lui-même**, hors de la conversation — passerelle : `hermes -p <profil> gateway restart` depuis un autre terminal ; en ligne de commande : quitter, puis relancer (`hermes -p <profil> -c` reprend la dernière session). Un agent Hermes ne peut pas se redémarrer lui-même. Le plugin lit `CONFORMS_SPOOL` et `CONFORMS_AGENT_ID` (défaut `hermes`) dans l'environnement de Hermes. Il n'accède jamais au réseau et aucune erreur ne remonte dans Hermes.

Puis, après la confirmation, activer l'envoi quotidien.

## Recette : n8n

Dans une **branche parallèle** du workflow, jamais comme dernier nœud d'un workflow qui répond à un appelant (déclencheur de chat ou webhook en mode « When Last Node Finishes ») : la réponse de Conforms remplacerait celle de l'agent.

1. Un nœud **Code**, mode « Run Once for All Items », avec le contenu de `n8n-code.txt` : il produit un seul corps JSONL (`corps`) pour tous les éléments. Adapter si besoin les champs lus (`input`, `output`, `model`).
2. Un nœud **HTTP Request** : méthode `POST`, l'adresse du prompt ; Authentication « Generic Credential Type » → « Header Auth », avec le nom `Authorization` et la valeur `Bearer <clé>`, **saisie par un humain dans l'identifiant n8n** ; Send Body activé, Body Content Type « Raw », Content Type `application/x-ndjson`, Body `{{ $json.corps }}`. Dans ses réglages : « On Error » sur « Continue » et « Retry On Fail » activé, pour qu'un refus ne fasse jamais échouer le workflow.

Un envoi par exécution, sans file locale : chaque exécution crée un upload, et une analyse si le système est en parcours automatique. Réserver cette recette aux workflows peu fréquents (quelques exécutions par jour) ; sinon, regrouper les éléments dans un workflow planifié quotidien, ou désactiver le parcours automatique du système concerné. Budget partagé : 60 envois par heure pour l'organisation. Exécuter une fois en test, faire confirmer, puis seulement **publier** le workflow : jusque-là, les modifications restent en brouillon et les exécutions de production utilisent la version publiée. Sur une version de n8n sans publication, travailler sur une copie désactivée du workflow jusqu'à la confirmation.

## Recette : application SDK

Journaliser après la réponse complète (après `finalMessage()` en flux), une seule fois par appel : pas dans un intercepteur HTTP, qui voit aussi les nouvelles tentatives.

```js
import { journaliserAnthropic, journaliserOpenAI } from "./journal-sdk.mjs";

const message = await anthropic.messages.create({ model, max_tokens, messages });
journaliserAnthropic(message, { agentId: "assistant-support", sessionId, input: question });

const completion = await openai.chat.completions.create({ model, messages });
journaliserOpenAI(completion, { agentId: "assistant-support", sessionId, input: question });
```

```python
from journal_sdk import journaliser_anthropic

message = client.messages.create(model=model, max_tokens=1024, messages=messages)
journaliser_anthropic(message, agent_id="assistant-support", session_id=session_id, input=question)
```

Pour tout autre fournisseur : `journaliser({ agentId, sessionId, input, output, model })`. Ces modules ne lèvent jamais d'erreur dans l'application (un échec d'écriture est signalé sur la sortie d'erreur). Puis, après la confirmation, activer l'envoi quotidien.

## Vérifier et dépanner

- `node envoyer.mjs --verifier` : envoie un corps vide ; la réponse « Corps vide » prouve que la clé et l'adresse sont acceptées. Aucun journal n'est transmis.
- `node envoyer.mjs` affiche l'`uploadId` de chaque envoi accepté : le transmettre à l'utilisateur pour la confirmation.
- Codes de sortie : `0` (envoyé, ou reporté au passage suivant sur `429`, `5xx` ou réseau), `1` (configuration à corriger : clé, droits du fichier de clé, adresse, redirection, `400` sur le système, `401`, `403`, `409`, ou « Réponse inattendue » : une réponse qui ne vient pas de l'API, d'un proxy ou d'un filtrage par exemple ; ou encore un fichier illisible dans la file, laissé en place, les autres suivant les règles habituelles ; rien n'est alors supprimé).
- Après un report, le passage quotidien suivant ne renvoie que les parties déjà tentées (celles qu'il peut lire) ; les lignes nouvelles partent le lendemain, en un seul envoi.
- Un seul passage à la fois par file : un second lancement simultané s'arrête aussitôt (« Un autre passage est en cours », code 1 à la main, sans bruit pour la tâche horaire). Un passage interrompu rend la main ; seul un arrêt brutal (`kill -9`) laisse le verrou, repris au bout d'une heure.
- Tâche horaire : `ls -l ~/.conforms/journal/.dernier-passage` donne la date du dernier passage abouti, et le journal de la tâche (`envoi.log`) le détail de chaque passage.

## Arrêter la connexion

1. Retirer les hooks des réglages (Claude Code, Cursor), le plugin Hermes (`hermes -p <profil> plugins disable conforms-journal`, puis supprimer son dossier), la ligne de la crontab, les nœuds n8n ou les appels `journaliser`.
2. Supprimer les données locales : `rm -rf ~/.conforms/journal` et le fichier de clé.
3. Révoquer la clé dans Conforms (Paramètres → Surveillance continue → Gérer les clés d'API).

Sources des procédures d'outils tiers, vérifiées le 2026-09-26 :

- https://code.claude.com/docs/en/hooks
- https://cursor.com/docs/hooks
- https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.httprequest/
- https://docs.n8n.io/integrations/builtin/credentials/httprequest/
- https://docs.n8n.io/build/understand-workflows/save-and-publish-workflows
- https://hermes-agent.nousresearch.com/docs/user-guide/features/hooks
- https://hermes-agent.nousresearch.com/docs/user-guide/features/plugins
- https://platform.claude.com/docs/en/api/sdks/typescript
- https://github.com/openai/openai-node
