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>
This commit is contained in:
RubenRWU
2026-10-15 15:30:26 +02:00
commit 7d8c226da2
89 changed files with 13975 additions and 0 deletions

125
app/services/contract.py Normal file
View File

@@ -0,0 +1,125 @@
"""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