Add signed licensing enforcement and ISO notes

This commit is contained in:
Jino Jose committed 2026-07-08 10:47:49 +05:30
1 parent dee4486826
commit d645249a13
14 files changed
+1526 -103

No files matched your search

@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxOGixLLE9iwPNngpX2gr
pUiVPHN0//LcBUovRsKr1qq5xXRxhMmE1Fgux8yMHBlTLRCKdd4wH1N3E7EGMg9T
vHeLQFZJ8uGkK2U7X9nY6h9prAe9VVNvz6OwdYQqxPbttW723w2cy2p2/Jdxry5x
9iqJ3Q084cDyT30QHdkhqGTiYFAJ7+K95acktuKUs/A2WuHgEmhbK8aCWW2kQzLS
x50aFjxGwXbHydXG7D0WJYCXKPjTXusjTNuPopjUH3Yp9xqieGqqxSFXFkIXdrgc
wkpMmgJEz/RQlG+fFkGd3VdoxD3taRMamuVky+9Kf6MbMDZKoxte4gf2FjXav9j8
mQIDAQAB
-----END PUBLIC KEY-----
@@ -0,0 +1,321 @@
import base64
import copy
import json
import os
import subprocess
import tempfile
from datetime import datetime, timezone
from pathlib import Path
TIER_ORDER = ("starter", "basic", "pro", "max")
TIER_ALIASES = {
"entry": "basic",
"entry tier": "basic",
"mid": "pro",
"mid tier": "pro",
"advanced": "max",
"advanced tier": "max",
"starter tier": "starter",
"basic tier": "basic",
"pro tier": "pro",
"max tier": "max",
}
STAGING_MAX_TIER = "basic"
SUPPORTED_SIGNATURE_ALGS = {"rsa-sha256", "sha256-rsa"}
def utcnow_iso():
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
def parse_time(value):
text = (value or "").strip()
if not text:
return None
if len(text) == 10:
text += "T00:00:00Z"
if text.endswith("Z"):
text = text[:-1] + "+00:00"
try:
return datetime.fromisoformat(text)
except Exception:
return None
def normalize_tier(value, default="basic"):
raw = (value or "").strip().lower()
if raw in TIER_ORDER:
return raw
return TIER_ALIASES.get(raw, default)
def tier_rank(tier):
return TIER_ORDER.index(normalize_tier(tier))
def tier_lte(left, right):
return tier_rank(left) <= tier_rank(right)
def min_tier(*tiers):
items = [normalize_tier(t) for t in tiers if t]
if not items:
return STAGING_MAX_TIER
return sorted(items, key=tier_rank)[0]
def read_json_file(path):
try:
data = json.loads(Path(path).read_text())
return data if isinstance(data, dict) else {}
except Exception:
return {}
def write_json_file(path, payload):
p = Path(path)
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(json.dumps(payload, indent=2))
def _canonical_payload(payload):
clean = copy.deepcopy(payload or {})
for key in ("signature", "signature_status", "verification_status"):
clean.pop(key, None)
return json.dumps(clean, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
def verify_signature(payload, public_key_path):
if not payload:
return False, "missing"
signature = (payload.get("signature") or "").strip()
if not signature:
return False, "missing_signature"
alg = (payload.get("signature_alg") or "").strip().lower()
if alg not in SUPPORTED_SIGNATURE_ALGS:
return False, "unsupported_signature_alg"
key_path = Path(public_key_path)
if not key_path.exists():
return False, "missing_public_key"
canonical = _canonical_payload(payload).encode()
try:
sig_bytes = base64.b64decode(signature, validate=True)
except Exception:
return False, "bad_signature_encoding"
with tempfile.NamedTemporaryFile(delete=False) as data_file, tempfile.NamedTemporaryFile(delete=False) as sig_file:
data_file.write(canonical)
sig_file.write(sig_bytes)
data_file.flush()
sig_file.flush()
data_path = data_file.name
sig_path = sig_file.name
try:
result = subprocess.run(
["openssl", "dgst", "-sha256", "-verify", str(key_path), "-signature", sig_path, data_path],
capture_output=True,
text=True,
check=False,
)
return result.returncode == 0, "valid" if result.returncode == 0 else "invalid_signature"
finally:
for tmp in (data_path, sig_path):
try:
os.unlink(tmp)
except OSError:
pass
def public_license_record(record):
public = {k: v for k, v in (record or {}).items() if k != "signature"}
if record.get("license_key"):
public["license_key_prefix"] = str(record.get("license_key"))[:12]
public["license_key_present"] = True
else:
public["license_key_present"] = False
return public
def evaluate_license(payload, public_key_path, machine_id=None, now=None):
now_dt = parse_time(now) if isinstance(now, str) else now or datetime.now(timezone.utc)
base = {
"schema": "cezen.license.v2",
"status": "missing",
"valid": False,
"allowed_tier": STAGING_MAX_TIER,
"feature_overrides": {},
"install_type": "field-staging",
"needs_activation": True,
"license_record": {},
"notes": [],
}
if not payload:
return base
record = payload if isinstance(payload, dict) else {}
base["license_record"] = public_license_record(record)
base["install_type"] = (record.get("install_type") or "licensed").strip() or "licensed"
base["allowed_tier"] = normalize_tier(record.get("allowed_tier") or record.get("tier"), STAGING_MAX_TIER)
overrides = record.get("feature_overrides") or {}
base["feature_overrides"] = overrides if isinstance(overrides, dict) else {}
ok, sig_status = verify_signature(record, public_key_path)
if not ok:
base["status"] = "invalid_signature" if sig_status != "missing_signature" else "missing"
base["notes"].append(sig_status)
return base
issued_at = parse_time(record.get("issued_at"))
if issued_at and issued_at > now_dt:
base["status"] = "not_yet_valid"
base["notes"].append("issued_at_in_future")
return base
expires_at = parse_time(record.get("expires_at"))
if expires_at and expires_at < now_dt:
base["status"] = "expired"
base["notes"].append("license_expired")
return base
machine_binding = record.get("machine_binding") or {}
if machine_id and isinstance(machine_binding, dict):
bound_id = (machine_binding.get("machine_id") or "").strip()
if bound_id and bound_id != machine_id:
base["status"] = "machine_mismatch"
base["notes"].append("machine_binding_mismatch")
return base
base["status"] = "valid"
base["valid"] = True
base["needs_activation"] = False
return base
def evaluate_override(payload, public_key_path, now=None):
if not payload:
return {"status": "missing", "valid": False, "allow_hardware_mismatch": False, "max_override_tier": None}
record = payload if isinstance(payload, dict) else {}
ok, sig_status = verify_signature(record, public_key_path)
if not ok:
return {
"status": "invalid_signature" if sig_status != "missing_signature" else "missing",
"valid": False,
"allow_hardware_mismatch": False,
"max_override_tier": None,
}
expires_at = parse_time(record.get("expires_at"))
now_dt = parse_time(now) if isinstance(now, str) else now or datetime.now(timezone.utc)
if expires_at and expires_at < now_dt:
return {"status": "expired", "valid": False, "allow_hardware_mismatch": False, "max_override_tier": None}
return {
"status": "valid",
"valid": True,
"allow_hardware_mismatch": bool(record.get("allow_hardware_mismatch")),
"max_override_tier": normalize_tier(record.get("max_override_tier"), "max"),
"override_record": public_license_record(record),
}
def build_tier_options(license_eval, hardware_tier, override_eval=None):
override_eval = override_eval or {}
licensed_cap = normalize_tier(license_eval.get("allowed_tier"), STAGING_MAX_TIER)
hardware_cap = normalize_tier(hardware_tier, STAGING_MAX_TIER)
override_cap = normalize_tier(override_eval.get("max_override_tier"), "max") if override_eval.get("allow_hardware_mismatch") else None
options = []
for tier in TIER_ORDER:
state = "enabled"
reason = ""
if not tier_lte(tier, licensed_cap):
state = "disabled_by_license"
reason = f"Not included in current license ({licensed_cap})."
elif not tier_lte(tier, hardware_cap):
if override_cap and tier_lte(tier, override_cap):
state = "override_required"
reason = f"Requires Cezen override; hardware recommendation is {hardware_cap}."
else:
state = "disabled_by_hardware"
reason = f"Requires larger hardware; recommendation is {hardware_cap}."
options.append({"tier": tier, "state": state, "reason": reason, "selectable": state in {"enabled", "override_required"}})
return options
def provisioned_components_from_record(record):
record = record or {}
components = record.get("components") or {}
if isinstance(components, dict):
return components
skip_roles = set(record.get("skip_roles") or [])
return {
"ollama": "ollama" not in skip_roles,
"jupyterlab": "jupyterlab" not in skip_roles,
"chromadb": "chromadb" not in skip_roles,
"vllm": "vllm" not in skip_roles,
"mlflow": "mlflow" not in skip_roles,
"minio": "minio" not in skip_roles,
"monitoring": "monitoring" not in skip_roles,
"k3s": "k3s" not in skip_roles,
}
def resolve_effective_features(base_features, feature_overrides, provisioned_components, hardware_features):
features = copy.deepcopy(base_features or {})
for key, value in (feature_overrides or {}).items():
features[key] = value
provisioned_components = provisioned_components or {}
hardware_features = hardware_features or {}
if features.get("rag") and not provisioned_components.get("chromadb", True):
features["rag"] = False
if features.get("gpu_inference") and not (
hardware_features.get("ollama_gpu") or hardware_features.get("vllm")
):
features["gpu_inference"] = False
if features.get("fine_tuning") and not (
provisioned_components.get("jupyterlab", False) and hardware_features.get("fine_tuning_qlora")
):
features["fine_tuning"] = False
if features.get("deepspeed") and not hardware_features.get("distributed_training"):
features["deepspeed"] = False
return features
def collect_entitlement(tier_matrix, license_record, install_record, feasibility, public_key_path, machine_id=None):
license_eval = evaluate_license(license_record, public_key_path, machine_id=machine_id)
override_eval = evaluate_override((install_record or {}).get("override_record") or read_json_file((install_record or {}).get("override_path", "")), public_key_path)
licensed_tier = normalize_tier(license_eval.get("allowed_tier"), STAGING_MAX_TIER)
provisioned_tier = normalize_tier((install_record or {}).get("provisioned_tier") or (install_record or {}).get("selected_tier"), STAGING_MAX_TIER)
hardware_tier = normalize_tier(((feasibility or {}).get("recommendation") or {}).get("recommended_tier"), STAGING_MAX_TIER)
components = provisioned_components_from_record(install_record)
tier_defaults = copy.deepcopy((tier_matrix or {}).get(licensed_tier, {}))
effective_features = resolve_effective_features(
tier_defaults.get("features", {}),
license_eval.get("feature_overrides", {}),
components,
(feasibility or {}).get("features", {}),
)
states = []
if tier_rank(licensed_tier) > tier_rank(provisioned_tier):
states.append("licensed_not_provisioned")
if tier_rank(provisioned_tier) > tier_rank(licensed_tier):
states.append("provisioned_above_license")
if tier_rank(provisioned_tier) > tier_rank(hardware_tier) and not override_eval.get("allow_hardware_mismatch"):
states.append("hardware_below_provisioned")
if license_eval.get("status") != "valid":
states.append(license_eval.get("status"))
return {
"license_status": license_eval.get("status"),
"license_valid": license_eval.get("valid"),
"license_record": public_license_record(license_record),
"licensed_tier": licensed_tier,
"provisioned_tier": provisioned_tier,
"hardware_recommended_tier": hardware_tier,
"effective_features": effective_features,
"feature_overrides": license_eval.get("feature_overrides", {}),
"provisioned_components": components,
"compliance_state": states or ["licensed_and_provisioned"],
"override": override_eval,
"install_type": license_eval.get("install_type") or (install_record or {}).get("install_type") or "field-staging",
}
+208 -41
View File
@@ -27,6 +27,15 @@ import psutil
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.interval import IntervalTrigger
from cezen_license import (
STAGING_MAX_TIER,
collect_entitlement,
evaluate_license,
normalize_tier as license_normalize_tier,
read_json_file,
tier_lte,
write_json_file,
)
# ── Config ────────────────────────────────────────────────────────────────────
@@ -35,6 +44,8 @@ DB_PATH = DATA_DIR / "cezen.db"
SECRET_FILE = DATA_DIR / ".jwt_secret"
BACKUP_DIR = Path(os.environ.get("CEZEN_BACKUP_DIR", str(DATA_DIR.parent / "backups")))
LICENSE_FILE = Path(os.environ.get("CEZEN_LICENSE_JSON", "/opt/cezen/license.json"))
INSTALL_RECORD_FILE = Path(os.environ.get("CEZEN_INSTALL_RECORD_JSON", "/opt/cezen/install-record.json"))
LICENSE_PUBLIC_KEY_FILE = Path(os.environ.get("CEZEN_LICENSE_PUBLIC_KEY", str(Path(__file__).with_name("cezen-license-public.pem"))))
DATA_DIR.mkdir(parents=True, exist_ok=True)
BACKUP_DIR.mkdir(parents=True, exist_ok=True)
@@ -821,23 +832,16 @@ def _setting_value(key: str, default: str = "") -> str:
db.close()
def _normalize_tier(value: str) -> str:
raw = (value or "").strip().lower()
if raw in TIER_MATRIX:
return raw
return TIER_ALIASES.get(raw, "basic")
return license_normalize_tier(value, "basic")
def _license_record() -> dict:
try:
if LICENSE_FILE.exists():
data = json.loads(LICENSE_FILE.read_text())
if isinstance(data, dict):
return data
except Exception:
pass
return {}
return read_json_file(LICENSE_FILE)
def _install_record() -> dict:
return read_json_file(INSTALL_RECORD_FILE)
def _public_license_record(record: dict) -> dict:
public = {k: v for k, v in (record or {}).items() if k != "license_key"}
public = {k: v for k, v in (record or {}).items() if k not in {"license_key", "signature"}}
if record.get("license_key"):
public["license_key_prefix"] = record["license_key"][:12]
public["license_key_present"] = True
@@ -848,26 +852,60 @@ def _public_license_record(record: dict) -> dict:
def _current_tier() -> str:
if CEZEN_TIER:
return _normalize_tier(CEZEN_TIER)
license_tier = (_license_record().get("tier") or "").strip()
if license_tier:
return _normalize_tier(license_tier)
entitlement = _entitlement_summary()
licensed_tier = (entitlement.get("licensed_tier") or "").strip()
if licensed_tier:
return _normalize_tier(licensed_tier)
return _normalize_tier(_setting_value("tier_label", "Basic"))
def _entitlement_summary(feasibility: Optional[dict] = None) -> dict:
feasibility = feasibility or _cached_feasibility()
return collect_entitlement(
TIER_MATRIX,
_license_record(),
_install_record(),
feasibility,
str(LICENSE_PUBLIC_KEY_FILE),
)
def _tier_payload() -> dict:
tier = _current_tier()
entitlement = _entitlement_summary()
tier = entitlement.get("licensed_tier") or _current_tier()
info = TIER_MATRIX[tier]
license_record = _license_record()
return {
"tier": tier,
"label": info["label"],
"locked": bool(CEZEN_TIER or license_record.get("tier")),
"locked": bool(CEZEN_TIER or license_record.get("allowed_tier") or license_record.get("tier")),
"positioning": info["positioning"],
"max_users": info["max_users"],
"features": info["features"],
"features": entitlement.get("effective_features", info["features"]),
"feature_overrides": entitlement.get("feature_overrides", {}),
"provisioned_tier": entitlement.get("provisioned_tier"),
"hardware_recommended_tier": entitlement.get("hardware_recommended_tier"),
"compliance_state": entitlement.get("compliance_state", []),
"license_status": entitlement.get("license_status"),
"override": entitlement.get("override", {}),
"tiers": TIER_MATRIX,
"license_record": _public_license_record(license_record),
}
def _feature_enabled(feature_key: str, feasibility: Optional[dict] = None) -> bool:
entitlement = _entitlement_summary(feasibility)
value = (entitlement.get("effective_features") or {}).get(feature_key)
if isinstance(value, bool):
return value
if isinstance(value, str):
return value.lower() not in {"", "false", "no", "none", "disabled", "optional"}
return bool(value)
def require_feature(feature_key: str):
def dependency(user: dict = Depends(current_user)):
if not _feature_enabled(feature_key):
raise HTTPException(status_code=403, detail=f"Feature '{feature_key}' is not licensed or provisioned")
return user
return dependency
def _readiness_score(feasibility: dict, license_info: dict) -> dict:
features = feasibility.get("features") or {}
recommendation = feasibility.get("recommendation") or {}
@@ -1717,17 +1755,141 @@ async def system_feasibility(admin: dict = Depends(admin_only)):
},
}
def _cached_feasibility() -> dict:
paths = [
Path(os.environ.get("CEZEN_FEASIBILITY_JSON", "")),
DATA_DIR / "feasibility.json",
Path("/opt/cezen/feasibility.json"),
]
for p in paths:
if p and str(p) != "." and p.exists():
try:
return json.loads(p.read_text())
except Exception:
pass
metrics_now = collect_metrics()
gpu_vram = metrics_now.get("gpu_mem_total_gb") or 0
return {
"schema": "cezen.feasibility.cached-fallback.v1",
"generated_at": utcnow(),
"recommendation": {
"recommended_tier": STAGING_MAX_TIER,
"recommended_profile": "core",
"estimated_concurrent_users": "1-2",
"notes": [],
},
"features": {
"ollama_gpu": gpu_vram >= 8,
"vllm": gpu_vram >= 24,
"fine_tuning_qlora": gpu_vram >= 24,
"distributed_training": False,
},
}
@app.get("/api/license")
async def license_info(user: dict = Depends(current_user)):
return _tier_payload()
payload = _tier_payload()
payload["entitlement"] = _entitlement_summary()
return payload
@app.post("/api/license/upload")
async def upload_license(body: dict, request: Request, admin: dict = Depends(admin_only)):
raw_license = (body.get("license_json") or "").strip()
raw_override = (body.get("override_json") or "").strip()
if not raw_license:
raise HTTPException(400, "license_json required")
try:
license_record = json.loads(raw_license)
if not isinstance(license_record, dict):
raise ValueError("license_json must be a JSON object")
except Exception as exc:
raise HTTPException(400, f"Invalid license_json: {exc}")
if raw_override:
try:
override_record = json.loads(raw_override)
if not isinstance(override_record, dict):
raise ValueError("override_json must be a JSON object")
except Exception as exc:
raise HTTPException(400, f"Invalid override_json: {exc}")
write_json_file(Path("/opt/cezen/license.override.json"), override_record)
else:
Path("/opt/cezen/license.override.json").unlink(missing_ok=True)
write_json_file(LICENSE_FILE, license_record)
entitlement = _entitlement_summary()
db = get_db()
audit(
db,
admin["sub"],
admin["username"],
"license_upload",
f"status={entitlement.get('license_status')} licensed_tier={entitlement.get('licensed_tier')}",
request.client.host if request.client else "",
"success" if entitlement.get("license_valid") else "failure",
)
db.commit()
db.close()
return {"ok": True, "entitlement": entitlement}
@app.post("/api/license/reverify")
async def reverify_license(request: Request, admin: dict = Depends(admin_only)):
entitlement = _entitlement_summary()
db = get_db()
audit(
db,
admin["sub"],
admin["username"],
"license_reverify",
f"status={entitlement.get('license_status')}",
request.client.host if request.client else "",
"success" if entitlement.get("license_valid") else "failure",
)
db.commit()
db.close()
return {"ok": True, "entitlement": entitlement}
@app.post("/api/license/reconcile")
async def reconcile_license(request: Request, admin: dict = Depends(admin_only)):
entitlement = _entitlement_summary()
record = _install_record()
requested_tier = entitlement.get("licensed_tier") or record.get("provisioned_tier") or STAGING_MAX_TIER
pending_roles = []
if not tier_lte(requested_tier, record.get("provisioned_tier") or STAGING_MAX_TIER):
if requested_tier in {"pro", "max"} and not (record.get("components") or {}).get("vllm", False):
pending_roles.append("vllm")
if requested_tier == "max":
for role in ("mlflow", "minio", "k3s"):
if not (record.get("components") or {}).get(role, False):
pending_roles.append(role)
reconcile_record = {
"schema": "cezen.reconcile_request.v1",
"requested_at": utcnow(),
"requested_by": admin["username"],
"target_tier": requested_tier,
"pending_roles": pending_roles,
"status": "pending" if pending_roles else "noop",
}
write_json_file(Path("/opt/cezen/reconcile-request.json"), reconcile_record)
db = get_db()
audit(
db,
admin["sub"],
admin["username"],
"license_reconcile",
f"target_tier={requested_tier} pending_roles={','.join(pending_roles) or 'none'}",
request.client.host if request.client else "",
)
db.commit()
db.close()
return {"ok": True, "reconcile": reconcile_record, "entitlement": entitlement}
@app.get("/api/system/readiness-report")
async def readiness_report(admin: dict = Depends(admin_only)):
feasibility = await system_feasibility(admin)
license_payload = _tier_payload()
entitlement = _entitlement_summary(feasibility)
readiness = _readiness_score(feasibility, license_payload)
recommended_tier = (feasibility.get("recommendation") or {}).get("recommended_tier", "starter")
current_rank = list(TIER_MATRIX.keys()).index(license_payload["tier"])
current_rank = list(TIER_MATRIX.keys()).index(entitlement.get("licensed_tier") or license_payload["tier"])
recommended_rank = list(TIER_MATRIX.keys()).index(_normalize_tier(recommended_tier))
commercial_fit = "matched" if current_rank >= recommended_rank else "license_upgrade_recommended"
return {
@@ -1735,11 +1897,13 @@ async def readiness_report(admin: dict = Depends(admin_only)):
"generated_at": utcnow(),
"customer_mode": "software_only" if (feasibility.get("features") or {}).get("software_only", True) else "appliance",
"license": license_payload,
"entitlement": entitlement,
"feasibility": feasibility,
"readiness": readiness,
"commercial_fit": {
"status": commercial_fit,
"current_tier": license_payload["tier"],
"current_tier": entitlement.get("licensed_tier") or license_payload["tier"],
"provisioned_tier": entitlement.get("provisioned_tier"),
"recommended_tier": _normalize_tier(recommended_tier),
"note": "Current license covers the recommended deployment." if commercial_fit == "matched" else "Quote a higher tier or reduce enabled features for this hardware.",
},
@@ -1829,8 +1993,9 @@ async def get_branding():
db.close()
result = {r["key"]: r["value"] for r in rows}
license_record = _license_record()
entitlement = _entitlement_summary()
# If Cezen has locked the tier via env var, override whatever is in DB
if CEZEN_TIER or license_record.get("tier"):
if CEZEN_TIER or license_record.get("allowed_tier") or license_record.get("tier"):
tier = _current_tier()
result["tier_label"] = TIER_MATRIX[tier]["label"]
result["tier_slug"] = tier
@@ -1842,13 +2007,15 @@ async def get_branding():
result["license_customer_id"] = license_record.get("customer_id", "")
result["license_key_prefix"] = (license_record.get("license_key", "") or "")[:12]
result["license_support_until"] = license_record.get("support_until", "")
result["license_status"] = entitlement.get("license_status", "missing")
result["provisioned_tier"] = entitlement.get("provisioned_tier", "")
return result
@app.put("/api/settings/branding")
async def update_branding(body: dict, admin: dict = Depends(admin_only)):
# tier_label is always excluded from customer-editable fields when locked
allowed = {"org_name","stack_name","logo_url","accent_color","footer_text","support_email"}
if not CEZEN_TIER:
if not (CEZEN_TIER or _license_record().get("allowed_tier") or _license_record().get("tier")):
# Only allow tier changes when NOT locked by env var (dev/demo mode)
allowed.add("tier_label")
db = get_db()
@@ -2149,7 +2316,7 @@ def _count_rows(path: Path, orig_name: str) -> int:
return 0
@app.post("/api/training/datasets")
async def upload_dataset(file: UploadFile = File(...), admin: dict = Depends(admin_only)):
async def upload_dataset(file: UploadFile = File(...), _licensed: dict = Depends(require_feature("fine_tuning")), admin: dict = Depends(admin_only)):
"""Upload a JSONL or CSV dataset for fine-tuning."""
allowed_ext = {".jsonl", ".json", ".csv"}
orig = file.filename or "dataset"
@@ -2180,7 +2347,7 @@ async def upload_dataset(file: UploadFile = File(...), admin: dict = Depends(adm
return {"id": row_id, "filename": orig, "size_bytes": len(data), "row_count": row_count}
@app.get("/api/training/datasets")
async def list_datasets(admin: dict = Depends(admin_only)):
async def list_datasets(_licensed: dict = Depends(require_feature("fine_tuning")), admin: dict = Depends(admin_only)):
db = get_db()
rows = db.execute(
"SELECT id, orig_name, size_bytes, row_count, uploaded_at FROM training_datasets ORDER BY uploaded_at DESC"
@@ -2189,7 +2356,7 @@ async def list_datasets(admin: dict = Depends(admin_only)):
return {"datasets": [dict(r) for r in rows]}
@app.delete("/api/training/datasets/{dataset_id}")
async def delete_dataset(dataset_id: int, admin: dict = Depends(admin_only)):
async def delete_dataset(dataset_id: int, _licensed: dict = Depends(require_feature("fine_tuning")), admin: dict = Depends(admin_only)):
db = get_db()
row = db.execute("SELECT * FROM training_datasets WHERE id=?", (dataset_id,)).fetchone()
if not row:
@@ -2212,7 +2379,7 @@ async def delete_dataset(dataset_id: int, admin: dict = Depends(admin_only)):
return {"ok": True}
@app.post("/api/training/jobs")
async def launch_job(body: dict, admin: dict = Depends(admin_only)):
async def launch_job(body: dict, _licensed: dict = Depends(require_feature("fine_tuning")), admin: dict = Depends(admin_only)):
"""Launch a QLoRA fine-tuning job as a background subprocess."""
required = {"name", "base_model", "dataset_id"}
if not required.issubset(body):
@@ -2284,7 +2451,7 @@ async def launch_job(body: dict, admin: dict = Depends(admin_only)):
return {"job_id": job_id, "status": "running"}
@app.get("/api/training/jobs")
async def list_jobs(admin: dict = Depends(admin_only)):
async def list_jobs(_licensed: dict = Depends(require_feature("fine_tuning")), admin: dict = Depends(admin_only)):
db = get_db()
rows = db.execute(
"""SELECT j.id, j.name, j.base_model, j.status, j.config_json,
@@ -2303,7 +2470,7 @@ async def list_jobs(admin: dict = Depends(admin_only)):
return {"jobs": result}
@app.get("/api/training/jobs/{job_id}")
async def get_job(job_id: int, admin: dict = Depends(admin_only), tail: int = 200):
async def get_job(job_id: int, _licensed: dict = Depends(require_feature("fine_tuning")), admin: dict = Depends(admin_only), tail: int = 200):
db = get_db()
row = db.execute(
"""SELECT j.*, d.orig_name as dataset_name
@@ -2338,7 +2505,7 @@ async def get_job(job_id: int, admin: dict = Depends(admin_only), tail: int = 20
return item
@app.delete("/api/training/jobs/{job_id}")
async def cancel_job(job_id: int, admin: dict = Depends(admin_only)):
async def cancel_job(job_id: int, _licensed: dict = Depends(require_feature("fine_tuning")), admin: dict = Depends(admin_only)):
db = get_db()
row = db.execute("SELECT * FROM training_jobs WHERE id=?", (job_id,)).fetchone()
if not row:
@@ -2723,7 +2890,7 @@ class BenchmarkRun(BaseModel):
max_tokens: int = 256
@app.post("/api/benchmark/run")
async def run_benchmark(body: BenchmarkRun, admin: dict = Depends(admin_only)):
async def run_benchmark(body: BenchmarkRun, _licensed: dict = Depends(require_feature("gpu_inference")), admin: dict = Depends(admin_only)):
"""
Runs each prompt against each model and returns timing + response.
Calls Ollama /api/generate for each (model, prompt) pair sequentially.
@@ -5171,14 +5338,14 @@ async def run_workflow_api(workflow_id: str, body: dict, user: dict = Depends(cu
return result
@app.get("/api/connectors")
async def list_connectors(user: dict = Depends(current_user)):
async def list_connectors(user: dict = Depends(require_feature("connectors"))):
db = get_db()
rows = db.execute("SELECT * FROM connectors ORDER BY updated_at DESC").fetchall()
db.close()
return [_connector_from_row(r) for r in rows]
@app.put("/api/connectors")
async def save_connectors(body: list[dict], user: dict = Depends(current_user)):
async def save_connectors(body: list[dict], user: dict = Depends(require_feature("connectors"))):
now = utcnow()
db = get_db()
seen = []
@@ -5284,7 +5451,7 @@ def _connector_db_stats(cfg: dict) -> tuple[int, int]:
raise ValueError(f"Unsupported database connector type: {db_type}")
@app.post("/api/connectors/{connector_id}/sync")
async def sync_connector(connector_id: str, user: dict = Depends(current_user)):
async def sync_connector(connector_id: str, user: dict = Depends(require_feature("connectors"))):
db = get_db()
row = db.execute("SELECT * FROM connectors WHERE id=?", (connector_id,)).fetchone()
if not row:
@@ -5327,14 +5494,14 @@ async def sync_connector(connector_id: str, user: dict = Depends(current_user)):
return {"ok": True, "files": stats.get("files", 0), "rowsRead": stats.get("rowsRead", 0), "status": status}
@app.get("/api/connectors/log")
async def connector_log(limit: int = 30, user: dict = Depends(current_user)):
async def connector_log(limit: int = 30, user: dict = Depends(require_feature("connectors"))):
db = get_db()
rows = db.execute("SELECT level,msg,ts FROM connector_log ORDER BY id DESC LIMIT ?", (limit,)).fetchall()
db.close()
return {"lines": [dict(r) for r in reversed(rows)]}
@app.get("/api/router/rules")
async def get_router_rules(user: dict = Depends(current_user)):
async def get_router_rules(user: dict = Depends(require_feature("model_router"))):
db = get_db()
rows = db.execute("SELECT * FROM router_rules ORDER BY priority, id").fetchall()
db.close()
@@ -5348,7 +5515,7 @@ async def get_router_rules(user: dict = Depends(current_user)):
return {"routes": routes}
@app.put("/api/router/rules")
async def save_router_rules(body: dict, admin: dict = Depends(admin_only)):
async def save_router_rules(body: dict, _licensed: dict = Depends(require_feature("model_router")), admin: dict = Depends(admin_only)):
routes = body.get("routes") or []
db = get_db()
db.execute("DELETE FROM router_rules")
@@ -5364,7 +5531,7 @@ async def save_router_rules(body: dict, admin: dict = Depends(admin_only)):
return {"ok": True, "routes": len(routes)}
@app.put("/api/router/fallback")
async def save_router_fallback(body: dict, admin: dict = Depends(admin_only)):
async def save_router_fallback(body: dict, _licensed: dict = Depends(require_feature("model_router")), admin: dict = Depends(admin_only)):
db = get_db()
for key, value in body.items():
if key == "cloud_key" and value:
@@ -5449,7 +5616,7 @@ async def rag_quality_warnings(admin: dict = Depends(admin_only)):
return {"warnings": warnings}
@app.post("/api/meeting/analyse")
async def analyse_meeting(body: dict, user: dict = Depends(current_user)):
async def analyse_meeting(body: dict, user: dict = Depends(require_feature("meeting_assistant"))):
transcript = (body.get("transcript") or "").strip()
if not transcript:
raise HTTPException(400, "transcript required")
@@ -5466,7 +5633,7 @@ async def analyse_meeting(body: dict, user: dict = Depends(current_user)):
return result
@app.post("/api/meeting/process")
async def process_meeting(file: UploadFile = File(...), meta: str = Form("{}"), user: dict = Depends(current_user)):
async def process_meeting(file: UploadFile = File(...), meta: str = Form("{}"), user: dict = Depends(require_feature("meeting_assistant"))):
try:
meta_obj = json.loads(meta) if isinstance(meta, str) else {}
except Exception: