Skip to main content
Version: v5.6.1

Entitlements and licensing

Included from Basic tier

The entitlement service is the server's central licence enforcement: it resolves the effective entitlements — the embedded Basic-tier defaults plus the stored, signature-re-verified licence documents — and answers every create gate, feature gate and expiry-stop check through one component. Stored envelopes are re-verified on every resolution (never trusted as a stored validity flag); a document that fails re-verification contributes nothing and is counted in the diagnostic invalid_documents, and one named by the embedded revocation denylist contributes nothing and is counted in denied_documents. Resolution is cached for 15 seconds and additionally expires at the next expiry-state boundary, so a licence never reads "valid" past its clock boundary.

The licence file​

A licence file is a signed envelope: {payload, signature, key_id}, where the signature is Ed25519 over the canonical JSON of the payload and key_id selects the verification key from the keyring embedded in the binary (pkg/licence/keyring.json). The package holds verification only — signing lives in a separate utility LimePoint holds, built on the same package so the two sides cannot drift format. Verification failures are named sentinels:

  • licence: malformed licence file
  • licence: unknown signing key
  • licence: signature verification failed

Expiry is deliberately not a verification failure: it is computed state, so an expired licence applies cleanly and reports its state — which is what makes renewal painless.

Every keyring key carries a role that scopes what it may sign: an issuing key (the default for an entry without a role) signs licences of every tier; a registration key signs only Basic-tier self-service documents (any other tier under it is a signature failure); a development key signs anything an issuing key signs and labels the signer as the dev/demo keypair — the committed dev/demo licences (examples/licences) are signed by the 2026-test development key, which only development builds of the binary carry (opschain version prints dev keys: on on such a build). A released binary trusts production keys alone, so a dev/demo licence answers licence: unknown signing key on it; evaluate a release with the evaluation licence shipped beside it. A mistyped role makes the keyring invalid rather than widening a key's scope.

The Basic-tier registration lane​

A Basic-tier licence can be obtained by self-service registration on LimePoint's licence service: the registration page captures a company email address and three declared fields (organisation, country as an ISO 3166-1 code, use case from a short list), shows the full list of what is collected and why, and records acceptance of the privacy notice (by version) with the registration. The signed document that arrives is a Basic-tier, online-enforcement licence minted by the service's registration key — the one key that lives in the service, and the one role that can sign nothing but the Basic tier, so a compromised registration key can never produce a paid-tier licence. A released binary trusts the production registration key, so a registered document verifies on it; a development build additionally carries a development registration key for the lane's own tests.

Disclosed installation intelligence is a condition of that tier. For a Basic-tier online licence only, each verification round attaches a coarse platform profile beside the identifiers below: the operating system and architecture families, the CPU count, the installed memory as a coarse class (under 4 GB … over 64 GB, or unknown) and whether the installation runs in a container, a virtual machine or on a host — every value one of a short fixed list. The profile has no field for a hostname, a MAC address, a serial number or any other identifier, and the service refuses any such field by name. Paid tiers never send it: an Enterprise or pack licence's verification round carries the identifiers below and nothing else, and usage telemetry remains opt-in on both the licence and the installation.

The signed document payload carries licensee, licence_id, key_id, issued, expires, optional grace_days, tier, limits and features. Grace is licence-carried, never a server setting: a document without grace_days gets the default 30 days. A stored setting would let customers extend their own licences.

The legacy RSA surface​

A second, older verifier handles upstream-format RSA licence files ({data, signature}, RSA-SHA256 over the data object), verified against the embedded production public key oc_public_key.pem (an RSA public key — it verifies signatures, it cannot produce them). A legacy file accepted by PUT /api/licence is stored Base64-encoded in the licence key of the global settings blob and reported back as /api/info's licence_info attribute (the data object when valid, the exact sentinel licence file invalid otherwise, null when absent). PUT /api/licence routes by file shape: an envelope (payload/key_id present) goes to the entitlement service; anything else takes the legacy lane. licence_info keeps the upstream GUI contract; licence_status (below) is the enforcement truth.

