
Pistes d'audit inviolables pour agents d'IA : Runtime Records chaînés par hachage, sans dépendance, vérifiables par tous.
# halo-record
**Pistes d'audit inviolables pour les agents IA** — des Runtime Records chaînés par hachage, restitués sous forme de Runtime Report que vos clients peuvent vérifier eux-mêmes.
Chaque action capturée par votre instrumentation (appels d'outils, appels de modèles, accès aux données, approbations) devient un **Runtime Record** dans un journal en ajout seul, chaîné par hachage ; le **Runtime Report** est cette chaîne restituée sous forme de page HTML qui recalcule la chaîne de hachage dans le navigateur du lecteur et affiche ce qu'elle présente à partir des enregistrements qu'elle a vérifiés (pour une vérification qui ne dépend pas du code de la page elle-même, exécutez `halo verify` sur le JSONL). Toute partie détenant un point de contrôle de la chaîne peut vérifier que les enregistrements jusqu'à ce point de contrôle n'ont pas été altérés depuis qu'il a été pris, sans faire confiance à celui qui les a produits — ce point de contrôle est la pièce maîtresse : la chaîne seule est inviolable face à tous, sauf face à la partie qui exploite l'enregistreur ([LIMITS.md §1](https://github.com/bkuan001/halo-record/blob/main/LIMITS.md)). Quand l'équipe sécurité d'un client demande « qu'a fait votre agent avec nos données ? », vous lui remettez un lien au lieu d'un paragraphe. Les revues de sécurité posent déjà des questions sur l'IA à côté de la checklist SOC 2 — et de plus en plus, ces questions proviennent de l'ISO 42001, des articles de l'EU AI Act sur la tenue de registres, et des questionnaires propres aux clients. Aujourd'hui, une assurance écrite passe encore. Le pari derrière ce projet, c'est que cela ne durera pas longtemps.
**Présenté dans [Help Net Security](https://www.helpnetsecurity.com/2026/08/31/halo-record-open-source-ai-agent-audit-trail/)** (août 2026).
Le format d'enregistrement est ouvert et libre d'implémentation. Ce paquet est l'implémentation de référence : enregistreur, vérificateur, client témoin et serveur de rapports.
> **Vous utilisez halo-record, ou vous y pensez ?** Dites-moi qui vous êtes et pour quoi faire → [Qui utilise halo-record ?](https://github.com/bkuan001/halo-record/discussions/7)
## Vérifiez par vous-même
On vous demande d'installer un enregistreur à l'intérieur de votre agent. Vous ne devriez pas accepter cela sur parole :
- **Zéro dépendance à l'exécution.** Bibliothèque standard uniquement. `pip install halo-record` installe exactement un paquet.
- **Aucun appel réseau**, sauf quatre optionnels — l'ancrage à un témoin (envoie l'identifiant du sujet, un nombre d'enregistrements et deux empreintes de chaîne — la tête et la racine de chaîne), la relecture des points de contrôle d'un témoin (envoie l'identifiant du sujet), l'horodatage RFC 3161 (envoie uniquement le hash d'état d'un point de contrôle à une Timestamp Authority), et `halo serve --verify-email` (envoie un code à usage unique et le nom du sujet du rapport, par SMTP via le serveur que vous configurez dans `$HALO_SMTP_HOST`, à l'adresse saisie par un lecteur du rapport). Tous sont désactivés sauf si vous les invoquez ; aucun autre contenu d'enregistrement ne quitte votre infrastructure.
- **Les arguments bruts des outils sont hachés, avec un résumé expurgé en parallèle.** Les arguments sont stockés sous forme de hash canonique plus un résumé : le texte des arguments avec les motifs connus de secrets et de données personnelles masqués, limité à 200 caractères. Une entrée courte qui ne correspond à aucun motif apparaît intégralement dans le résumé ; le mode hash uniquement (`summaries=False`) ne conserve aucun résumé. L'expurgation est au mieux (regex sur les formats courants de secrets et de données personnelles plus un attrape-tout par entropie) : considérez-la comme une défense en profondeur, pas comme une garantie. Les champs de résultat que vous fournissez au-delà de `summary` sont analysés et masqués avec les mêmes motifs, mais ne sont pas tronqués à 200 caractères (LIMITS §13).
- **Assez petit pour être audité.** ~8 700 lignes de Python, y compris le JavaScript et le CSS intégrés à la page de rapport (lignes de code, sans compter les lignes vides, les commentaires et les docstrings). Lisez tout en une journée.
- **Apache-2.0.**
- **La documentation est de premier ordre.** [LIMITS.md](https://github.com/bkuan001/halo-record/blob/main/LIMITS.md) (ce que la chaîne ne peut pas prouver), [PRIVACY.md](https://github.com/bkuan001/halo-record/blob/main/PRIVACY.md) (ce que contiennent les enregistrements et ce qui quitte votre machine), [RETENTION.md](https://github.com/bkuan001/halo-record/blob/main/RETENTION.md) (fonctionner sous une politique de rétention), et [REVIEWERS.md](https://github.com/bkuan001/halo-record/blob/main/REVIEWERS.md) — la vérification indépendante en quatre commandes plus un format de citation pour les conclusions de revue.
Ce que chaque couche prouve — la distinction porteuse dans ce projet ([LIMITS.md §1](https://github.com/bkuan001/halo-record/blob/main/LIMITS.md)) : une chaîne que vous détenez vous-même prouve que les enregistrements n'ont pas été **modifiés**, par rapport à une tête que quelqu'un détient déjà ; seuls des points de contrôle détenus en dehors de l'opérateur prouvent qu'aucun n'a été **supprimé** ; et aucun hash ne prouve que chaque action a été **capturée**.
| Affirmation | Chaîne auto-détenue | + Points de contrôle externes | + Capture de confiance |
|---|---|---|---|
| Détecter les modifications d'un artefact établi | ✔ | ✔ | ✔ |
| Détecter la réécriture d'un historique validé | — | ✔ | ✔ |
| Détecter les points de contrôle manquants/tardifs | — | ✔ (cadence convenue) | ✔ |
| Prouver que chaque action a été enregistrée | — | — | dépend de la frontière de capture |
**Voyez-en un avant d'installer :** [un exemple de Runtime Report](https://bkuan001.github.io/halo-record/sample-report.html) — données fictives, chaîne réelle, et son script recalcule dans votre navigateur la chaîne de hachage des enregistrements intégrés lorsque vous l'ouvrez.
## Démo en 60 secondes
Aucun agent requis. Avec [uv](https://docs.astral.sh/uv/), rien à installer :
```
uvx --from halo-record halo demo --serve
```
ou de la manière classique :
```
pip install halo-record
halo demo --serve
```
Soit on échafaude un fournisseur fictif d'agents de support avec deux clients, on assiste aux chaînes (avec un fichier témoin local tenant lieu de témoin extérieur à l'opérateur — voir [LIMITS.md §1](https://github.com/bkuan001/halo-record/blob/main/LIMITS.md)), on sert leurs Runtime Reports à accès restreint, et on ouvre la console de l'opérateur dans votre navigateur. Essayez ensuite le test d'altération : supprimez une ligne de l'un des fichiers `.jsonl` et rechargez. Le rapport le détecte.
## Enregistrez votre propre agent
Une ligne à la frontière :
```python
from halo_record import trace
agent = trace(run_my_agent, profile="my-agent", log="audit.jsonl") # wraps your entrypoint; records the run boundary to ./audit.jsonl — add record_call() or a framework adapter at each tool boundary to capture individual calls
```
Un shim de commodité `from halo import ...` est également fourni — mais le nom `halo` sur PyPI appartient à un paquet de terminal-spinner sans rapport, et si ce paquet est installé, c'est lui qui remporte l'import. `halo_record` est sans ambiguïté, donc les exemples l'utilisent.
Sans `log=`, les enregistrements vont dans `~/.halo/my-agent.jsonl` (une chaîne par agent). Le wrapper scelle la frontière d'exécution ; les preuves résident dans les enregistrements par appel. Capturez-les avec un adaptateur de framework (matrice ci-dessous) — ou explicitement, ce qui montre aussi comment les liens de délégation fonctionnent :
```python
from halo_record import Recorder, record_call
rec = Recorder("audit.jsonl")
with record_call(rec, "crm.lookup", {"account": "acct-9"}) as call: # one sealed record per tool call
call.result = crm.lookup("acct-9")
with record_call(rec, "payments.refund", {"amount": 120},
parent_id=rec.last_record_id()) as call: # child links to the action that spawned it
call.result = payments.refund(120)
```
Ensuite, générez le rapport :
```
halo report audit.jsonl -o report.html # one chain -> HTML that re-verifies in the browser
halo serve ./records --port 8721 # all tenants, gated per customer
```
Mise à niveau d'un répertoire initialement servi par 0.2.45 ? `halo serve` relit chaque autorisation antérieure au démarrage. Une autorisation sur un fichier dont les enregistrements ne portent aucun identifiant de sujet (un fichier `Recorder` simple tel que `agent.jsonl`) est mise en quarantaine et affichée à chaque démarrage avec la commande `halo grant` qui la réémet — voir [LIMITS.md §17, « Serving a directory first served by 0.2.45 »](https://github.com/bkuan001/halo-record/blob/main/LIMITS.md#17-one-chain-file-per-subject).
Le démarrage rapide se termine lorsque vous consultez le rapport d'exécution de votre propre agent dans un navigateur. Si vous avez obtenu un fichier JSONL et aucun rapport, quelque chose ne va pas : ouvrez un ticket.
### Le bloc de vérification
Si une couche de garde-fou ou de politique a vérifié l'action, son verdict peut accompagner l'enregistrement — un bloc optionnel consignant ce que la barrière a décidé, scellé dans la chaîne de hachage comme tout autre champ :
```python
from halo_record import build
build("tool_call", "security", tool="payments.refund",
verification={"status": "allowed", "verifier": "gate/1.2",
"policy_ref": "sha256:1f3a...",
"checked_at": "2026-08-01T12:00:00Z"})
```
Une note de nommage : le package exporte une *fonction* `record` (le décorateur), qui masque le *module* `halo_record.record` sur l'objet package. Importez directement depuis le chemin du module lorsque vous voulez ses éléments internes — `from halo_record.record import build` — plutôt que `import halo_record.record as record`.
qui scelle dans l'enregistrement comme :
```json
"verification": {"status": "allowed", "verifier": "gate/1.2", "policy_ref": "sha256:1f3a...", "checked_at": "2026-08-01T12:00:00Z"}
```
`record_call(...)` accepte le même mot-clé `verification=`. `status` est requis dans le bloc ; `verifier`, `policy_ref` et `checked_at` sont facultatifs. Ce que chaque statut signifie :
| Statut | Ce que le gate rapporte | L'action a-t-elle été exécutée ? |
|---|---|---|
| `allowed` | il a autorisé l'action | oui — l'action a été exécutée |
| `blocked` | il a refusé l'action | déterminé par l'intégration, pas par ce champ — un enregistrement peut toujours porter un résultat, et un blocage ne prouve pas à lui seul la non-exécution |
| `modified` | il a modifié l'action avant exécution — `action.input` décrit l'action **telle qu'exécutée**, après modification | oui, sous la forme modifiée |
| `unverified` | il s'est exécuté (ou a été consulté) mais n'a pris aucune décision — distinct d'un bloc absent, qui signifie qu'aucune revendication de vérification n'a été faite du tout | oui — l'action a été exécutée sans verdict |
Le bloc est fourni par le code d'intégration de l'opérateur et enregistre ce qu'il rapporte que le gate a dit — la même posture de confiance que `principal` (voir [LIMITS](https://github.com/bkuan001/halo-record/blob/main/LIMITS.md#11-verification-status-is-the-gates-report-not-halos-finding)). Le scellement prouve que le statut n'a pas été modifié après coup ; il ne prouve pas que la vérification a eu lieu, que le verdict était correct, ou qu'une action bloquée n'a pas été exécutée. Ce n'est pas une vérification indépendante.
Pour que `policy_ref` soit utilisable comme preuve, utilisez un hash de contenu du jeu de règles et conservez l'artefact du jeu de règles — une étiquette non résolvable rend le champ décoratif.
## Connectez-vous à ce que vous exécutez déjà
| Capturé à la frontière | Ingéré depuis la télémétrie existante |
|---|---|
| Enregistreur natif (`from halo_record import trace`) | Spans OpenTelemetry GenAI |
| Intercepteur MCP | Callbacks LiteLLM |
| Callback LangChain / LangGraph | Export Langfuse |
| Hooks OpenAI Agents SDK | Tout journal de gateway / reverse-proxy |
| Hook Claude Agent SDK | Hooks `PostToolUse` de Claude Code et Codex CLI (déclenchés après l'exécution de l'outil) |
Les adaptateurs de framework et les chemins d'ingestion marquent chaque enregistrement avec un tag `source`, de sorte que le rapport divulgue comment chaque élément de preuve a été collecté. Les enregistrements capturés et ingérés vivent dans la même chaîne.
Pour LangChain / LangGraph, il s'agit d'un gestionnaire de callback :
```python
from halo_record import Recorder
from halo_record.integrations.langchain import HaloCallbackHandler
recorder = Recorder("audit.jsonl")
result = my_chain.invoke(inputs, config={"callbacks": [HaloCallbackHandler(recorder)]}) # every tool call becomes a record
```
Pour MCP, un appel encapsule la session client — et ensuite *tout* agent utilisant MCP émet des enregistrements pour chaque appel d'outil, quel que soit le framework qui le pilote :
```python
from mcp import Client # mcp 2.x
from halo_record import Recorder
from halo_record.integrations.mcp import instrument_client_session
async def run(server): # an MCPServer, a server URL, or stdio parameters
async with Client(server) as client:
instrument_client_session(client, Recorder("audit.jsonl"), server="stripe")
await client.call_tool("charge", {"customer": "cus_123", "amount": 500}) # recorded
```
Chaque `call_tool()` sur le client enveloppé est enregistré, et un outil qui renvoie un résultat d'erreur est scellé comme `error` (l'adaptateur lit à la fois `isError` et `is_error` de mcp 2.x). Sur mcp 1.x, passez le `ClientSession` initialisé au lieu de `Client` ; l'enveloppe est identique.
Pour les journaux de passerelle ou de proxy (Cloudflare AI Gateway, Portkey, nginx devant le modèle), mappez une ligne de journal dans la chaîne — explicitement étiquetée comme ingérée, et non capturée à la frontière :
```python
from halo_record.integrations.gateway import record_log
record_log(Recorder("audit.jsonl"), {"tool": "gen_ai:gpt-4o", "model": "gpt-4o", "status": 200, "subject": "acme-corp"})
```
Tout ce qui émet des spans OpenTelemetry GenAI (CrewAI, LlamaIndex, et la plupart des frameworks d'agents avec instrumentation OTel) arrive dans la chaîne via l'adaptateur OTel, et le [package TypeScript](https://github.com/bkuan001/halo-record-ts) fournit des adaptateurs natifs pour le Vercel AI SDK et l'écosystème d'agents JS. Il manque un adaptateur pour votre stack ? Ouvrez une issue. La plupart des adaptateurs font environ une centaine de lignes.
## Enregistrez votre agent de codage (Claude Code ou Codex)
Claude Code déclenche un hook `PostToolUse` après chaque appel d'outil. Pointez-le vers `halo hook` et chaque action — écritures de fichiers, commandes shell, appels de connecteurs MCP — devient un enregistrement dans une chaîne locale. Aucune modification de code ; une seule entrée de configuration :
```json
{
"hooks": {
"PostToolUse": [
{"matcher": "*", "hooks": [{"type": "command", "command": "halo hook"}]}
]
}
}
```
Ajoutez ceci à `~/.claude/settings.json` et les enregistrements arrivent dans `~/.halo/audit.jsonl` (remplaçable avec `$HALO_LOG`). Les outils de pure orchestration qui ne touchent à aucune donnée, réseau ou état externe sont ignorés — la chaîne enregistre les actions franchissant les frontières de confiance, pas la réflexion. Définissez `HALO_HASH_ONLY=1` pour enregistrer les hachages de contenu sans résumés. Définissez `HALO_AGENT_VERSION` (et éventuellement `HALO_AGENT_MODEL`) pour lier chaque enregistrement à la version de l'agent qui l'a produit — lorsqu'un auditeur s'interroge sur la version qui était en cours d'exécution dans une fenêtre donnée, l'export répond par colonne plutôt que de mémoire.
Codex CLI embarque les mêmes hooks de cycle de vie avec la même forme d'événement (les hooks sont activés par défaut). Ajoutez ceci à `~/.codex/hooks.json` et les commandes shell de Codex, les modifications `apply_patch` et les appels MCP arrivent dans la même chaîne :
```json
{
"hooks": {
"PostToolUse": [
{"matcher": ".*", "hooks": [{"type": "command", "command": "halo hook"}]}
]
}
}
```
Le hook distingue les deux à partir de l'événement lui-même (Codex ajoute `turn_id` et `model`) et étiquette chaque enregistrement `claude-code` ou `codex` ; définissez `HALO_HOOK_AGENT` pour en forcer un. Les deux relèvent du niveau ingéré : un hook `PostToolUse` se déclenche après l'exécution de l'outil, donc l'enregistrement est construit à partir de ce que le harness rapporte.
Si vous avez besoin que le rapport réponde à « sous quelles règles cette exécution a-t-elle eu lieu ? », définissez `HALO_AUTHORITY_FILE` sur un instantané JSON de l'autorité effective pour la session. Gardez-le respectueux de la vie privée : des hachages et des références, pas d'invites brutes, de texte de politique privée, de secrets ou de schémas d'outils complets — les formats connus de secrets et de données personnelles sont masqués au moment du scellement, mais les hachages et les références passent tels quels et le texte libre n'est pas détecté (voir LIMITES §6). Réutilisez un `snapshot_id` uniquement tant que l'autorité sous-jacente est inchangée ; les enregistrements consécutifs avec le même id et un contenu inchangé sont compactés — un id réutilisé sur un contenu modifié est stocké intégralement, avec un avertissement.
```json
{
"snapshot_id": "auth_2026_07_08T1100Z",
"captured_at": "2026-07-08T11:00:00Z",
"scope": "session",
"workspace": {"path_hash": "sha256:...", "git_commit": "abc1234"},
"refs": [
{"kind": "project_rules", "id": "CLAUDE.md", "hash": "sha256:...", "loaded": true, "truncated": false},
{"kind": "mcp_tool_registry", "id": "filesystem", "hash": "sha256:..."}
],
"omissions": [{"kind": "private_policy", "reason": "customer_secret", "hash": "sha256:..."}],
"stale_if": ["project_rules_hash_changed", "mcp_tool_registry_hash_changed"]
}
```
```sh
HALO_AUTHORITY_FILE=./authority.json halo hook
```
Le snapshot est scellé dans la même chaîne de hachage que les enregistrements d'actions. Une bonne valeur par défaut est un snapshot au niveau de la session au démarrage, plus un nouveau snapshot lorsque les règles, les Skills, les hooks, les registres d'outils MCP ou la politique de compaction changent. Pour garder les sessions longues légères, les enregistrements consécutifs ayant le même `authority.snapshot_id` sont compactés après le premier snapshot complet : les enregistrements suivants ne conservent que `{"snapshot_id": "...", "same_as_previous": true}`. Le pointeur reste chaîné par hachage, mais le bloc volumineux refs/omissions/stale-if n'est pas répété à chaque action. (La compaction est propre au processus d'enregistrement : la capture de style hook qui lance un processus par appel d'outil réenregistre le corps complet chaque fois que le snapshot complet précédent n'est pas l'enregistrement de queue, de sorte que les processus de courte durée échangent la taille de la chaîne contre la protection de réutilisation.)
Les utilisateurs du SDK attachent le même bloc directement — `build(..., authority={...})` ou `record_call(..., authority={...})` ; la capture par hachage seul utilise la même interface (`summaries=False` sur l'un ou l'autre) :
```python
from halo_record import Recorder, record_call
rec = Recorder("audit.jsonl")
with record_call(rec, "crm.lookup", {"account": "acct-9"},
authority={"snapshot_id": "auth_1", "rules_hash": "sha256:..."},
summaries=False) as call: # hash-only: no summaries, no excerpts
call.result = crm.lookup("acct-9")
```
Ensuite, comme d'habitude :
```
halo verify ~/.halo/audit.jsonl
halo report ~/.halo/audit.jsonl -o report.html
```
Tout runtime d'agent qui expose un hook post-action peut alimenter la même commande — le hook lit un événement en JSON sur stdin et ajoute un enregistrement.
Une chaîne, un seul écrivain à la fois. Une chaîne est une liste chaînée : deux écrivains qui lisent la même tête et ajoutent tous les deux vont la bifurquer (deux enregistrements revendiquant le même prédécesseur), et la vérification nommera les enregistrements concernés. `Recorder` sérialise ses propres ajouts avec un verrou sidecar (POSIX `flock` ici ; un répertoire de verrou dans le paquet TypeScript), et `halo hook` ajoute via `Recorder`, donc la configuration du hook ci-dessus est couverte. Tout ce qui écrit directement dans le fichier de chaîne — un hook artisanal, des workers parallèles, un expéditeur de logs — doit détenir un verrou exclusif équivalent pendant toute la séquence lecture-de-la-tête-puis-ajout, ou écrire dans des chaînes par processus. [LIMITS.md section 9](https://github.com/bkuan001/halo-record/blob/main/LIMITS.md#9-single-writer-chains) couvre cela en détail, y compris la frontière inter-langages.
## Quand l'enregistrement échoue
Les enregistrements sont écrits après l'exécution de l'action. Halo ne bloque pas une action lorsque son enregistrement ne peut pas être écrit ; écrire l'enregistrement avant l'action n'est pas proposé.
- **Par défaut : le résultat de l'action passe tel quel.** Si un enregistrement ne peut pas être écrit (disque plein, permissions, expiration d'un verrou), `trace()`, `record_call`, `@record`, les adaptateurs de framework (OpenAI Agents, LangChain, LiteLLM, OpenTelemetry), le wrapper MCP et `halo hook` renvoient le résultat propre de l'action inchangé, ou relancent l'exception propre de l'action si elle a échoué. Un avertissement sur stderr nomme l'enregistrement manquant : `halo-record: FAILED to record '<tool>' (<error>); the action ran, but its record is NOT in the evidence log. ...` (sans stderr utilisable, il va dans le logger `halo_record`). Le handler LangChain et l'exportateur OTel comptent aussi les pertes (`lost_records`). Rien dans la chaîne ne peut montrer un enregistrement qui n'a jamais été écrit, donc les enregistrements restants se vérifient toujours. Un relecteur qui surveille la cadence des points de contrôle témoins voit une chaîne qui s'est bloquée ; un seul enregistrement manquant entre des enregistrements ultérieurs n'apparaît que dans la ligne stderr.
- **Le mode strict lève aussi.** Passez `strict_recording=True` (à `trace`, `record_call`, `@record`, `instrument_client_session`, ou au constructeur d'un adaptateur), ou définissez `HALO_STRICT_RECORDING=1`, et la même ligne stderr est écrite, puis l'erreur d'enregistrement est levée. Elle atteint l'appelant depuis `trace`, `record_call`, `@record`, le wrapper MCP, les hooks OpenAI Agents et le handler LangChain (qui définit le `raise_error` de LangChain). Elle ne peut pas atteindre l'appelant depuis l'exportateur OTel ou le logger LiteLLM : ces frameworks interceptent et journalisent ce qu'un exportateur ou un callback lève, donc là la ligne stderr est le signal dans les deux modes (LIMITS.md section 18). L'action s'est déjà exécutée lorsque l'erreur est levée, donc un appelant qui la traite comme « l'action a échoué » et réessaie répétera les effets de bord de l'action (un remboursement émis deux fois). N'utilisez le mode strict que là où un enregistrement manquant est pire que ce risque.
- `record_call` et `@record` sans recorder (aucun passé, aucun lié par `trace`) lèvent `RuntimeError` avant l'exécution du code enveloppé.
- `@record` et `trace()` acceptent les fonctions `async def`. Le wrapper est lui-même `async def` : il attend la fonction, puis enregistre ce qu'elle a renvoyé ou levé (une annulation est enregistrée comme une erreur). `trace()` garde son recorder lié pendant que le corps est attendu, donc un outil `record_call` ou `@record` à l'intérieur d'un agent asynchrone atterrit sur la chaîne tracée, et les agents concurrents gardent chacun la leur. Une fonction simple qui renvoie un awaitable est enregistrée lorsqu'elle renvoie. Une fonction génératrice asynchrone est enregistrée lorsqu'elle est appelée, avec l'objet générateur comme résultat ; ce qu'elle produit n'est pas enregistré.
## Intégrité vs. complétude (lisez cette partie)
Soyez précis sur ce que chaque couche prouve — car ce sont des affirmations différentes, et les différences sont le point essentiel :
Une chaîne auto-détenue prouve une **intégrité relative à une tête établie** : étant donné une tête de chaîne que quelqu'un détient déjà, toute modification, réordonnancement ou suppression dans les enregistrements derrière elle devient détectable. En soi — avant que quiconque en dehors de l'opérateur ait vu une tête — une chaîne prouve la cohérence interne, pas l'historique : un opérateur pourrait supprimer un enregistrement et re-sceller, et le nouveau fichier se vérifierait. La chaîne devient **historiquement engagée** au moment où sa tête quitte le contrôle de l'opérateur.
C'est là le témoin : une partie extérieure à l'opérateur détenant des points de contrôle périodiques de la chaîne — l'identifiant du sujet, un nombre d'enregistrements, et deux empreintes de chaîne (la tête et la racine de chaîne) ; la charge utile exacte, et rien d'autre. Les points de contrôle rendent détectable la réécriture d'un historique engagé, et un point de contrôle manqué est lui-même un événement visible :
```
halo anchor audit.jsonl witness.jsonl # anchor a checkpoint to a local witness
halo anchor audit.jsonl witness.jsonl --check # completeness verdict against it
```
Pour *time* spécifiquement, un horodatage externe RFC 3161 remplace l'horloge auto-déclarée du point de contrôle par une preuve provenant d'une Autorité d'Horodatage hors du contrôle de l'opérateur — « cette chaîne a atteint cette tête au plus tard à T », vérifiable par un tiers sans infrastructure hébergée. L'ATS par défaut est le gratuit freetsa.org (acceptable pour l'évaluation) ; pour la production, pointez `--tsa` vers une ATS hors du contrôle de l'opérateur (par ex. DigiCert / Sectigo, ou celle désignée par une partie s'appuyant sur le service) :
```
halo anchor audit.jsonl witness.jsonl --timestamp # attach a TSA time proof to the checkpoint
halo anchor audit.jsonl witness.jsonl --check # reads the token's claimed time
```
`--check` confirme que le jeton est lié à cet état de chaîne et lit son horodatage attesté, mais il ne valide **pas** la signature du TSA — cela est délibérément laissé à un outil standard afin qu'un relecteur ne fasse confiance à aucun de nos codes. Pour vérifier l'heure de manière indépendante (c'est ce que vous remettez à un relecteur de sécurité) :
```
# tsa.token_b64 lives in the witness log; decode the latest one to a standard .tsr file
python3 -c 'import json,base64; cps=[json.loads(l) for l in open("witness.jsonl") if l.strip()]; t=[c["tsa"] for c in cps if c.get("tsa")][-1]; open("token.tsr","wb").write(base64.b64decode(t["token_b64"])); print(t["digest"])'
curl -s -o tsa-ca.pem https://freetsa.org/files/cacert.pem # CA for the default TSA (a commercial TSA publishes its own)
openssl ts -verify -digest <the digest printed above> -in token.tsr -CAfile tsa-ca.pem # → "Verification: OK"
```
Une limite de plus, énoncée clairement : ni la chaîne ni le témoin ne prouvent que chaque action du monde réel est passée par l'enregistreur. C'est la **complétude de capture** — une propriété de l'endroit où l'enregistreur se situe dans la pile (instrumentation native, hooks, ingestion par passerelle), et non d'un quelconque hash. Les enregistrements portent une balise `source` précisément pour cette raison. Le tableau des affirmations sous « Vérifiez par vous-même » en haut de cette page est le résumé de ces trois couches.
N'importe qui peut exécuter un témoin. Un témoin que vous exécutez vous-même engage l'historique envers *vous* ; l'engager envers *votre client* exige un témoin auquel il a des raisons de faire confiance. Le protocole est ouvert dans les deux cas.
Un témoin hébergé et reconnu est la manière dont ce projet subviendra à ses besoins. Accès anticipé : [email protected].
## Données personnelles dans la chaîne
La chaîne est en ajout seul : tout ce qui est scellé dans un enregistrement y reste, car
le retirer briserait la vérification de tout ce qui suit. Les arguments des outils sont
déjà traités — stockés sous forme de hash plus un résumé expurgé limité à 200 caractères
(le mode hash-only ne conserve aucun résumé ; dans les champs id, il masque un jeton dans un format de secret connu comme dans les résumés et remplace tout autre jeton en forme de secret par `sha256:` plus 16 chiffres hexadécimaux — sauf `subject.id`, qui route l'enregistrement vers la chaîne de son tenant).
Notez la limite dans cette phrase : *expurgé*, pas supprimé. La section 6 de [LIMITS.md](https://github.com/bkuan001/halo-record/blob/main/LIMITS.md)
est explicite : un nom ou une adresse postale n'a pas de motif fiable, donc
ni l'un ni l'autre n'est détecté et ni l'un ni l'autre n'est masqué. Et `subject` n'est pas le seul champ qui porte du texte que vous fournissez — `principal`, `approver`, `session_id`, `agent`,
`authority`, `data` et les résumés le font tous.
Le motif qui fonctionne : placez un identifiant pseudonyme stable dans la chaîne et conservez la
correspondance vers toute personne dans un système que vous pouvez supprimer. Une demande d'effacement est
alors satisfaite en supprimant la correspondance. Gardez `subject` pointant vers l'organisation
locataire, pas vers une personne :
```python
from halo_record import build
build("tool_call", "privacy", subject={"id": "acme", "name": "Acme Corp"})
```
Aucun paramètre n'impose cela — c'est une discipline dans la façon dont vous appelez l'enregistreur.
Cela rend l'effacement traitable ; ce n'est pas de l'anonymisation, et il n'existe encore
aucune rétention ou élagage intégré.
[LIMITS.md section 13](https://github.com/bkuan001/halo-record/blob/main/LIMITS.md#13-personal-data-and-erasure) contient la liste complète des champs,
explique pourquoi l'empreinte de l'entrée stockée peut confirmer une valeur devinable même
après la disparition de la correspondance, et se termine par les questions qu'un relecteur devrait poser.
Enregistrer un appel de modèle (la première question de l'acheteur : « quel modèle a vu mes données ? ») :
```python
from halo_record import Recorder, record_model_call
rec = Recorder("audit.jsonl")
record_model_call(rec, provider="anthropic", model="claude-sonnet-4-6",
zdr=True, purpose="draft support reply",
subject="acme") # tool=model.generate, scope=model:anthropic
```
Le modèle est également scellé en tant que `agent.model` et les conditions de rétention en tant que `data.zdr`,
afin que la colonne modèle de l'export et les règles de politique puissent les lire, et les deux survivent
à `summaries=False`. Passez `messages=` et l'entrée de l'enregistrement indique combien il y en avait — un comptage, jamais leur contenu.
## Où cela se situe dans une pile de conformité
halo-record est une couche de preuve, pas une certification. Il produit l'artefact que les cadres d'évaluation ne cessent de réclamer en des termes différents. Une note de périmètre qui régit chaque point ci-dessous : ce sont des affirmations d'intégrité concernant l'enregistrement ; l'exhaustivité par rapport à l'opérateur nécessite un témoin externe détenant des points de contrôle ([LIMITS.md §1](https://github.com/bkuan001/halo-record/blob/main/LIMITS.md)).
- **Questionnaires de sécurité et revues SOC 2 :** répondez aux sections IA avec un Runtime Report vérifiable plutôt qu'avec des captures d'écran et de la prose.
---
[Read more](https://github.com/bkuan001/halo-record)