Skip to main content

Platform licences

French version: ../licensing.md

The platform is installed with an annual (12 months) or triennial (36 months) licence, signed by the publisher and verified locally without any network call. The licence never limits users, agents, applications, use cases or plugins and carries no price: a licence payload holding a price, amount, currency, seat, quota, limit or counter field is refused (422 license_forbidden_field). FinOps budgets and security limits chosen by the customer remain customer settings, unrelated to the licence.

Licence content​

JWS compact signed with EdDSA (Ed25519), header {"alg": "EdDSA", "kid": ..., "typ": "license+jws"}, canonical JSON payload conforming to contracts/v1/license-entitlement.schema.json:

FieldPurpose
license_idlic-... identifier
customer_id, product_id, editioncustomer, product, edition (enterprise, enterprise-sovereign)
installation_group_idinstallation group: every installation of the group accepts the licence
environmentsincluded environments (dev, test, staging, prod)
valid_from, valid_until, term_monthsperiod; term_months is 12 or 36 and valid_until = valid_from + term_months months
grace_daysgrace period after valid_until (0 to 90 days)
offline_allowedtrue: may be activated from a file on an air-gapped installation; false: API upload only
support_ref, issued_at, key_id, schema_versionsupport reference, issue date, signing key, contract version

Issuance happens on the publisher side with tools/license-issuer (README (tools/license-issuer/README.md)).

Preparing the installation​

  1. Declare the publisher public key(s) delivered with the release bundle: EAI_LICENSE_PUBLIC_KEYS=publisher-2026:<x> (see configuration.md).
  2. An installation receives a unique installation_id at migration V110; it never changes, even after a database restore. The installation_group_id is set by the first activation.
  3. Without a licence the installation works from day one (state unlicensed, information banner).

Online activation (upload)​

A platform administrator uploads the token:

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

"Online" only means going through the Control Plane API: verification stays local, no publisher service is contacted. Response 201 {activation: activated | already_active, license, status}.

Air-gapped activation​

Copy the licence file into the Control Plane pod, then:

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

The licence must have offline_allowed: true (else 422 license_offline_not_allowed: upload it through the API). The activation is written to the history and the platform audit chain with actor cli:<actor>. The test tests/test_licensing.py::test_offline_issuance_and_file_activation_without_network runs issuance and activation with every network access denied.

States​

With now the UTC time, soon = EAI_LICENSE_EXPIRING_SOON_DAYS (default 30) and the effective licence = the started licence (valid_from <= now) whose grace ends last:

StateExact ruleEffect
unlicensedno started licencenothing refused, information banner
validvalid_from <= now < valid_until - soonnone
expiring_soonvalid_until - soon <= now < valid_untilwarning banner (critical at 7 days)
gracevalid_until <= now < valid_until + grace_dayscritical banner, nothing refused
expirednow >= valid_until + grace_dayscritical banner, only the actions listed below are refused

GET /v1/license returns the state, the dates, the remaining days and the banner (banner.level, banner.code, banner.message_i18n.en/fr) to any authenticated user, without customer id or support reference.

Support contract​

Every activation (API upload or offline file) records, in the same transaction, the support contract of the licence (table eai.support_contracts, migration V168), from the signed data only: reference (support_ref), customer, level (the signed edition), period (valid_from, valid_until, end of grace grace_until). A renewal licence of the same installation group replaces the previous contract from its own start date (superseded_by, superseded_from). The state (scheduled, active, grace, superseded, ended) is computed with the licensing clock. The activation response (support_contract) and GET /v1/license/history (support_contracts, platform administrator) return it; GET /v1/license never does. The contract carries no price, no amount and no currency; the table is platform only (forced RLS, no tenant reads or writes it).

What expiry changes, and what it never changes​

After the grace period (state expired) only two actions are refused with 403 license_expired:

  • distribution.upgrade: installing a new distribution version;
  • marketplace.install: installing a new pack from the marketplace.

The components owning these two entry points call eai.domains.licensing.guard.require(principal, action) before acting; each refusal is written to the history and the audit chain (license.guard.<action>).

Expiry never changes (list never_restricted of GET /v1/license): authentication, authorization and policies, security controls, the audit trail and SIEM forwarding, incidents and containment, the kill switch, backup, restore and recovery, running and critical agents, new agent runs, the Model Gateway, installed plugins and packs, users and tenants, configuration, monitoring and alerts, licence activation and renewal. There is no commercial kill switch. The test tests/test_licensing.py::test_expiry_refuses_only_the_documented_actions_and_nothing_stops demonstrates it.

Warnings​

The license_check job of python -m eai.worker (interval EAI_WORKER_LICENSE_CHECK_SECONDS) records each notice once: 30, 7 and 1 days before valid_until (EAI_LICENSE_WARNING_DAYS), start of grace, end of grace. Each notice writes a history event, an audit event license.notice.<kind>, a license.alert log line and an event sent to the configured SIEM targets. A renewal scheduled without a gap silences the warnings. The job never stops anything.

Renewal​

Issue a new licence (new license_id, same installation_group_id) whose valid_from is at the latest the end of the grace of the current licence, then activate it as above. It becomes effective by itself on its valid_from; the banner shows renewal_scheduled until then. An expired licence is renewed the same way: activation is never blocked.

DR, cluster migration and second site​

  • Restore: the restored database keeps installation_id, installation_group_id and the licences; the state is unchanged, no reactivation is needed (reactivating the same token answers already_active).
  • Cluster migration or DR site: a new installation (new installation_id) activates the same licence file; it joins the licence group.
  • A licence of another group is refused (409 license_group_mismatch).
  • Demonstration: tests/test_licensing.py::test_dr_restore_cluster_migration_and_second_site.

History and API​

RouteRequired roleUse
GET /v1/licenseany authenticated userstate and banner
POST /v1/license/activationsplatform_adminactivation by upload
GET /v1/license/historyplatform_admininstallation, licences, paginated events

Each route takes a policy decision (license.read, license.activate, license.history.read) recorded in the audit chain. Error codes: 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.

Publisher key rotation​

Add the new key to EAI_LICENSE_PUBLIC_KEYS and keep the old one while licences signed with it are current. A removed key makes its licences unverifiable for a new activation (422 license_unknown_key); licences already activated stay recorded and the state does not change.

Troubleshooting​

CodeCauseAction
license_unknown_keykid missing from EAI_LICENSE_PUBLIC_KEYSdeclare the delivered public key
license_signature_invalidfile modified or different keyask the publisher for the file again
license_environment_not_includedEAI_ENVIRONMENT not in environmentsask for a licence including this environment
license_group_mismatchlicence of another installation groupuse the licence of the group
license_offline_not_allowedoffline_allowed: falseupload through the API