Mon coach sportif manipule des données de santé : fréquence cardiaque, charge d’entraînement, séances, ressenti. Par défaut, tout reste sur ma machine : le modèle de langage qui rédige les messages tourne en local. Mais je peux activer temporairement un modèle dans le cloud, plus capable, pour le chat principal. Pendant cette fenêtre, certaines de ces données pourraient sortir de chez moi pour être envoyées au fournisseur.

Je voulais que cette frontière soit explicite. Pas un if enfoui quelque part dans le code qui déciderait, au cas par cas, si telle donnée a le droit de partir. Une règle écrite à part, lisible, attachée à la donnée elle-même, et appliquée par un composant dédié avant chaque envoi. C’est exactement ce que permet ODRL.

ODRL en deux mots

ODRL (Open Digital Rights Language) est un langage du W3C pour exprimer des règles d’usage sur des ressources. Une politique y est un ensemble de règles de trois types : des permissions, des interdictions et des obligations. Chaque règle porte sur une action appliquée à une cible, sous certaines contraintes.

Les actions que j’utilise sont celles d’ODRL 2.2 : read (lire), use (utiliser), reproduce (reproduire) et distribute (transmettre). Les contraintes, elles, sont des triplets : un terme de gauche (leftOperand), un opérateur, un terme de droite (rightOperand). Par exemple « finalité égale coaching », ou « destinataire parmi {fournisseur cloud, modèle tiers} ».

ODRL ne fixe pas le vocabulaire métier ; il faut le définir. J’ai donc un petit profil, coach-odrl-v1, qui déclare les termes propres au coach :

  • des finalités : coaching, scoring, dataPortability, modelTraining, audit
  • des destinataires : dataSubject (la personne elle-même), cloudProvider, thirdPartyLLM, externalThirdParty
  • des lieux d’exécution : localRuntime, cloudRuntime.

Une politique concrète

Voici, simplifiée, la politique attachée à une donnée de santé qui doit rester locale (je l’appelle health_local_only). Les préfixes coach: et odrl: renvoient respectivement au vocabulaire du coach et à celui d’ODRL.

{
  "@type": "Set",
  "uid": "urn:coach:policy:health_local_only:…",
  "permission": [
    {
      "action": "read",
      "constraint": [
        {"leftOperand": "purpose", "operator": "isAnyOf",
         "rightOperand": ["coach:coaching", "coach:scoring"]},
        {"leftOperand": "virtualLocation", "operator": "eq",
         "rightOperand": "coach:localRuntime"}
      ]
    }
  ],
  "prohibition": [
    {
      "action": "distribute",
      "constraint": [
        {"leftOperand": "recipient", "operator": "isAnyOf",
         "rightOperand": ["coach:cloudProvider", "coach:thirdPartyLLM",
                          "coach:externalThirdParty"]}
      ]
    },
    {
      "action": "use",
      "constraint": [
        {"leftOperand": "purpose", "operator": "eq",
         "rightOperand": "coach:modelTraining"}
      ]
    }
  ]
}

Elle se lit directement. On a le droit de lire la donnée pour faire du coaching ou du scoring, tant qu’on reste en exécution locale. On n’a pas le droit de la transmettre à un fournisseur cloud ou à un modèle tiers, ni de l’utiliser pour entraîner un modèle. (J’ai retiré quelques règles pour la lisibilité ; la vraie politique en compte un peu plus.)

Quand le cloud est explicitement autorisé, une autre politique prend le relais, health_cloud_llm. Elle ajoute une permission : transmettre la donnée à un modèle tiers, mais seulement pour du coaching ou du scoring, et toujours pas pour de l’entraînement ni vers un tiers externe quelconque.

La règle voyage avec la donnée

Chaque donnée stockée par le coach est rangée dans une enveloppe qui, en plus du contenu, porte des métadonnées : d’où elle vient, sa catégorie, et la politique qui s’y applique. Une séance importée de Garmin ou saisie en conversation est classée comme donnée de santé, et reçoit par défaut la politique health_local_only.

Le choix de la politique n’est pas pris au moment de l’envoi, mais au moment de l’écriture. La donnée sait, dès sa naissance, ce qu’on a le droit d’en faire. C’est elle qui transporte sa règle, pas le code qui la manipule.

Une seule source de vérité

