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 server with no licence document stored runs on the evaluation
footprint — 1 project, 1 environment, 1 asset, 1 runner, 1 runner
group, no agents, no GitHub integration, no Academy — and every status
surface carries a persistent registration notice. The Basic tier
itself (1 project, 3 environments, 1 asset, 3 runners, 1 runner group,
the GitHub integration with its Basic caps, the Academy's Foundations
track) comes from a registered Basic licence, obtained by
self-service registration on LimePoint's licence service: opschain admin licence register (or 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 — then applies the minted
document to the server and stores its phone-home credential, so
registration never means copying JSON by hand
(opschain docs cli/admin-licence-register). Registering is free; the
disclosure is its condition. A service whose registration lane is
closed answers so by name — an evaluation licence from LimePoint is the
other way up. 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 evaluation footprint when no licence
is stored, or a registered Basic document), enterprise (replaces the
defaults wholesale), and pack (layers packs:<name> entitlements
beside a base licence).
Resolve is pure — the same documents and clock always resolve
identically:
- With no base document the embedded evaluation footprint applies and
the state is
valid— the footprint never expires; its grants are sourcedevaluationandlicence_status.registrationnames the way to the Basic tier. - A registered Basic document is a base document like an enterprise one: it replaces the footprint with the Basic limits and features (and runs 365 days, renewed over the phone-home channel); an enterprise document replaces the defaults wholesale (tier, limits, features, grace days); when several base 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:commonandidentity:builtinare always granted, andpacks:academyon every licensed resolution, whatever the applied documents carry (every licensed tier carries the built-in sign-in capability and the Academy agent packaging) — the mint tool also injectspacks:commonat 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. The evaluation footprint unionspacks:commonandidentity:builtinonly: the Academy is one of the things registration lights up. packs:academyis in the Basic tier — the registered Basic document carries it explicitly, and the union keeps it on every licensed resolution — 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 evaluation footprint (pkg/licence/defaults.json, overridable
at packaging time via a build overlay, never at deploy time): limits of
1 project, 1 environment, 1 asset, 1 runner, 0 agents, 1 runner group, 0
GitHub trust policies, 0 GitHub token exchanges and mints per day and 0
self-hosted GitHub instances; features assets, identity:builtin,
packs:common, templates, triggers. The registered Basic document
the licence service mints carries the Basic tier proper: 1 project, 3
environments, 1 asset, 3 runners, 0 agents, 1 runner group, 3 GitHub
trust policies, 1,000 GitHub token exchanges and 1,000 mints per day, 0
self-hosted GitHub instances; features assets, identity:builtin,
integrations:github, packs:academy, packs:common, templates,
triggers.
Upgrading onto the footprint. An unregistered server that upgrades
to this release lands on the footprint at once, with no grace window.
Counts are create gates, so everything already in the estate stays —
three environments and three runners keep working — and the next create
above the footprint is refused by name, with the refusal naming
opschain admin licence register first. Features lapse politely: the
GitHub integration keeps its connections and trust policies, refuses new
token exchanges, just-in-time mints, attestations and webhook
provisioning with its named refusals and audit events, keeps deletion
open, and lights back up the moment the registered document is applied.
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, the per-feature grants (evaluation sources the footprint), the registration notice while the footprint applies, every stored document with its own state, and the invalid_documents / denied_documents counts. Served beside the legacy licence_info. |
LicenceService.StoreLicenceCredential (API v2) | Stores one online installation's phone-home credential in the platform secret vault under licence/<installation_id>/credential — the last step of opschain admin licence register; the environment variable, when set, still wins for that installation. Authorised as update on /licence; never echoes the credential. |
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 register|apply|show|remove|features|verify
— see opschain docs cli/admin-licence and its subpages
(cli/admin-licence-register, cli/admin-licence-apply,
cli/admin-licence-show, cli/admin-licence-remove,
cli/admin-licence-features, cli/admin-licence-verify).
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.