Files
Onyscribe-Admin/vendor/platform/core/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

112 lines
4.6 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 zwischen Core und Admin-Plattform (siehe docs/CONTRACT.md).
Der Core veröffentlicht bei jedem Release: Image, Manifest, Schema (`core/schema/tenant_schema.json`), Bundle-Format und
`/health`. Die Admin-Plattform liest das – sie hält keine eigene Kopie der Tenant-Optionen.
python -m core.contract # Manifest als JSON auf stdout
python -m core.contract --check # Schema gegen den Code prüfen (Exit-Code ≠ 0 bei Abweichung)
"""
from __future__ import annotations
import hashlib
import json
import os
import subprocess
import sys
from pathlib import Path
CONTRACT_VERSION = 1 # Version dieses Vertrags (Schema-Format, Bundle-Format, Health-Felder)
HOOK_API = 1 # Version der Hook-Schnittstelle (core/hooks.py); overrides.py kann `HOOK_API = 1` deklarieren
BUNDLE_FORMAT = 1 # Format des Tenant-Bundles (tar.gz mit tenant.json + Dateien + MANIFEST.json)
CORE_DIR = Path(__file__).resolve().parent
ROOT = CORE_DIR.parent
SCHEMA_PATH = CORE_DIR / "schema" / "tenant_schema.json"
BUILD_INFO_PATH = CORE_DIR / "BUILD_INFO.json" # schreibt das Dockerfile; lokal fehlt die Datei
# Env-Variablen, die der Code liest, die aber bewusst nicht im Schema stehen (interne/Legacy/Plattformvariablen)
INTERNAL_ENV = {"TENANTS_DIR", "TEMP_DIR", "LOCALAPPDATA", "APP_SESSION_SECRET", "CORE_SHA", "CORE_BUILT_AT", "PYTHONPATH", "PATH", "HOME"}
def build_info() -> dict:
"""{sha, built_at, source}: BUILD_INFO.json (Image) > Env CORE_SHA > git > 'dev'."""
try:
data = json.loads(BUILD_INFO_PATH.read_text(encoding="utf-8"))
if data.get("sha"):
return {"sha": data["sha"], "built_at": data.get("built_at"), "source": "image"}
except (OSError, ValueError):
pass
if os.getenv("CORE_SHA"):
return {"sha": os.environ["CORE_SHA"], "built_at": os.getenv("CORE_BUILT_AT"), "source": "env"}
try:
sha = subprocess.run(["git", "-C", str(ROOT), "rev-parse", "HEAD"], capture_output=True, text=True, timeout=5).stdout.strip()
if sha:
return {"sha": sha, "built_at": None, "source": "git"}
except Exception: # noqa: BLE001
pass
return {"sha": "dev", "built_at": None, "source": "dev"}
def load_schema() -> dict:
return json.loads(SCHEMA_PATH.read_text(encoding="utf-8"))
def _sha256(path: Path) -> str | None:
try:
return hashlib.sha256(path.read_bytes()).hexdigest()
except OSError:
return None
def build_manifest() -> dict:
schema = load_schema()
return {
"contract": CONTRACT_VERSION, "hook_api": HOOK_API, "bundle_format": BUNDLE_FORMAT,
"core": build_info(),
"requirements_sha256": _sha256(ROOT / "requirements.txt"),
"schema_sha256": _sha256(SCHEMA_PATH),
"fields": len(schema["fields"]), "env": len(schema["env"]),
}
def schema_paths(schema: dict | None = None) -> set[str]:
return {f["path"] for f in (schema or load_schema())["fields"]}
def check_tenant_json(raw: dict, schema: dict | None = None) -> list[str]:
"""Meldet unbekannte Schlüssel und falsche Typen in einer tenant.json (Tippfehler fallen sonst still durch)."""
schema = schema or load_schema()
fields = {f["path"]: f for f in schema["fields"]}
ignored = set(schema.get("ignored_keys", []))
problems: list[str] = []
def walk(prefix: str, obj: dict) -> None:
for k, v in obj.items():
path = f"{prefix}{k}"
if not prefix and k in ignored or (not prefix and k == "id"):
continue
if path in fields:
t = fields[path]["type"]
ok = {"bool": isinstance(v, bool), "number": isinstance(v, (int, float)) and not isinstance(v, bool),
"enum": v in (fields[path].get("options") or []) or v in (None, ""),
"color": isinstance(v, str) and len(v) in (4, 7) and v.startswith("#")}.get(t, isinstance(v, str) or v is None)
if not ok:
problems.append(f"{path}: ungültiger Wert {v!r} (erwartet {t})")
elif isinstance(v, dict) and any(p.startswith(path + ".") for p in fields):
walk(path + ".", v)
else:
problems.append(f"{path}: unbekannte Option (nicht im Schema)")
walk("", raw)
return problems
if __name__ == "__main__":
if "--check" in sys.argv:
sys.path.insert(0, str(ROOT))
from tests.test_contract import drift_report # noqa: E402
rep = drift_report()
print("\n".join(rep) or "Schema und Code stimmen überein.")
sys.exit(1 if rep else 0)
print(json.dumps(build_manifest(), indent=2, ensure_ascii=False))