# Licence

> The licence is an offline signed file verified on the control plane at boot and scheduler ticks. It makes no call to a licence server.

_Reference · Updated 2026-10-17 · Describes release 0.1.164_

Evidence reviewed **8 October 2026**; operating baseline **0.1.130** (the 16 October 2026 handover build; see the [release notes](/help/release-notes.md)).

The licence is an **offline signed file** verified on the control plane at boot and scheduler ticks. It makes no call to a licence server. Establish whether this installation uses Mode A or Mode B before diagnosing blocked runs.

## Mode A and Mode B {#mode-a-and-mode-b}

| Mode | Configuration | Result |
|---|---|---|
| A — no enforcement | No licence token, or no public verification key | `unlicensed`; Ops says no enforcement; runs continue with a boot warning |
| B — signed licence | Matching `licence.publicKey` and licence token supplied together | Claims and signature are verified; expiry and invalid-state enforcement apply |

The published deployment procedure records **no production signing key issued yet** and Mode A for existing installations. Verification code and the deployment slot are delivered; production issuance and its approval job are not proof of a live Mode B installation. A development-key rehearsal is not a production licence.

## Install or renew {#install-or-renew}

For the managed pipeline, the secrets file remains the source of truth. The release/licence owner updates `AIOPS_LICENSE` there, updates the Azure DevOps group, and deploys with the current live image tag. The values file on the customer branch carries `licence.publicKey`. The pipeline passes the token through `licence.value` to the control plane.

The update command comes from deployment runbook §11. Set `AIOPS_SLUG` to the approved installation slug; `ADO_PAT` must already be supplied securely in the operator's environment and must not be printed.

```bash
AIOPS_SLUG=example-org
python3 scripts/customer-onboard.py ado "$AIOPS_SLUG" --update-group
```

That command writes **every group value from the file**; a value changed only in the Azure DevOps Library will be overwritten. Queue the normal approved deployment with the current image/chart. There is no upload or activation endpoint; renewal needs a rollout. Confirm the resulting licence card and `licence.state_changed` audit evidence. The scheduler picks up the state within a minute of startup.