Les politiques ne sont pas écrites en dur dans le code. Elles viennent d’un fichier de modèles, et ce même fichier sert deux choses à la fois :

  • le programme, qui génère la politique concrète attachée à chaque donnée ;
  • la documentation publique : deux adresses qu’on peut ouvrir dans un navigateur, qui décrivent le vocabulaire et le profil.

Le but est qu’il n’y ait aucun écart entre ce que le système dit publiquement et ce qu’il applique réellement. Un test vérifie justement que les deux coïncident : si la doc publiée et la règle appliquée divergent, le test échoue. La cohérence n’est pas une promesse, c’est une vérification.

Décider avant d’envoyer

Reste à appliquer tout ça. C’est le rôle de deux composants classiques dans ce genre d’architecture : un point de décision (PDP, Policy Decision Point) qui tranche, et un point d’application (PEP, Policy Enforcement Point) qui pose la question au bon moment et obéit à la réponse.

Le PEP du coach est placé juste avant l’appel au modèle de langage. Il ne se déclenche que dans un cas précis : le modèle visé n’est pas local, et le contenu de la requête porte du contexte santé. Le reste du temps, il ne fait rien. Pas de coût inutile.

Quand il se déclenche, il construit une requête : « ai-je le droit de transmettre (distribute) cette donnée, pour la finalité coaching, vers un modèle tiers (thirdPartyLLM), en exécution cloud (cloudRuntime) ? »

{
  "target": "urn:coach:data:…",
  "action": "distribute",
  "context": {
    "purpose": "coach:coaching",
    "recipient": "coach:thirdPartyLLM",
    "virtualLocation": "coach:cloudRuntime"
  }
}

Il pose cette question au PDP, avec la politique de la donnée. La réponse a cette forme :

{
  "allowed": false,
  "reason": "prohibition_active",
  "active_permissions": [],
  "active_prohibitions": ["…:distribute"]
}

Avec health_local_only, cette requête tombe sur l’interdiction de transmettre vers un destinataire cloud : la décision est allowed: false, et l’envoi est bloqué. Avec health_cloud_llm (c’est-à-dire quand j’ai explicitement ouvert le cloud), la même requête trouve une permission correspondante et aucune interdiction : la décision passe à allowed: true.

La règle de décision est simple : c’est autorisé s’il existe au moins une permission active et aucune interdiction active. Une interdiction l’emporte toujours sur une permission.

Le moteur, et le doute

Deux détails comptent. D’abord, je ne réécris pas la logique d’ODRL moi-même : la décision est rendue par un moteur dédié (FORCE, et la bibliothèque odrl-evaluator), que j’appelle comme un petit service. Je lui passe la politique et la requête, il me renvoie la décision. Je n’ai pas à coder à la main « si telle action et tel destinataire alors… » ; c’est précisément ce que je voulais éviter.

Ensuite, ce moteur peut tomber en panne, mettre trop de temps à répondre, ou renvoyer quelque chose d’invalide. Dans tous ces cas, la décision par défaut est le refus, pas l’autorisation. Une panne du point de décision ne doit jamais transformer une donnée censée rester locale en donnée envoyée dans le cloud : en cas de doute, on ferme.

Conclusion

À l’échelle du coach, bloquer un envoi tiendrait en quelques lignes de code. Si j’ai choisi ODRL, ce n’est pas pour faire compliqué : c’est le langage dans lequel s’écrivent les règles d’usage au sein des dataspaces, ces espaces de partage de données où chaque participant garde la maîtrise de ce que les autres ont le droit de faire des siennes. Je prépare le coach à entrer dans un dataspace sport ; et comme il s’agit de données de santé, une règle d’usage standard, attachée à la donnée et lue de la même façon par tous les acteurs, n’est pas un luxe. C’est un sujet que je détaillerai dans un autre article.

En attendant, la même brique sert déjà à l’intérieur du coach. La règle est écrite une fois, dans un langage standard, attachée à la donnée, publiée telle qu’elle est appliquée, et évaluée par un moteur qui en connaît la sémantique. La frontière entre « ce qui reste chez moi » et « ce qui peut partir » n’est plus une condition perdue dans le code : c’est une politique qu’on peut lire, documenter et vérifier. Et tant que je n’ouvre pas explicitement le cloud, toute donnée de santé reste interdite de sortie, la moindre défaillance du contrôle se résolvant par un refus.