Tiers and resolution​

Three tiers: basic (the embedded default when no licence is stored), enterprise (replaces the defaults wholesale), and pack (layers packs:<name> entitlements beside an enterprise licence).

Resolve is pure — the same documents and clock always resolve identically:

  • With no enterprise document the embedded Basic-tier defaults apply and the state is valid — the Basic tier never expires.
  • An enterprise document replaces the defaults wholesale (tier, limits, features, grace days); when several enterprise documents are stored (a renewal re-mint beside its predecessor) the one expiring last wins.
  • Pack documents layer their features beside the base while valid or in grace, and stop contributing once expired.
  • An expired enterprise licence keeps its entitlements in the result, with the state carrying the stop signal — expiry never reverts a licensed system to the Basic tier; only explicit removal of the last enterprise document does.
  • The baseline union applies last: packs:common, packs:academy and identity:builtin are always granted, whatever the applied documents carry (every tier carries the built-in sign-in capability and the Academy agent packaging) — the mint tool also injects the first two at mint time, and this union is the product-side belt for licences issued before it did, so applying an enterprise licence never silently revokes the bundled pack, the Academy packaging or the packaged sign-in buttons.
  • packs:academy is in the Basic tier explicitly — the embedded defaults carry it, not only the union — and is the gate on any academy content: the Basic tier unlocks the Foundations track only, and every other academy track is owned by a pack and opens with that pack's own entitlement, which the Academy agent reads back from licence_status and enforces as a track gate, never the licence (the Academy pack, "What the licence unlocks").

The embedded Basic-tier defaults (pkg/licence/defaults.json, overridable at packaging time via a build overlay, never at deploy time): limits of 1 project, 3 environments, 1 asset, 3 runners, 0 agents, 1 runner group, 3 GitHub trust policies, 1,000 GitHub token exchanges per day and 0 self-hosted GitHub instances; features assets, identity:builtin, integrations:github, packs:academy, packs:common, templates, triggers.

The feature registry​

The fixed feature vocabulary (pkg/licence/features.go) is the single table the enforcement sites, validation and the features listing share:

