getting-started Intermediate 7 min read

Activation and Entitlement

What pyvorin-native 1.0.9 does when you activate: the Ed25519-signed lease, device binding, gate semantics, anti-rollback, and the offline grace table per tier.

Published Aug 20, 2026

Activating Pyvorin takes one command and one network call. Everything after that — the daily gate check, offline operation, grace after expiry — is designed so that a flaky network or a laptop on a plane degrades your licence status slowly and predictably, never suddenly. This page describes exactly what activation sends and stores, how the local licence gate decides pass or fail, and how long each tier keeps working when the machine cannot reach the licensing server.

All of it was verified against the installed 1.0.9 build, including the runtime behaviour of the gate itself.

Activating

pyvorin activate PYV-XXXX-XXXX-XXXX

The key can also be supplied with --key if that suits your tooling better; both forms are accepted by the console script. On success the command prints one line — License activated. tier=… expires=… — and writes the credential to disk.

The single outbound request is a POST to https://api.pyvorin.com/api/v1/licenses/check with a 30-second timeout, carrying a JSON body of exactly two fields:

{"license_key": "PYV-…",
 "device_fingerprint": "9ae48003e4f366edcd233d987aa7c91a"}

The fingerprint is the first 32 hex characters of a SHA256 over the hostname, the username and the machine ID. It binds the resulting licence to this device: the file written by activation is meaningless on any other host. Note what is absent from the payload — no source code, no file paths, no environment inventory. The PYVORIN_LICENSE_URL environment variable can redirect this call, which is how you point activation at a logging proxy for audit purposes.

When the server responds valid with an active status, the client writes ~/.pyvorin/license.json at file mode 0600 — readable only by the owning account. A legacy path at ~/.nexus/license.json is honoured if present, for installations carried over from the older Nexus-branded builds.

What is inside the licence file

The stored file holds the licence key, a lease token, a signed lease, lease and licence expiry timestamps, the tier, the feature list and the device fingerprint. Two layers protect it from tampering. The whole payload carries an _integrity field — an HMAC-SHA256 over the file's contents using a build-time secret — so editing any field invalidates the file. And the server also issues an Ed25519-signed lease, verified locally with a public key embedded in the package (LEASE_VERIFY_KEY). This second layer is the stronger one: the signing secret exists only on the Pyvorin entitlement server, so a forged lease cannot be manufactured from anything on the machine. The signed lease overrides the local file's tier and feature claims — tamper with the JSON all you like, the cryptographically signed facts win.

Three limits bound the lease itself. Maximum lease validity is 800 days; the local licence summary reports when a lease exceeds this, which is treated as invalid. A monotonic anti-rollback watermark (with a five-minute tolerance for clock skew) prevents replaying an older, longer-lived lease over a newer, shorter one. And an hours-used counter with a backup file and an anti-replay nonce makes simple rewinding of entitlement consumption detectable.

Tiers and default-deny

Five entitlement tiers are baked into the package: demo, trial, basic, professional and enterprise. Entitlement evaluation is default-deny: a feature not present in the signed claims is off, rather than on. The claims live in an entitlement cache under ~/.pyvorin/, stored as msgpack with its own hours backup, nonce counter and impossible-state detection. One honest caveat from verification: the cache's Ed25519 signature field is not enforced in the shipped 1.0.9 build because the verification key ships empty — the active protections on entitlement state are the HMAC, the signed-lease override, the watermark and the anti-replay counters. The full delivery-and-verification picture, including that gap, is documented on the protected component delivery page.

The gate: fail-open one way, fail-closed the other

The hard gate — run explicitly with pyvorin check, and invoked internally before gated work — evaluates the licence in a deliberately asymmetric way. This asymmetry is the single most important design decision in the system, and it is worth understanding rather than just trusting.

Local (offline) validationOnline revalidationGate result
PassesSucceedsPass
PassesFails or unreachablePass — the failure is returned from silently
FailsSucceedsPass (server truth wins)
FailsFails or unreachableLicenseError, carrying both reasons

Read the second row again: a locally valid licence passes even when the online revalidation fails. The gate tries the network, and if the network says nothing useful, it trusts the cryptographic evidence it already holds — a locally verifiable Ed25519 lease is strong proof of entitlement, and a transient outage should never halt a paying workload. The fourth row is the mirror image: a licence that fails local validation gets one chance to prove itself online, and if the server cannot be reached either, the gate fails closed with an error naming both failures.

This is a trade-off, and we will not pretend otherwise. Fail-open on local validity prioritises availability: a revoked licence whose revocation has not yet propagated, or a machine tucked behind a firewall, keeps working. The compensating controls are time-boxed — leases expire, the anti-rollback watermark blocks lease downgrades, and the server can issue explicit revocation — but there is a genuine window. The alternative, failing closed the moment the network is silent, would hand every DNS outage a kill switch over customer batch jobs. We judged availability the right side of that line for a build-time tool; your auditor may want to read this paragraph specifically, and it is here so they can.

The verification harness exercised the top row of that table empirically: with a forged-but-invalid local lease and a reachable server, the gate passed; with a locally valid lease and a silent network, it also passed. The behaviour is in the shipped binary, not just the source.

Offline grace after expiry

When a lease expires, the entitlement cache does not cut you off immediately. Each tier carries a grace period, counted from lease_expires_at, during which the cached entitlements remain valid without any server contact:

TierGrace period
demo24 hours (86,400 s)
trial24 hours (86,400 s)
basic72 hours (259,200 s)
professional72 hours (259,200 s)
enterprise90 days (7,776,000 s)

Ninety days for enterprise reflects how that tier is actually deployed: long-running clusters in segregated networks where a licensing outage must never be the event that pages anyone. Enterprise licences can additionally carry a server-issued offline permit — a bounded budget of offline hours the server grants in advance, beyond the standing grace period.

One escape hatch deserves a warning label. PYVORIN_SKIP_LICENSE=1 exists and bypasses the gate — but it is honoured only when the package ships with a _dev_build.marker file, and that marker is excluded from production wheels. If you find yourself reaching for it in production, the correct move is to fix the licence state, not to wish the marker into existence. Treat any environment where the variable appears to work as a packaging anomaly worth reporting, not as a feature to rely on.

Checking status day to day

pyvorin status   # JSON summary: status, tier, expires_at, features
pyvorin check    # hard gate; prints "License gate OK" and exits 0, or exits 1

status is the read-only question ("what does this machine believe its licence is?"); check is the enforcing one ("may this machine run gated work right now?"). Both are local and fast. The exit code on check makes it the right command for CI preflight and container healthchecks, where a shell test is worth more than a paragraph of JSON.

For fleet-level questions — which machines hold which keys, what the server believes about a licence — the account dashboard at app.pyvorin.com under /licenses is the other half of the picture. The CLI tells you what one machine thinks; the dashboard tells you what the issuer thinks. When those disagree, trust the dashboard, because the issuer is the party whose signature the local gate ultimately defers to.

One last operational note. Because the licence is device-bound, migrating to a new host is not a file copy: activate on the new machine and let the old lease lapse. If you automate builds on ephemeral CI runners, plan activations accordingly — a fresh runner image is a fresh device fingerprint, and treating activation as part of runner provisioning, alongside your other secrets, avoids the failure mode where a benchmark job passes on Tuesday's runner and fails on Wednesday's.

Where to go next

Last reviewed 6 August 2026 against pyvorin-native 1.0.9 installed at /root/pvfinal. Gate semantics and the offline grace table were verified by runtime probe with a throwaway home directory; the activation payload quoted is a captured request body, not a reconstruction.