Aller au contenu principal

Licences de la plateforme

English version: en/licensing.md

La plateforme s'installe avec une licence annuelle (12 mois) ou triennale (36 mois), signée par l'éditeur et vérifiée localement, sans aucun appel réseau. La licence ne limite ni les utilisateurs, ni les agents, ni les applications, ni les cas d'usage, ni les plugins, et elle ne porte aucun prix : un contenu de licence qui contiendrait un champ de prix, de montant, de devise, de siège, de quota, de limite ou de compteur est refusé (422 license_forbidden_field). Les budgets FinOps et les limites de sécurité décidés par le client restent des réglages du client, sans lien avec la licence.

Contenu d'une licence​

JWS compact signé en EdDSA (Ed25519), en-tête {"alg": "EdDSA", "kid": ..., "typ": "license+jws"}, contenu JSON canonique conforme au contrat contracts/v1/license-entitlement.schema.json :

ChampRôle
license_ididentifiant lic-...
customer_id, product_id, editionclient, produit, édition (enterprise, enterprise-sovereign)
installation_group_idgroupe d'installation : toutes les installations du groupe acceptent la licence
environmentsenvironnements inclus (dev, test, staging, prod)
valid_from, valid_until, term_monthspériode ; term_months vaut 12 ou 36 et valid_until = valid_from + term_months mois
grace_dayspériode de grâce après valid_until (0 à 90 jours)
offline_allowedtrue : activable depuis un fichier sur une installation isolée ; false : uniquement par l'API
support_ref, issued_at, key_id, schema_versionréférence support, date d'émission, clé de signature, version du contrat

L'émission se fait côté éditeur avec tools/license-issuer (README (tools/license-issuer/README.fr.md)).

Préparer l'installation​

  1. Déclarer la ou les clés publiques de l'éditeur livrées avec le bundle de release : EAI_LICENSE_PUBLIC_KEYS=publisher-2026:<x> (voir configuration.md).
  2. Une installation reçoit un installation_id unique à la migration V110 ; il ne change plus, même après une restauration de la base. Le installation_group_id est fixé par la première activation.
  3. Sans licence, l'installation fonctionne dès le premier jour (état unlicensed, bandeau d'information).

Activer une licence en ligne (téléversement)​

Un administrateur plateforme téléverse le jeton :

curl -sS -X POST https://<control-plane>/v1/license/activations \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d "{\"token\": \"$(cat acme.lic)\"}"

« En ligne » désigne uniquement le passage par l'API du Control Plane : la vérification reste locale, aucun service de l'éditeur n'est contacté. Réponse 201 {activation: activated | already_active, license, status}.

Activer une licence en mode isolé (air-gapped)​

Copier le fichier de licence dans le pod du Control Plane puis :

kubectl -n enterprise-ai exec deploy/control-plane -- \
python -m eai.domains.licensing.cli activate --file /tmp/acme.lic --actor ops-jdupont
kubectl -n enterprise-ai exec deploy/control-plane -- python -m eai.domains.licensing.cli status