FeatureGates
agentsfleet agent registration and connection — the bare feature packs:agents supersedes (either grants; the pack name is canonical on new licences)
assetscreating asset nodes (and the template-driven estate model with them)
identity:builtinregistering the built-in packaged sign-in providers as plain identity — GitHub (github.com), Google and Microsoft Entra ID on your tenant's issuer (https://login.microsoftonline.com/<tenant id>/v2.0, registered with --tenant-id; the multi-tenant common endpoint is not supported). Basic-available and baseline-unioned; GHES hosts are gated by tier instead.
identity:ssoenterprise SSO: customer-configured OIDC provider instances (your own IdP or tenant) and group→role mapping writes on them (an ldap-kind connection's mappings are base platform, never gated). Enterprise-weighted; a lapse refuses new registrations and mapping creates but retains configuration, keeps listings and mapping deletion open, and never touches local/LDAP login or the step-up floor.
integrations:githubthe GitHub-native integration suite's server side: GitHub App connections, trust policies, the Actions OIDC token exchange, just-in-time GH_TOKEN installation-token minting, run attestation and webhook provisioning (opschain docs integrations-github). Basic-available with limits as the lever — github_trust_policies (3 on Basic), github_token_exchanges_per_day (1,000 on Basic), github_token_mints_per_day (1,000 on Basic) and github_self_hosted_instances (0 on Basic: GitHub Enterprise Server is the Enterprise tier boundary, decided by host; github.com including GitHub Enterprise Cloud stays Basic-reachable). A lapse refuses new registrations, enabling writes, token exchanges, JIT mints, attestations and webhook provisioning with named messages (each an audit event), retains configuration untouched, and keeps deletion open — everything lights back up on upgrade. Sign-in with GitHub rides identity:builtin, never this feature.
integrations:jira-service-managementthe Jira Service Management integration's server side: JSM connections (Atlassian cloud or Jira Data Center, project-key scoped), the jsm:request / jira:issue typed-reference lane, Jira webhook provisioning on the signed inbound lane, and the single audited outbound write door (internal comments, transitions, field writes, evidence-linked resolution — opschain docs integrations-jsm). Enterprise-weighted, never in the Basic-tier defaults. A lapse refuses new registrations, enabling writes and every outbound request write with named messages, retains configuration untouched, and keeps deletion open — everything lights back up on upgrade.
maintenancemaintenance windows
secrets:multi-vaultmulti-vault secrets: enabling more than one secrets provider and declaring scope bindings. The first enabled provider is base platform and survives licence lapse; a lapse refuses new enabling writes and binding declarations but retains configuration — existing providers and bindings keep resolving, and binding deletion stays open. The runner lease ships ungated: security posture, never metered.
templatestemplate authoring
triggerstrigger creation

Beside the fixed registry, each signed commercial action pack names its own packs:<name> entitlement, validated by shape only. The canonical pack entitlements are product entitlements in their own right, each with its own reference page:

Pack entitlementPage
packs:academythe OpsChain Academy agent packaging (opschain academy: install the pinned agent release, run it against your server) and the gate on any academy content — in the Basic tier and always granted (the baseline union above); it unlocks the Foundations track only, and every other track is owned by a pack and opens with that pack's entitlement, read back by the agent — the Academy pack
packs:agent-governancethe agent-governance pack's gated surfaces — the agent-governance pack
packs:agentsthe agent platform's pack surfaces — the agents pack
packs:ai-agentsthe AI-agents pack (requires packs:agent-governance) — AI agents
packs:ai-agents-<domain>one specialised AI-agents pack per SME domain (e.g. packs:ai-agents-oracle), each licensed separately and requiring packs:ai-agents — the specialised AI-agent domain packs
packs:awsthe AWS content pack (opschain-pack-aws): the aws:* actions — the governed aws:api lane, the generated typed catalog and the EC2/S3 provisioning actions. Its signed manifest names requires: packs:aws — the AWS pack
packs:azurethe Azure content pack (opschain-pack-azure): the azure:* actions — the governed azure:api ARM lane, the generated typed catalog and the VM/blob/deployment actions. Its signed manifest names requires: packs:azure — the Azure pack
packs:bindings-<framework>one regulatory binding pack per framework's vocabulary (e.g. packs:bindings-dora), declared bindings onto the canonical governance themes — the regulatory binding packs
packs:change-governancethe change-governance pack's gated surfaces — change governance
packs:commonthe bundled starter pack — always granted, the baseline union above — the common pack
packs:infrastructurethe infrastructure pack: host-targeted actions against registered infrastructure host nodes and the provision hand-over; satisfies the packs:rpc gate — the infrastructure pack
packs:mintpressthe MintPress journey: the Ruby lane, MintModel actions and renders — MintPress pack composition
packs:ping-aicthe PingOne Advanced Identity Cloud content pack (opschain-pack-ping-aic, mounted as ping_aic): the ping_aic:* actions — the ping_tenant and ping_aic_* YAML resources with plan/apply/export, environment promotion, environment secrets and variables, the configuration tree, identities, service accounts, tenant logs and the double-gated ping_aic:api lane. Its signed manifest names requires: packs:ping-aic — the PingOne Advanced Identity Cloud pack
packs:rpcthe rpc pack: the rpc YAML actions and the transport-provider family, as one grant — the rpc pack
packs:secrets-awsthe aws-secrets-manager SecretsProvider kind — the AWS Secrets Manager provider
packs:secrets-azurethe azure-key-vault SecretsProvider kind — the Azure Key Vault provider
packs:security-governancethe security-governance admin and evidence surfaces — the security-governance pack

The pack entitlements required by the five signed pack repositories, as their manifests name them: opschain-pack-rpcu requires packs:rpc, opschain-pack-infrastructure requires packs:infrastructure (which satisfies the packs:rpc gate by composition), opschain-pack-aws requires packs:aws, opschain-pack-azure requires packs:azure, and the ping-aic pack of the opschain-pack-ping family requires packs:ping-aic. Further signed content packs (OCI and GCP automation content) are minted the same way, as packs:<name> entitlements validated by shape; each gains its reference page here as it ships.

Grant-side validation is strict — minting or defaults with an unknown name fail with unknown feature %q — valid features are agents, assets, identity:builtin, identity:sso, integrations:github, integrations:jira-service-management, maintenance, secrets:multi-vault, templates, triggers, or packs:<name> for a pack entitlement — but document verification is tolerant: issued licences may carry names this build does not gate (e.g. ha on licences minted before the registry existed), which are inert and reported as unknown_features in licence_status, never a verification failure.

List the vocabulary and the server's effective grants with opschain admin licence features (see opschain docs cli/admin-licence-features).

Tool entitlements (tools:<name>)​

Beside the pack vocabulary, a licence document may carry tools:<name> entitlements for standalone LimePoint tool binaries: the tool verifies the licence document offline and refuses without the grant — no OpsChain server involved.

Tool entitlementGates
tools:rpcuthe rpcu transport utility's user verbs — composed into pack-entitled licences at mint, or granted explicitly
tools:<name>the standalone LimePoint tool <name>: its binary verifies the licence document offline and refuses without the grant

Tool entitlements are composed in at licence minting: a licence granting packs:rpc, packs:infrastructure or packs:mintpress is minted with tools:rpcu alongside, and the tool's offline gate also accepts those pack entitlements directly, so a licence minted before the composition existed still drives the tool.

The server hands the licence to the tool at dispatch: a step whose command references the OPSCHAIN_LICENCE_DOCUMENT variable receives the applied licence envelope by value — the latest-expiring applied licence granting tools:rpcu or one of the pack entitlements that compose it — and the variable stays unset when no applied licence grants it, leaving the tool's own ambient licence gate in charge.

What each documentation page requires​

Every served documentation page — this reference tree, the resource and CLI dictionaries, and the getting-started runbooks — declares the licence entitlements its documented surface requires, as an entitlements: key in the page's front matter. The value is either the single value base, or a list of entitlement codes from the vocabulary on this page:

ValueMeaning
baseThe surface ships in every tier from the Basic tier up — no additional entitlement is required to use it.
[<code>, …]Using the documented surface requires the listed entitlements; a page documenting a mixed surface lists every relevant code, and a list that also carries base means the core surface is available from the Basic tier while the named codes gate the parts the page calls out.

Every code referenced by a page is defined on this page — in the feature registry, the pack entitlement table or the tool entitlement table above — so this page is the single registry the badges resolve against.

The gates​

Every gate reads the same resolution:

  • Node creates — the kind's feature entitlement first (asset is the one kind with a feature gate, assets), then its countable limit (projects / environments / assets, live non-deleted nodes).
  • Feature creates — triggers, maintenance windows, templates.
  • The agent plane — agent registration and session establishment require packs:agents (or the bare agents feature it supersedes), and the agents limit bounds concurrently connected instances, counted from the durable registry's liveness (it survives failover): existing connections stay live when the limit shrinks or the licence lapses; a NEW registration or reconnect refuses loudly with a named error plus a customer-visible warn:licence:feature_refused event (throttled per subject). AI-type enrolment (a manifest declaring type: ai) additionally hard-depends on packs:agent-governance + packs:ai-agents — the refusal names every missing pack; ungoverned AI cannot even enrol. packs:agent-governance gates the governance funnel over the base agent capability (engine in base, pack gates creation and surfaces): trust-grant create/approve/reject and the grants listing, the recommendations lifecycle, and the per-agent activity/trust-review reads — each a named refusal. Base and never gated: enrolment and the channel, tier enforcement on the request lane, assignments (the ADVISE floor's carrier), grant revoke (authority-shedding), every halt/drain scalpel, and the audit trail.
  • Pack use — a LimePoint-signed action pack whose verified manifest declares a required entitlement is gated on that packs:<name> feature; a pack with no manifest is never gated. The cloud content packs ride exactly this gate: the AWS pack on packs:aws and the Azure pack on packs:azure, each named by its signed manifest, refused at signed-bundle install and at every action run without the grant, with installed content and configuration retained across a lapse.
  • The Ruby lane (packs:mintpress) — this lane is only applicable to existing MintPress customers and those using the MintPress SDK or the packs:mintpress feature. Without the entitlement, everything that needs the opschain-core Ruby gem refuses, per step: creating a change whose frozen tree executes through the Ruby lane (the actions.rb discovery run, stored-catalog and MintModel actions — at the direct create, the retry, and the workflow change-step alike, every client — the server is the authority behind the GUI's greyed tabs); new MintModel renders (…/mintmodels/generate); and the actions.rb/MintModel catalog derive, refused at enqueue. The degrade is transparent: assets on MintModel-backed templates still create, re-pin and bulk-assign, and every YAML action file runs as normal. Reads stay up and nothing is silently hidden: stored mintmodels, mintmodel_history and template authoring keep working, and the action catalog keeps every entry listed — Ruby-lane entries marked entitlement_required, with the list's actions_rb_present/entitlement_required meta flagging a never-derived actions.rb catalog (a git-tree file-presence check, never Ruby execution) — so existing MintPress customers are made whole by licence issuance, never a code carve-out. Each refusal is a 422 naming the entitlement (v2: permission_denied with ERROR_REASON_FEATURE_ENTITLEMENT_REQUIRED) plus a throttled customer-visible warn:licence:feature_refused event (one per surface/node/hour). The runner fleet is enforced server-side: no MintPress work exists to dispatch without the entitlement, and registering a runner (or minting a registration token) into a group bound to MintModel-templated node paths emits the same warn event as a signal — never a refusal, since a group name is only convention.
  • Notification governance (packs:security-governance) — without the entitlement, the NotificationAdminService governance procedures (mandated-subscription administration, the delivery-evidence query, retention configuration) answer the typed ERROR_REASON_PACK_ENTITLEMENT_REQUIRED refusal naming the pack. Enforcement of existing mandates — the engine's matching, the member-verb refusals — and the retention reaper never consult the licence: the controls are ungated, the admin surfaces are the pack (the posture), so a lapsed entitlement freezes administration without dropping the org's guaranteed coverage. Retention additionally reads the tier: Basic is fixed at 30d feed / 90d evidence; an Enterprise licence makes both tunable (defaults 90d / 400d).
  • Step-up by policy (packs:security-governance) — setting require_step_up/step_up_on_read on an authorisation rule refuses without the pack (ERROR_REASON_PACK_ENTITLEMENT_REQUIRED); clearing a bit and every other rule write stay ungated. Enforcement of stored bits is likewise pack-gated with the polite-lapse posture: a lapsed estate keeps the bits but they go inert — signalled by a throttled warn:licence:feature_refused event (surface authorisation_rule_step_up, once per process-interval) — and the hard-coded step-up verb floor is never affected either way.
  • Cloud secrets-provider kinds (packs:secrets-aws / packs:secrets-azure) — creating a SecretsProvider of kind aws-secrets-manager or azure-key-vault, or enabling a disabled one, answers the typed ERROR_REASON_FEATURE_ENTITLEMENT_REQUIRED refusal naming the kind's pack. The polite-lapse posture applies: already-enabled providers keep resolving, reads and the test verb stay up, and disabling/deleting stays open for lapse cleanup — only the create/enable writes gate. The vault kinds (openbao, hashicorp-vault) are base platform, and the secrets:multi-vault gate composes independently on top (a second enabled provider of any kind). See the AWS Secrets Manager provider page and the Azure Key Vault provider page.
  • Transport providers (packs:rpc) — creating a transport provider, or enabling a disabled one, answers the typed ERROR_REASON_FEATURE_ENTITLEMENT_REQUIRED refusal naming the pack (packs:rpc natively grants the transport-provider family and the rpc YAML pack — no separate feature flag; bundles such as packs:mintpress compose it in at minting). The polite-lapse posture applies: stored records keep serving, reads and the test verb stay up, and disabling/deleting stays open for lapse cleanup — only the create/enable writes gate.
  • Infrastructure pack (packs:infrastructure) — the infrastructure action pack's surfaces: host-targeted rpc actions dispatched through registered infrastructure host nodes (a host_ref input re-points the transport session at the referenced host, with the transport grant enforced against the host node's path) and the provision hand-over that registers and destroys host nodes. Every licence granting packs:infrastructure is minted with packs:rpc (and packs:common) alongside, and the gate itself backstops the composition: a packs:infrastructure grant satisfies every packs:rpc gate, so a licence minted without the bundle still drives the pack's transport engine. The reverse never holds — packs:rpc alone does not grant the infrastructure pack.
  • Transport grants and bindings (packs:rpc) — creating or directly approving a transport grant, and declaring a scope binding, gate on the same pack with the same polite-lapse posture: stored grants keep serving dispatch, reads stay up, and reject/revoke and binding deletes stay open for lapse cleanup, so a lapsed estate can always unwind authority.
  • Runner registration — the runners limit against currently registered (unrevoked) runners.
  • Runner group creation — the runner_groups limit against distinct existing group names; joining an existing group is never refused, and a document minted before the limit existed (field absent) is unlimited.
  • GitHub integration (integrations:github) — connection and trust-policy creates and enabling writes check the feature; a non-github.com host counts against github_self_hosted_instances (never upliftable on paid Basic — the tier boundary itself), policy creates against github_trust_policies, each token exchange against github_token_exchanges_per_day per UTC day, and each just-in-time GH_TOKEN installation-token mint at step dispatch against github_token_mints_per_day per UTC day. All four are pointer-shaped: absent on a minted document means unlimited. Refusals are named with the uplift remedy and audit-evented; deletes never gate.
  • JSM integration (integrations:jira-service-management) — connection creates, enabling writes, webhook provisioning and every outbound request write (internal comment, transition, field write, evidence-linked resolution) check the feature; refusals are named and deletes never gate.
  • The work stop — see lifecycle below.

