Files
Onyscribe-Admin/app/services/contract.py
RubenRWU 7d8c226da2 Onyscribe Admin: Initialer Stand
Admin-Plattform für die Tenants von Onyscribe: Editor mit Live-Vorschau (echter Core in Sandbox), Versionen, KI-Agent (Cortecs),
OIDC-Abgleich über die Login-Developer-API, Deploy-Engine (Mittwald, gesperrt ohne DEPLOY_LIVE, Freigabe pro Tenant),
Core-Vertrag (Schema/Manifest), Core-Releases mit Kompatibilitätsprüfung, Canary-Rollout und Rollback, Zustandsabgleich.
Runbook: docs/RUNBOOK_PARALLELTEST.md

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 15:30:26 +02:00

126 lines
4.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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