ScenarioForge — licensing & entitlements
Marketing / product name is ScenarioForge. The on-disk identifier (Python package, CLI, env vars) stays
scenariogenfor backwards compatibility. Seedocs/BRAND.mdfor the naming rule.
scenariogen ships with an enterprise licensing layer that gates trackers,
features, projects, seats, and monthly run quotas. When no licence is
configured the tool runs in community mode (Jira + CSV ingest, single
project, no enterprise features) — exactly the behaviour shipped before
this layer was added. Existing pipelines are not affected.
Licence token format
Tokens are compact, JWS-like, Ed25519-signed strings:
base64url(header) "." base64url(payload) "." base64url(signature)
- Header:
{"alg":"Ed25519","typ":"SCEN-LIC"} - Payload: the
Licensemodel (scenariogen/licensing/models.py) - Signature: Ed25519 over
header.payload
Only the vendor holds the private key. Customers ship with the bundled
public key (scenariogen/licensing/keys/scenariogen_pub.pem), or override
it via SCENARIOGEN_LICENSE_PUBLIC_KEY_FILE for air-gapped installs.
Configuration
| Env var | Purpose |
|---|---|
SCENARIOGEN_LICENSE_KEY |
Inline licence token. Wins over the file. |
SCENARIOGEN_LICENSE_FILE |
Path to a file containing the token. Falls back to ~/.scenariogen/license.jwt. |
SCENARIOGEN_LICENSE_PUBLIC_KEY |
Inline public key PEM (rare; air-gap rotation). |
SCENARIOGEN_LICENSE_PUBLIC_KEY_FILE |
Path to override the bundled public key. |
Monthly run counters persist under <SCENARIOGEN_CACHE_DIR>/license/runs.json.
CLI
scenariogen license show # tabular view of the active licence
scenariogen license verify # exit 0 if valid, non-zero otherwise
Tiers and entitlements
| Tier | Trackers | Projects | Seats | Runs/mo | Notes |
|---|---|---|---|---|---|
| community | jira | 1 | n/a | unlimited | default when no licence is present |
| team | 1 | 3 | 5 | 200 | single squad |
| pro | up to 3 | 10 | 20 | 1,000 | multi-squad QA org |
| business | up to 6 | 30 | 75 | 5,000 | regulated / multi-tracker |
| enterprise | unlimited | unl | unl | unl | air-gap, custom adapters |
Tiers are advisory — the actual limits in any issued licence are whatever
fields the vendor signs into the token (max_projects, max_seats,
max_runs_per_month, trackers, features).
Programmatic gating
from scenariogen.licensing import get_entitlements
ent = get_entitlements()
ent.require_tracker("linear") # raises EntitlementError if not licensed
ent.require_feature("sso")
ent.check_project_count(active_projects)
ent.record_run() # increments monthly counter, enforces quota
Call sites:
- ingest.*_client resolvers should call require_tracker(kind) before
hitting the API.
- orchestrator.run_pipeline should call record_run() at the start of
each run.
- Server admin endpoints should check require_feature("sso") /
require_feature("audit_export") before exposing those routes.
Vendor: issuing a licence
The vendor private key is not shipped with the package. Issuance is a manual offline step.
Bootstrap a keypair once (commit the public PEM only):
python scripts/issue_license.py generate-keypair \
--private-out vendor-secrets/scenariogen_license.private.pem \
--public-out scenariogen/licensing/keys/scenariogen_pub.pem
Mint a customer licence:
python scripts/issue_license.py issue \
--private-key vendor-secrets/scenariogen_license.private.pem \
--customer "Acme Bank" \
--tier business \
--trackers jira,linear,ado \
--features sso,audit_export \
--max-projects 30 --max-seats 75 --max-runs-per-month 5000 \
--days 365
The customer pastes the printed token into SCENARIOGEN_LICENSE_KEY or
~/.scenariogen/license.jwt.
Air-gap considerations
- Licences are self-contained — no phone-home, no network check at verify time.
grace_dayslets customers run pastexpires_atfor a configurable buffer before the tool hard-stops. Default 30 days.- Public-key rotation: ship a new bundled key in the next release; old tokens continue to verify against the old key until rotation completes.
BYOK — bring your own key
scenariogen is BYOK by design. All LLM, embedding, and web-search
calls go directly from the customer's environment to the customer's own
provider account using credentials they configure (OPENAI_API_KEY,
ANTHROPIC_API_KEY, AZURE_OPENAI_*, AWS_BEDROCK_*,
GOOGLE_API_KEY, OPENAI_COMPATIBLE_BASE_URL, TAVILY_API_KEY).
The licence layer therefore never: - proxies LLM traffic, - holds or rotates customer LLM keys, - meters tokens, prompts, completions, or any provider-side spend, - charges margin on inference.
Quota gating in this licence layer is run-level (max_runs_per_month)
— a coarse usage signal that lets us tier the product without touching
the BYOK pipe. If a future tier ever needs finer-grained limits, they
must still respect BYOK: count runs, durations, or work-items — not
tokens.
This is a non-negotiable product constraint. Any new wedge (packaging, auth, observability, multi-tracker) must preserve it.
What this layer does NOT do
- Does not meter the content customers produce. We never see scenarios, requirements, or run logs.
- Does not validate the licence over the network.
- Does not phone home with telemetry.
- Does not touch LLM credentials or LLM traffic (see BYOK above).