The refusals are named 422s, verbatim:

  • Limit: the <tier> licence allows <N> <kind>s (<M> in use) — apply a licence with a higher limit to create more (opschain admin licence apply)
  • Feature: <noun> require an enterprise licence with the "<feature>" feature — the <tier> licence does not include it (opschain admin licence apply)
  • Pack: action pack "<pack>" (mounted as "<mount>") requires the "<feature>" entitlement — the <tier> licence does not include it; apply the licence pack that grants it (opschain admin licence apply / PUT /api/licence)
  • Revoked on apply: licence <id> has been revoked by LimePoint (<reason>) and cannot be applied — contact LimePoint support for a replacement licence

Lifecycle: valid → in_grace → expired​

The expiry state is always computed from the document and the clock, never stored:

StateMeaningWhat it stops
validBefore expiry.Nothing.
in_graceInside the document's own grace window (its grace_days, default 30) after expiry.Nothing — everything works, loudly (warn:licence:expiring).
expiredGrace ended.Starting new work (changes, workflow runs, trigger firings, maintenance lifecycles, runner job offers). Reads, licence uploads and in-flight steps are never stopped.

The post-grace stop's refusal, verbatim: the licence for <licensee> expired on <date> and its <N>-day grace window has ended — no new <activity> are accepted until a licence is applied (opschain admin licence apply / PUT /api/licence); existing data, reads and in-flight steps are unaffected. On the dispatch plane the stop parks queued work with its own verdict — see the licence rung of opschain docs fleet-queue's verdict ladder. The boolean form of the stop (the runner offer gate) fails open on a resolution error, so a degraded database read never halts the fleet by itself.