For a chart using top-level `existingSecret`, put the licence in that Secret or `licence.existingSecret`; inline `licence.value` has nowhere to land and the chart refuses that combination. The [licence reference](/help/ops-licence.md#install) defines the supported Secret keys.

## Expiry and failure behaviour {#expiry-and-failure-behaviour}

| State | Operator sees | Effect |
|---|---|---|
| `valid` | Licence card | Normal runs |
| `grace` | Expired banner and grace days remaining | Warning only; runs continue. A token with `grace_days: 0` has no grace |
| `expired` | Red banner | New agent spend and approved executions refuse to start |
| `invalid` | Red banner with reason | Same block as expired; check signature, claims, time and key/token match |
| `unlicensed` | No-enforcement card | Runs continue |

The default grace is 30 days unless the signed file overrides it. The verifier allows 300 seconds clock skew; keep system time correct rather than using clock changes as a renewal procedure.

| Surface after expiry/invalid state | Behaviour |
|---|---|
| Alert intake | Alerts still ingest/correlate; a blocked investigation fails and escalates the incident to a human |
| New chat, discovery and agent API | Refused before start; queued remote discovery can fail; a running chat stops at its next step |
| Approved change release and automatic execution | Refused (`licence_blocked`, API 409); rollback and ticket routes remain available |
| Investigation or scan already past its first step | Can finish; API-mode resumes are not rechecked, while embedded/database-mode resumes pass the start guard again |
| Login, portal, Ops, history, audit/RCA exports and reports | Continue to work; boot is never blocked |

Runner processes need no public key or licence token. The control plane publishes the verdict for database-mode runners and enforces it when handing jobs to API-mode runners. State changes land in the **platform** audit chain, not a workspace export.

## Renewals and key changes to track {#renewals-and-key-changes-to-track}

Keep expiry, grace, token id and renewal owner in the operating calendar. When the vendor changes the public signing key, install the new public key and a token signed by it **in the same rollout**. The verifier accepts one key; an old token paired with the new key is invalid. Keep licence renewal separate from tenant secret-store encryption-key rotation.

Sources: [Offline licence reference](/help/ops-licence.md), and [Deployment runbook §11](/help/ops-installation-runbook.md#11-licence).

## The file {#the-file}

A compact JWT. Header `{"alg":"EdDSA","typ":"JWT"}`; the Ed25519 signature is the raw 64 bytes, used directly as the JWS signature. Claims:

| Claim | Meaning |
|---|---|
| `iss` | a fixed issuer string: the `ISSUER` constant in `app/aiops/licence.py`, which `scripts/issue-licence.py` also writes |
| `sub` | the customer slug the file was issued to |
| `aud` | always `aiops` |
| `iat`, `nbf`, `exp` | issued / not-before / expiry, epoch seconds |
| `jti` | unique id of this file |
| `features` | list of enterprise feature keys; empty = all |
| `max_workspaces` | integer ceiling or `null`; informational, shown on the ops console, not enforced |
| `grace_days` | optional; overrides the platform default (`AIOPS_LICENSE_GRACE_DAYS`, 30) |

Verification requires `iss`, `sub`, `aud`, `iat`, `exp`, `jti`, checks issuer and audience, accepts 300 s of clock skew on `exp`/`nbf`/`iat`, and rejects an `iat` further in the future than that (a tampered clock). Only `EdDSA` is accepted — a token signed with any other algorithm is `invalid`.

## Install {#install}

Helm (`deploy/aiops-helm/values.yaml`):

```yaml
licence:
  publicKey: |
    -----BEGIN PUBLIC KEY-----
    ...the vendor PEM published with the release...
    -----END PUBLIC KEY-----
  # either inline (lands in the chart Secret as AIOPS_LICENSE) ...
  value: "eyJhbGciOiJFZERTQSIs..."
  # ... or a pre-made Secret (External Secrets, sealed-secrets), key AIOPS_LICENSE
  existingSecret: ""
  existingSecretKey: AIOPS_LICENSE
```

The pod receives `AIOPS_LICENSE` and `AIOPS_LICENSE_PUBLIC_KEY`; the app also accepts a PEM whose newlines arrive as literal `\n`. Any other deployment (compose, bare uvicorn) sets the same two environment variables. With the chart's top-level `existingSecret` in use, `licence.value` has nowhere to land and the chart refuses to render — put `AIOPS_LICENSE` in that Secret or use `licence.existingSecret`.

Only the control plane needs the two variables. Runner processes (`aiops-agent-helm`, compose `agents` profile) carry no key: the control plane publishes its verdict to `PlatformSetting` key `licence` at boot and on every state change, and a key-less process's run guard reads that row instead of its own (always `unlicensed`) cache. api-mode runners never see the guard at all — the control plane enforces it when it hands out the job.

Renewal is a new `value` (or a new Secret) and a rollout. There is no renewal ceremony, no activation, no upload endpoint; the scheduler tick picks the new state up within a minute of the pod starting.

## What it does not do {#what-it-does-not-do}

- **No phone-home.** The control plane never contacts the vendor. Usage evidence for the contract is what `GET /admin/overview` and the audit export already show; nothing is transmitted.
- **No anti-tamper.** The image is readable and the check lives in one Python module; anyone willing to patch it and rebuild can remove it, as with GitLab, Grafana Enterprise or Vault. The deterrents are the contract, revocable pull access to the image registry and the fact that the value is in updates, skills and support.
- **No revocation before `exp`.** Verification is offline; the pre-expiry levers are the registry policy and the deploy pipeline.
- **No feature or seat enforcement.** `features` and `max_workspaces` are surfaced on the ops console; the platform does not yet gate on them.
- **No licence server, heartbeat or usage report.** Air-gap installs work with the same file for the same reason.


## Next steps

- Next: [Cost management](/help/ops-expected-cost.md)
- [All Install and operate the platform pages](/help/install-and-operate-the-platform/llms.txt)
- [Documentation home](/help/)