La licence doit avoir offline_allowed: true (sinon 422 license_offline_not_allowed, la téléverser par l'API). L'activation est inscrite dans l'historique et la chaîne d'audit plateforme avec l'acteur cli:<actor>. Le test tests/test_licensing.py::test_offline_issuance_and_file_activation_without_network exécute l'émission et l'activation avec tout accès réseau interdit.

États​

Avec now l'heure UTC, soon = EAI_LICENSE_EXPIRING_SOON_DAYS (30 par défaut) et la licence effective = la licence commencée (valid_from <= now) dont la grâce se termine le plus tard :

ÉtatRègle exacteEffet
unlicensedaucune licence commencéeaucun refus, bandeau d'information
validvalid_from <= now < valid_until - soonaucun
expiring_soonvalid_until - soon <= now < valid_untilbandeau d'avertissement (critique à 7 jours)
gracevalid_until <= now < valid_until + grace_daysbandeau critique, aucun refus
expirednow >= valid_until + grace_daysbandeau critique, refus des seules actions listées ci-dessous

GET /v1/license renvoie l'état, les dates, les jours restants et le bandeau (banner.level, banner.code, banner.message_i18n.fr/en) à tout utilisateur authentifié, sans identifiant client ni référence support.

Contrat de support​

Chaque activation (téléversement par l'API ou fichier hors ligne) enregistre, dans la même transaction, le contrat de support de la licence (table eai.support_contracts, migration V168), uniquement à partir des données signées : référence (support_ref), client, niveau (l'édition signée), période (valid_from, valid_until, fin de grâce grace_until). Une licence de renouvellement du même groupe d'installation remplace le contrat précédent à partir de sa propre date de début (superseded_by, superseded_from). L'état (scheduled, active, grace, superseded, ended) est calculé avec l'horloge de la licence. La réponse d'activation (support_contract) et GET /v1/license/history (support_contracts, administrateur de la plateforme) le renvoient ; GET /v1/license ne le renvoie jamais. Le contrat ne porte aucun prix, aucun montant ni aucune devise ; la table est réservée à la plateforme (RLS forcée, aucun tenant ne la lit ni ne l'écrit).

Ce que l'expiration change, et ce qu'elle ne change jamais​

Après la grâce (état expired), seules deux actions sont refusées avec 403 license_expired :

  • distribution.upgrade : installer une nouvelle version de la distribution ;
  • marketplace.install : installer un nouveau pack depuis la marketplace.

Les composants qui portent ces deux points d'entrée appellent eai.domains.licensing.guard.require(principal, action) avant d'agir ; chaque refus est inscrit dans l'historique et l'audit (license.guard.&lt;action&gt;).

L'expiration ne change jamais (liste never_restricted de GET /v1/license) : authentification, autorisations et politiques, contrôles de sécurité, journal d'audit et transfert SIEM, incidents et confinement, Kill Switch, sauvegarde, restauration et reprise, agents en cours et agents critiques, nouvelles exécutions d'agents, Model Gateway, plugins et packs déjà installés, utilisateurs et tenants, configuration, supervision et alertes, activation et renouvellement de licence. Il n'existe pas de Kill Switch commercial. Le test tests/test_licensing.py::test_expiry_refuses_only_the_documented_actions_and_nothing_stops le démontre.

Avertissements​

Le job license_check de python -m eai.worker (intervalle EAI_WORKER_LICENSE_CHECK_SECONDS) enregistre une seule fois chaque avis : 30, 7 et 1 jours avant valid_until (EAI_LICENSE_WARNING_DAYS), début de la grâce, fin de la grâce. Chaque avis écrit un événement d'historique, un événement d'audit license.notice.<kind>, une ligne de journal license.alert et un événement envoyé aux cibles SIEM configurées. Un renouvellement planifié sans interruption supprime les avertissements. Le job n'arrête jamais rien.

Renouvellement​

Émettre une nouvelle licence (nouveau license_id, même installation_group_id) avec valid_from au plus tard à la fin de la grâce de la licence en cours, puis l'activer comme ci-dessus. Elle devient effective d'elle-même à son valid_from ; le bandeau affiche renewal_scheduled d'ici là. Une licence déjà expirée se renouvelle de la même façon : l'activation n'est jamais bloquée.

PRA, migration de cluster et second site​

  • Restauration : la base restaurée garde installation_id, installation_group_id et les licences ; l'état reste le même, aucune réactivation n'est nécessaire (réactiver le même jeton répond already_active).
  • Migration de cluster ou site de secours : une nouvelle installation (nouvel installation_id) active le même fichier de licence ; elle rejoint le groupe de la licence.
  • Une licence d'un autre groupe est refusée (409 license_group_mismatch).
  • Démonstration : tests/test_licensing.py::test_dr_restore_cluster_migration_and_second_site.

Historique et API​

RouteRôle requisUsage
GET /v1/licensetout utilisateur authentifiéétat et bandeau
POST /v1/license/activationsplatform_adminactivation par téléversement
GET /v1/license/historyplatform_admininstallation, licences, événements paginés

Chaque route prend une décision de politique (license.read, license.activate, license.history.read) inscrite dans l'audit. Codes d'erreur : license_invalid_token, license_signature_invalid, license_unknown_key, license_invalid_payload, license_forbidden_field, license_invalid_term, license_environment_not_included, license_offline_not_allowed, license_group_mismatch, license_conflict, license_expired.

Rotation des clés de l'éditeur​

Ajouter la nouvelle clé à EAI_LICENSE_PUBLIC_KEYS en gardant l'ancienne tant que des licences signées avec elle sont en cours. Une clé retirée rend ses licences invérifiables pour une nouvelle activation (422 license_unknown_key) ; les licences déjà activées restent enregistrées et l'état ne change pas.

Dépannage​

CodeCauseAction
license_unknown_keykid absent de EAI_LICENSE_PUBLIC_KEYSdéclarer la clé publique livrée
license_signature_invalidfichier modifié ou clé différenteredemander le fichier à l'éditeur
license_environment_not_includedEAI_ENVIRONMENT absent de environmentsdemander une licence incluant cet environnement
license_group_mismatchlicence d'un autre groupe d'installationutiliser la licence du groupe
license_offline_not_allowedoffline_allowed: falsetéléverser par l'API