Every replica runs a watch loop (default every minute) that makes the lifecycle loud, throttled durably against the events table (once per 6 hours across all replicas and restarts): warn:licence:expiring during grace, warn:licence:expired once stopped.

Surfaces​

SurfaceWhat
PUT /api/licenceMultipart upload (form field licence). Authorised as update on the /licence path (ordinary user auth, not the admin bearer). Envelope lane: named 422s for verification failures, 201 with the document plus the post-apply licence_status. Legacy lane: upstream 422s (No licence file provided, Licence has failed content validation, …), 201 with the validated data object.
DELETE /api/licence/{licence_id}Removes one stored document; 404 for an unknown id; 200 with the post-removal licence_status. The explicit act that can revert to the Basic tier.
/api/info → licence_statusThe resolved tier, state, effective limits and features, grace_days, unknown_features, every stored document with its own state, and the invalid_documents / denied_documents counts. Served beside the legacy licence_info.
GET /admin/licence (admin bearer)The usage report the support bundle collects: store_kind (postgres or sqlite), licence_status plus used/limit per countable entitlement, counted by the same rules as the create gates. Degradations are named in notes, never silently omitted.
POST /api/licence/verify and the background roundThe online verification request to the licence service carries product, licence_id, installation_id, product_version and store_kind (postgres or sqlite). For a Basic-tier document it also carries the platform profile (os, arch, cpu_count, memory_class, runtime — closed vocabularies, see the registration lane above); paid-tier documents never carry it.

CLI: opschain admin licence apply|show|remove|features — see opschain docs cli/admin-licence and its subpages (cli/admin-licence-apply, cli/admin-licence-show, cli/admin-licence-remove, cli/admin-licence-features).

Storage​

Envelopes live in the licence_documents table on the scheduler database (PostgreSQL in server mode — HA-wide, shared by every replica — or SQLite), keyed by licence_id with the uploader recorded: re-applying the same licence replaces it, a renewal (new licence_id) accumulates beside its predecessor. A deletion of an unknown id is the named licence document not found. A server composition without a store refuses apply/remove with this server composition cannot store licences; GET /admin/licence on such a composition reports "enforcement": "not wired".

Events vocabulary​

All ordinary triggerable events, on the platform path:

  • api:licence:applied — every successful envelope apply (licence_id, licensee, tier, expires, actor).
  • api:licence:removed — every DELETE /api/licence/{id}.
  • warn:licence:expiring — during the grace window, throttled to once per 6 hours across replicas.
  • warn:licence:expired — once the stop is in force, same throttle.

Failure modes​

  • A tampered or wrongly-keyed file never stores: the 422 names the cause (Licence signing key is not trusted by this server, Licence signature verification failed — the file is not a genuine OpsChain licence or has been altered, Licence file is malformed — not a valid OpsChain licence).
  • A stored document that stops verifying (keyring change, tampering at rest) silently stops granting but is visibly counted in invalid_documents; a denylisted one in denied_documents.
  • Expiry is never a surprise: the grace window is loud (warn:licence:expiring), the stop is loud (warn:licence:expired), and queued work parks with a licence verdict instead of being lost.
  • A degraded entitlement read fails closed at create doors (the error surfaces) but open at the runner offer gate, so the fleet is never halted by a transient database fault alone.
  • opschain docs fleet-queue — the licence rung of the dispatch verdict ladder.
  • opschain docs cli/admin-licence — the admin licence command family.
  • opschain docs events — where the licence lifecycle events land.