Skip to content

feat(licensing): check the license with Customer Center - #1112

Merged
jlav merged 2 commits into
mainfrom
jl/license-enforcement
Nov 2, 2026
Merged

jlav merged 2 commits into
mainfrom
jl/license-enforcement

Conversation

@jlav

@jlav jlav commented Nov 1, 2026 •

Copy link
Copy Markdown
Member

HUMAN:

  • A human has tested these changes.

AGENT:


Why

OHE needs to know whether its license key is good and what the account is entitled to. Customer Center now answers that with a signed payload (customer-center#66).

This adds the OHE side: a module that checks the key and that other code can call to enforce the license, plus a banner on the settings pages that shows the license state. Enforcement is off by default, so it can ship before rollout.

Summary

  • server/licensing checks OPENHANDS_LICENSE_KEY with Customer Center on startup and then every LICENSE_CHECK_INTERVAL_SECONDS (30 minutes by default). It keeps the last trusted answer in memory.
  • Code that enforces the license calls is_licensed(), has_feature() and within_limit(), or the require_license / require_feature dependencies. With LICENSE_ENFORCEMENT_ENABLED off, they all allow and no check is made.
  • GET /api/license/status feeds a banner at the very top of the settings pages. It shows a missing or invalid key, an unverified license, the trial end date, or an overdue payment, and can't be dismissed.

Issue Number

Part of PLTF-3962.

How to Test

  1. Run customer-center#66 locally with the staging signing key (LICENSE_SIGNING_KEY_ID=staging-1, and LICENSE_SIGNING_KEY from the staging customer-center-app secret in saas-deploy). Sign up to get a license key.
  2. Start OHE with LICENSE_ENFORCEMENT_ENABLED=true, OPENHANDS_LICENSE_KEY=<key>, LICENSE_SERVER_URL=<customer center> and a short LICENSE_CHECK_INTERVAL_SECONDS (60 is the minimum).
  3. Open /settings. The banner shows the trial end date.
  4. Move the account's trial_ends_at into the past in Customer Center's database. After the next check, the banner says the trial has ended.

Unit tests: uv run pytest tests/unit/server/licensing tests/unit/server/routes/test_license.py and npm run test -- license-banner.

Video/Screenshots

See Demo below.

Type

  • Bug fix
  • Feature
  • Refactor
  • Breaking change
  • Docs / chore

Notes

OHE trusts an answer only when the Ed25519 signature verifies, key_sha256 matches its own key, and expires_at is in the future. If Customer Center is unreachable or sends something that doesn't verify, OHE keeps the last trusted answer until it expires.

The payload models ignore unknown fields and keep plan, state and reason as plain strings, so Customer Center can add values in v1 without breaking older installs.

The public keys OHE trusts are baked into the code, in TRUSTED_PUBLIC_KEYS (server/licensing/verification.py). No setting can add or replace one. LICENSE_SERVER_URL can point anywhere, and a server there still can't sign answers OHE accepts. A new key, such as production's, ships in a release.

The only trusted key so far is staging-1, added to staging in OpenHands/saas-deploy#1872. Nothing calls the enforcement helpers yet.

Demo

OHE checks a trial key with a local Customer Center (customer-center#66, signing with the staging key) and shows the trial end date on every settings page. The trial end is then moved into the past in Customer Center's database, and the next check turns the banner into "Your OpenHands Enterprise trial has ended."

PLTF-3962-license-enforcement.webm

Enterprise server image for this PR:

ghcr.io/openhands/enterprise-server:sha-fc25e72

@jlav jlav changed the title feat(licensing): check the license with customer center feat(licensing): check the license with Customer Center Nov 1, 2026
@github-actions github-actions Bot added the type: feat A new feature label Nov 1, 2026
@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  server/app_lifespan
  saas_app_lifespan_service.py 204
  server/licensing
  __init__.py
  config.py
  service.py 209-210, 227-228
  verification.py 130
  server/routes
  license.py
Project Total  

This report was generated by python-coverage-comment-action

@jlav
jlav marked this pull request as ready for review November 2, 2026 16:20
@jlav
jlav merged commit ba48efd into main Nov 2, 2026
28 checks passed
@jlav
jlav deleted the jl/license-enforcement branch November 2, 2026 16:20
@openhands-release-bot openhands-release-bot Bot added the released: 1.83.0 Shipped in 1.83.0 label Nov 2, 2026
@openhands-release-bot

Copy link
Copy Markdown
Contributor

🚀 Released in 1.83.0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

released: 1.83.0 Shipped in 1.83.0 type: feat A new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants