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 :
| Champ | Rôle |
|---|---|
license_id | identifiant lic-... |
customer_id, product_id, edition | client, produit, édition (enterprise, enterprise-sovereign) |
installation_group_id | groupe d'installation : toutes les installations du groupe acceptent la licence |
environments | environnements inclus (dev, test, staging, prod) |
valid_from, valid_until, term_months | période ; term_months vaut 12 ou 36 et valid_until = valid_from + term_months mois |
grace_days | période de grâce après valid_until (0 à 90 jours) |
offline_allowed | true : activable depuis un fichier sur une installation isolée ; false : uniquement par l'API |
support_ref, issued_at, key_id, schema_version | ré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
- 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). - Une installation reçoit un
installation_idunique à la migrationV110; il ne change plus, même après une restauration de la base. Leinstallation_group_idest fixé par la première activation. - 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 :
| État | Règle exacte | Effet |
|---|---|---|
unlicensed | aucune licence commencée | aucun refus, bandeau d'information |
valid | valid_from <= now < valid_until - soon | aucun |
expiring_soon | valid_until - soon <= now < valid_until | bandeau d'avertissement (critique à 7 jours) |
grace | valid_until <= now < valid_until + grace_days | bandeau critique, aucun refus |
expired | now >= valid_until + grace_days | bandeau 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.<action>).
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_idet les licences ; l'état reste le même, aucune réactivation n'est nécessaire (réactiver le même jeton répondalready_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
| Route | Rôle requis | Usage |
|---|---|---|
GET /v1/license | tout utilisateur authentifié | état et bandeau |
POST /v1/license/activations | platform_admin | activation par téléversement |
GET /v1/license/history | platform_admin | installation, 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
| Code | Cause | Action |
|---|---|---|
license_unknown_key | kid absent de EAI_LICENSE_PUBLIC_KEYS | déclarer la clé publique livrée |
license_signature_invalid | fichier modifié ou clé différente | redemander le fichier à l'éditeur |
license_environment_not_included | EAI_ENVIRONMENT absent de environments | demander une licence incluant cet environnement |
license_group_mismatch | licence d'un autre groupe d'installation | utiliser la licence du groupe |
license_offline_not_allowed | offline_allowed: false | téléverser par l'API |