"""Vertrag mit dem Core (siehe transkript-platform/docs/CONTRACT.md): Schema, Manifest, Build-Info, Env-Spezifikation. Gelesen wird aus dem Core, auf dem die Admin-Plattform läuft (PLATFORM_DIR: im Container das Core-Image, lokal der vendored Stand). `core/contract.py` ist import-sicher (nur Standardbibliothek) und wird per Dateipfad geladen – der Core-Code selbst wird NIE in diesem Prozess importiert (er hält Prozess-Singletons); gerendert wird in der Sandbox. Ältere Cores ohne Vertrag („legacy“) funktionieren weiter: Schema fehlt → feste Admin-Formulare und feste Env-Liste. """ from __future__ import annotations import importlib.util import json from functools import lru_cache from pathlib import Path from app.settings import PLATFORM_DIR SUPPORTED_CONTRACT = {1} # Rückfall für Cores ohne Schema (entspricht dem Stand vor dem Vertrag) LEGACY_SHARED_ENV = { "OIDC_ISSUER": "oidc_issuer", "AUTH_ENABLED": "auth_enabled", "APP_COOKIE_SECURE": "app_cookie_secure", "MAX_FILE_SIZE_MB": "max_file_size_mb", "LLM_MODEL": "llm_model", "LLM_MAX_TOKENS": "llm_max_tokens", "LLM_TEMPERATURE": "llm_temperature", "CORTECS_BASE_URL": "cortecs_base_url", "SMTP_HOST": "smtp_host", "SMTP_PORT": "smtp_port", "SMTP_USER": "smtp_user", "SMTP_FROM": "smtp_from", "MISTRAL_TRANSCRIBE_MODEL": "mistral_transcribe_model", "MISTRAL_DIARIZE": "mistral_diarize", "MISTRAL_TIMEOUT": "mistral_timeout", "COST_API_BASE": "cost_api_base", "COST_TRACKING_ENABLED": "cost_tracking_enabled", } LEGACY_REQUIRED_SECRETS = ["OIDC_CLIENT_ID", "OIDC_CLIENT_SECRET", "SESSION_SECRET"] @lru_cache(maxsize=1) def _module(): path = Path(PLATFORM_DIR) / "core" / "contract.py" if not path.exists(): return None spec = importlib.util.spec_from_file_location("onyscribe_core_contract", path) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) return mod def reload() -> None: _module.cache_clear() schema.cache_clear() def available() -> bool: return _module() is not None @lru_cache(maxsize=1) def schema() -> dict | None: m = _module() return m.load_schema() if m else None def build_info() -> dict: m = _module() if m: return m.build_info() f = Path(PLATFORM_DIR) / "CORE_VERSION" return {"sha": f.read_text().split()[0] if f.exists() else "unbekannt", "built_at": None, "source": "legacy"} def manifest() -> dict: m = _module() if m: return m.build_manifest() return {"contract": 0, "core": build_info(), "legacy": True} def contract_version() -> int: return int(manifest().get("contract", 0)) def supported() -> bool: return (not available()) or contract_version() in SUPPORTED_CONTRACT def sha() -> str: return build_info()["sha"] def same_sha(a: str, b: str) -> bool: a, b = (a or "").lower(), (b or "").lower() return bool(a and b) and (a.startswith(b) or b.startswith(a)) and min(len(a), len(b)) >= 7 def check_tenant_json(raw: dict) -> list[str]: m = _module() return m.check_tenant_json(raw, schema()) if m else [] def field_paths() -> set[str]: s = schema() return {f["path"] for f in s["fields"]} if s else set() def env_spec() -> list[dict]: s = schema() if s: return s["env"] return [{"name": n, "kind": "setting", "scope": "shared", "setting": k, "default": None, "label": n, "group": "Allgemein"} for n, k in LEGACY_SHARED_ENV.items()] def required_secrets(tenant_cfg: dict | None = None) -> list[str]: """Pflicht-Secrets laut Schema (required, required_if wie „stt.provider!=cortecs“ gegen die Tenant-Config ausgewertet).""" s = schema() if not s: return list(LEGACY_REQUIRED_SECRETS) out = [] for e in s["env"]: if e["kind"] != "secret": continue if e.get("required"): out.append(e["name"]) elif e.get("required_if"): path, _, rhs = e["required_if"].partition("!=") cur = tenant_cfg or {} for k in path.split("."): cur = cur.get(k, {}) if isinstance(cur, dict) else {} val = cur if isinstance(cur, str) else "" if (val or "mistral") != rhs: # Provider-Standard ist mistral out.append(e["name"]) return out