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 filelicence: unknown signing keylicence: 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:academyandidentity:builtinare 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:academyis 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 fromlicence_statusand 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:
| Feature | Gates |
|---|---|
agents | fleet agent registration and connection — the bare feature packs:agents supersedes (either grants; the pack name is canonical on new licences) |
assets | creating asset nodes (and the template-driven estate model with them) |
identity:builtin | registering 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:sso | enterprise 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:github | the 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-management | the 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. |
maintenance | maintenance windows |
secrets:multi-vault | multi-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. |
templates | template authoring |
triggers | trigger 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 entitlement | Page |
|---|---|
packs:academy | the 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-governance | the agent-governance pack's gated surfaces — the agent-governance pack |
packs:agents | the agent platform's pack surfaces — the agents pack |
packs:ai-agents | the 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:aws | the 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:azure | the 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-governance | the change-governance pack's gated surfaces — change governance |
packs:common | the bundled starter pack — always granted, the baseline union above — the common pack |
packs:infrastructure | the infrastructure pack: host-targeted actions against registered infrastructure host nodes and the provision hand-over; satisfies the packs:rpc gate — the infrastructure pack |
packs:mintpress | the MintPress journey: the Ruby lane, MintModel actions and renders — MintPress pack composition |
packs:ping-aic | the 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:rpc | the rpc pack: the rpc YAML actions and the transport-provider family, as one grant — the rpc pack |
packs:secrets-aws | the aws-secrets-manager SecretsProvider kind — the AWS Secrets Manager provider |
packs:secrets-azure | the azure-key-vault SecretsProvider kind — the Azure Key Vault provider |
packs:security-governance | the 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 entitlement | Gates |
|---|---|
tools:rpcu | the 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:
| Value | Meaning |
|---|---|
base | The 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 (
assetis 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 bareagentsfeature it supersedes), and theagentslimit 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-visiblewarn:licence:feature_refusedevent (throttled per subject). AI-type enrolment (a manifest declaringtype: ai) additionally hard-depends onpacks:agent-governance+packs:ai-agents— the refusal names every missing pack; ungoverned AI cannot even enrol.packs:agent-governancegates 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 onpacks:awsand the Azure pack onpacks: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 thepacks:mintpressfeature. 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_historyand template authoring keep working, and the action catalog keeps every entry listed — Ruby-lane entries markedentitlement_required, with the list'sactions_rb_present/entitlement_requiredmeta 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_deniedwithERROR_REASON_FEATURE_ENTITLEMENT_REQUIRED) plus a throttled customer-visiblewarn:licence:feature_refusedevent (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, theNotificationAdminServicegovernance procedures (mandated-subscription administration, the delivery-evidence query, retention configuration) answer the typedERROR_REASON_PACK_ENTITLEMENT_REQUIREDrefusal 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) — settingrequire_step_up/step_up_on_readon 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 throttledwarn:licence:feature_refusedevent (surfaceauthorisation_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 kindaws-secrets-managerorazure-key-vault, or enabling a disabled one, answers the typedERROR_REASON_FEATURE_ENTITLEMENT_REQUIREDrefusal 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 thesecrets:multi-vaultgate 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 typedERROR_REASON_FEATURE_ENTITLEMENT_REQUIREDrefusal naming the pack (packs:rpcnatively grants the transport-provider family and the rpc YAML pack — no separate feature flag; bundles such aspacks:mintpresscompose 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 (ahost_refinput 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 grantingpacks:infrastructureis minted withpacks:rpc(andpacks:common) alongside, and the gate itself backstops the composition: apacks:infrastructuregrant satisfies everypacks:rpcgate, so a licence minted without the bundle still drives the pack's transport engine. The reverse never holds —packs:rpcalone 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
runnerslimit against currently registered (unrevoked) runners. - Runner group creation — the
runner_groupslimit 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 againstgithub_self_hosted_instances(never upliftable on paid Basic — the tier boundary itself), policy creates againstgithub_trust_policies, each token exchange againstgithub_token_exchanges_per_dayper UTC day, and each just-in-timeGH_TOKENinstallation-token mint at step dispatch againstgithub_token_mints_per_dayper 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:
| State | Meaning | What it stops |
|---|---|---|
valid | Before expiry. | Nothing. |
in_grace | Inside the document's own grace window (its grace_days, default 30) after expiry. | Nothing — everything works, loudly (warn:licence:expiring). |
expired | Grace 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
| Surface | What |
|---|---|
PUT /api/licence | Multipart 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_status | The 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 round | The 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— everyDELETE /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 indenied_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.
Related pages
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.