From af6fe55577be4b7bc6e89fd5bb664382ce6a155a Mon Sep 17 00:00:00 2001 From: Lars Date: Tue, 8 Sep 2026 10:02:28 +0200 Subject: [PATCH] Allow remote detect in production until local Ollama is connected. Keep KANSHO_ENV=production and require an explicit operator flag instead of treating Prod as Development. Promote to Prod only via merge commit on main. Co-authored-by: Cursor --- .env.example | 3 + .gitea/workflows/test.yml | 3 +- backend/.env.example | 7 +- backend/entity_detect.py | 2 +- backend/env_loader.py | 23 +++++- backend/model_catalog.py | 34 +++++++- backend/provider_settings.py | 14 +++- backend/routers/admin.py | 13 ++++ backend/tests/harness.py | 1 + backend/tests/test_frame.py | 2 + backend/tests/test_model_catalog.py | 30 +++++++ backend/tests/test_privacy_detect.py | 7 ++ docker-compose.dev-env.yml | 1 + docker-compose.yml | 1 + docs/DEPLOYMENT.md | 4 +- docs/architecture/functional/guardrails.md | 14 ++++ .../functional/mvp_stand_und_abgleich.md | 4 +- .../technical/admin_diagnostics.md | 2 + .../technical/mvp_implementation.md | 6 +- .../architecture/technical/privacy_gateway.md | 10 +++ .../technical/runtime_and_deploy.md | 2 +- docs/work_orders/home_environment_setup.md | 2 +- frontend/src/pages/AdminProvidersPage.jsx | 78 ++++++++++++++++--- 23 files changed, 236 insertions(+), 27 deletions(-) diff --git a/.env.example b/.env.example index 7b21422..0aea4bc 100644 --- a/.env.example +++ b/.env.example @@ -9,6 +9,7 @@ # APP_URL=https://dev.kansho.jinkendo.de # ALLOWED_ORIGINS=https://dev.kansho.jinkendo.de,http://192.168.2.49:3096 # KANSHO_ENV=development +# KANSHO_ALLOW_REMOTE_DETECT= # KANSHO_FRONTEND_PORT=3096 # KANSHO_BACKEND_PORT=8096 # KANSHO_PROVIDER_KEY= @@ -21,6 +22,8 @@ DB_PASSWORD=CHANGE_ME_SECURE_PASSWORD APP_URL=https://kansho.jinkendo.de ALLOWED_ORIGINS=https://kansho.jinkendo.de KANSHO_ENV=production +# Transitional until local detect (Ollama). Empty = blocked in production. +KANSHO_ALLOW_REMOTE_DETECT= KANSHO_FRONTEND_PORT=3006 KANSHO_BACKEND_PORT=8005 KANSHO_PROVIDER_KEY= diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index 6b9d431..6046419 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -51,11 +51,12 @@ jobs: -e DB_NAME=kansho_test \ -e KANSHO_PROVIDER_KEY= \ -e KANSHO_DETECT_PROVIDER_KEY= \ + -e KANSHO_ALLOW_REMOTE_DETECT= \ -e PYTHONUTF8=1 \ backend sh -lc ' set -e unset KANSHO_DB_PATH KANSHO_FAKE_PROVIDER KANSHO_FAKE_DETECT - unset KANSHO_PROVIDER_KEY KANSHO_DETECT_PROVIDER_KEY + unset KANSHO_PROVIDER_KEY KANSHO_DETECT_PROVIDER_KEY KANSHO_ALLOW_REMOTE_DETECT export KANSHO_PROVIDER_KEY= export KANSHO_DETECT_PROVIDER_KEY= pip install -q -r requirements-dev.txt diff --git a/backend/.env.example b/backend/.env.example index 622812c..1d5114b 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -2,8 +2,10 @@ # Env überschreibt die Admin-Werte, wenn gesetzt. # development | test | production -# production: kein externes Klartext-Detect (Muster oder lokale Detect-URL). +# production: externes Klartext-Detect default-off. Ziel bleibt lokales Detect. +# Übergang bis Ollama: KANSHO_ALLOW_REMOTE_DETECT=1 (Operator, kein Admin-Flag). # KANSHO_ENV=development +# KANSHO_ALLOW_REMOTE_DETECT= # Generate: nur maskierter Kontext. Extern: Key Pflicht. KANSHO_PROVIDER_KEY= @@ -12,7 +14,8 @@ KANSHO_PROVIDER_KEY= # KANSHO_PROVIDER_ZDR=1 # KANSHO_PROVIDER_NO_TRAIN=1 -# Detect: Klartext nur lokal oder in Development/Test. Leer = Generate-Key mitnutzen (nur Dev/Test). +# Detect: Klartext lokal, in Development/Test, oder in Production mit KANSHO_ALLOW_REMOTE_DETECT. +# Leer = Generate-Key mitnutzen (nur wenn remote Detect erlaubt ist). KANSHO_DETECT_PROVIDER_KEY= # KANSHO_DETECT_PROVIDER_URL=https://openrouter.ai/api/v1/chat/completions # KANSHO_DETECT_PROVIDER_MODEL=openai/gpt-4.1-nano diff --git a/backend/entity_detect.py b/backend/entity_detect.py index 65b419e..2b8d925 100644 --- a/backend/entity_detect.py +++ b/backend/entity_detect.py @@ -251,7 +251,7 @@ def reset_detect_test_hooks() -> None: def uses_llm_detect(config) -> bool: - """Local detect stays a provider role. Remote plaintext detect is development/test only.""" + """Local detect stays a provider role. Remote plaintext detect needs an explicit allow.""" if not config or config.mode != "http": return False if config.local: diff --git a/backend/env_loader.py b/backend/env_loader.py index b279d89..05390ea 100644 --- a/backend/env_loader.py +++ b/backend/env_loader.py @@ -26,7 +26,7 @@ def load_env_file(path: Path | None = None) -> None: def runtime_env() -> str: - """development/test may use remote plaintext detect. production must not.""" + """development/test may use remote plaintext detect. production must not, unless overridden.""" raw = (os.environ.get("KANSHO_ENV") or "").strip().lower() if raw in {"production", "prod"}: return "production" @@ -37,8 +37,27 @@ def runtime_env() -> str: return "development" +def env_flag(name: str) -> bool: + raw = (os.environ.get(name) or "").strip().lower() + return raw in {"1", "true", "yes", "on"} + + +def remote_plaintext_detect_reason() -> str: + """Why remote detect is allowed or blocked. Never logs secrets.""" + if runtime_env() != "production": + return "non_production" + if env_flag("KANSHO_ALLOW_REMOTE_DETECT"): + return "operator_override" + return "blocked" + + def allows_remote_plaintext_detect() -> bool: - return runtime_env() != "production" + """Remote plaintext detect is default-off in production. + + Endbetrieb remains local detect. Until Ollama is connected, an operator + may set KANSHO_ALLOW_REMOTE_DETECT. That does not disable the gateway. + """ + return remote_plaintext_detect_reason() != "blocked" def upsert_env_value(name: str, value: str, path: Path | None = None) -> None: diff --git a/backend/model_catalog.py b/backend/model_catalog.py index 420aa63..9b6fb25 100644 --- a/backend/model_catalog.py +++ b/backend/model_catalog.py @@ -110,7 +110,7 @@ def _parse_model_row(row: dict) -> ModelWindow | None: ) -def _fetch_catalog(url: str, key: str) -> dict[str, dict]: +def _request_models_payload(url: str, key: str) -> list: headers = {"Content-Type": "application/json"} if key: headers["Authorization"] = f"Bearer {key}" @@ -143,6 +143,11 @@ def _fetch_catalog(url: str, key: str) -> dict[str, dict]: status_code=503, diagnostics={"models_url": url, "reason": "unexpected_shape"}, ) + return rows + + +def _fetch_catalog(url: str, key: str) -> dict[str, dict]: + rows = _request_models_payload(url, key) parsed: dict[str, dict] = {} for row in rows: if not isinstance(row, dict): @@ -249,3 +254,30 @@ def resolve_generate_metadata(config, *, now: float | None = None) -> ModelWindo diagnostics={"model": model, "reason": "model_not_in_catalog"}, ) return _window_from_cache(fetched[model], model=model) + + +def list_provider_models(config) -> list[dict]: + """Model ids for the admin picker. Soft-fail; free-text remains valid.""" + if not config or getattr(config, "mode", None) == "fake": + return [] + url = getattr(config, "url", "") or "" + if not url: + return [] + catalog_key = models_url(url) + try: + rows = _request_models_payload(catalog_key, getattr(config, "key", "") or "") + except JournalBudgetError: + return [] + seen: set[str] = set() + items: list[dict] = [] + for row in rows: + if not isinstance(row, dict): + continue + model_id = str(row.get("id") or "").strip() + if not model_id or model_id in seen: + continue + seen.add(model_id) + name = str(row.get("name") or model_id).strip() or model_id + items.append({"id": model_id, "name": name}) + items.sort(key=lambda item: item["id"]) + return items diff --git a/backend/provider_settings.py b/backend/provider_settings.py index 382fec9..8418a2c 100644 --- a/backend/provider_settings.py +++ b/backend/provider_settings.py @@ -7,7 +7,12 @@ from pathlib import Path from urllib.parse import urlparse from db import get_db -from env_loader import allows_remote_plaintext_detect, runtime_env, upsert_env_value +from env_loader import ( + allows_remote_plaintext_detect, + remote_plaintext_detect_reason, + runtime_env, + upsert_env_value, +) SEED_PATH = Path(__file__).resolve().parent / "config" / "providers.seed.json" ROLES = ("generate", "detect") @@ -23,7 +28,7 @@ ROLE_META = { }, "detect": { "title": "Maskierung", - "task": "Eigener Detect-Endpunkt. Vollständige semantische Detection des persönlichen Egress. Externes Klartext-Detect nur Development/Test. Produktiv: lokales Detect-Modell. Kein Pattern-Fallback.", + "task": "Eigener Detect-Endpunkt und eigenes Modell. Vollständige semantische Detection des persönlichen Egress. Ziel: lokales Detect-Modell. Übergang: externes Klartext-Detect nur mit Operator-Freigabe. Kein Pattern-Fallback.", }, } @@ -141,6 +146,7 @@ def public_role_status(role: str) -> dict: "mode": config.mode if config else "unconfigured", "remote_plaintext_detect": bool(config and not config.local and config.mode == "http" and role == "detect"), "remote_plaintext_allowed": allows_remote_plaintext_detect() if role == "detect" else None, + "remote_plaintext_reason": remote_plaintext_detect_reason() if role == "detect" else None, "updated": row.get("updated") or "", } @@ -149,5 +155,7 @@ def public_status() -> dict: return { "roles": [public_role_status(role) for role in ROLES], "runtime_env": runtime_env(), - "note": "Zwei Einträge: Maskierung (Detect) und Sprachmodell. Semantische Detection ist Pflicht vor Generate. Externes Klartext-Detect nur Development/Test. Produktiv lokal. Keys nur in backend/.env.", + "note": "Zwei Stufen, zwei Modelle: Maskierung und Sprachmodell. Semantische Detection ist Pflicht vor Generate. Ziel bleibt lokales Detect. Externes Klartext-Detect in Production nur mit KANSHO_ALLOW_REMOTE_DETECT. Keys nur in .env.", + "remote_plaintext_reason": remote_plaintext_detect_reason(), + "remote_plaintext_allowed": allows_remote_plaintext_detect(), } diff --git a/backend/routers/admin.py b/backend/routers/admin.py index 7119318..767d917 100644 --- a/backend/routers/admin.py +++ b/backend/routers/admin.py @@ -204,6 +204,19 @@ def admin_providers(session: dict = Depends(require_admin_dep)): return public_status() +@router.get("/providers/models") +def admin_provider_models(session: dict = Depends(require_admin_dep)): + from model_catalog import list_provider_models + from providers import detect_provider, generate_provider + + return { + "roles": { + "generate": list_provider_models(generate_provider()), + "detect": list_provider_models(detect_provider()), + } + } + + @router.put("/providers/{role}") def admin_update_provider(role: str, body: ProviderUpdate, session: dict = Depends(require_admin_dep)): if role not in ROLES: diff --git a/backend/tests/harness.py b/backend/tests/harness.py index 9de832a..b82beda 100644 --- a/backend/tests/harness.py +++ b/backend/tests/harness.py @@ -45,6 +45,7 @@ def prepare_test_env() -> None: os.environ.pop("KANSHO_DB_PATH", None) os.environ.pop("KANSHO_FAKE_PROVIDER", None) os.environ.pop("KANSHO_FAKE_DETECT", None) + os.environ.pop("KANSHO_ALLOW_REMOTE_DETECT", None) def apply_module_env(source) -> None: diff --git a/backend/tests/test_frame.py b/backend/tests/test_frame.py index 758654c..5620227 100644 --- a/backend/tests/test_frame.py +++ b/backend/tests/test_frame.py @@ -87,6 +87,8 @@ def main() -> None: expect(providers.status_code == 200, "admin providers") roles = {item["role"]: item for item in providers.json()["roles"]} expect(set(roles) == {"generate", "detect"}, "two provider roles") + expect(providers.json()["remote_plaintext_allowed"] is True, "test runtime allows remote detect") + expect(providers.json()["remote_plaintext_reason"] == "non_production", "tests are not production") expect(roles["generate"]["ready"] is False, "generate fail-closed without key") expect("sk-" not in providers.text, "provider status has no secret") expect("KANSHO_PROVIDER_KEY=" not in providers.text, "env assignment not leaked") diff --git a/backend/tests/test_model_catalog.py b/backend/tests/test_model_catalog.py index 319a28f..9a7fc28 100644 --- a/backend/tests/test_model_catalog.py +++ b/backend/tests/test_model_catalog.py @@ -17,6 +17,7 @@ Path(tempfile.gettempdir()).mkdir(parents=True, exist_ok=True) from model_catalog import ( ModelWindow, catalog_ttl, + list_provider_models, reset_catalog, resolve_generate_metadata, ) @@ -181,12 +182,41 @@ def test_env_fallback_and_fail_closed() -> None: _restore_env(previous) +def test_list_provider_models() -> None: + reset_catalog() + payload = { + "data": [ + {"id": "openai/gpt-4.1-nano", "name": "GPT 4.1 Nano"}, + {"id": "openai/gpt-5.4", "context_length": 128000}, + {"id": "openai/gpt-4.1-nano"}, + ] + } + with patch("model_catalog.httpx.get", lambda *args, **kwargs: FakeResponse(payload)): + items = list_provider_models(_openrouter_config()) + expect([item["id"] for item in items] == ["openai/gpt-4.1-nano", "openai/gpt-5.4"], "picker lists unique ids") + with patch("model_catalog.httpx.get", lambda *args, **kwargs: FakeResponse({"nope": True})): + expect(list_provider_models(_openrouter_config()) == [], "picker soft-fails on bad catalog") + fake = ProviderConfig( + role="generate", + name="fake", + mode="fake", + url="", + model="fake", + key="", + local=True, + zdr=True, + no_train=True, + ) + expect(list_provider_models(fake) == [], "fake provider has no catalog") + + def main() -> None: test_reads_context_and_completion() test_model_not_in_catalog() test_incomplete_metadata() test_cache_hit_and_ttl_expiry() test_env_fallback_and_fail_closed() + test_list_provider_models() print("model catalog tests passed.") diff --git a/backend/tests/test_privacy_detect.py b/backend/tests/test_privacy_detect.py index fe72639..c344d19 100644 --- a/backend/tests/test_privacy_detect.py +++ b/backend/tests/test_privacy_detect.py @@ -570,7 +570,13 @@ def main() -> None: ) expect(uses_llm_detect(remote), "development allows remote detect") os.environ["KANSHO_ENV"] = "production" + os.environ.pop("KANSHO_ALLOW_REMOTE_DETECT", None) expect(not uses_llm_detect(remote), "production blocks remote detect") + os.environ["KANSHO_ALLOW_REMOTE_DETECT"] = "1" + expect(uses_llm_detect(remote), "operator override allows remote detect in production") + os.environ["KANSHO_ALLOW_REMOTE_DETECT"] = "0" + expect(not uses_llm_detect(remote), "false override does not allow remote detect") + os.environ.pop("KANSHO_ALLOW_REMOTE_DETECT", None) local = ProviderConfig( role="detect", name="ollama", @@ -584,6 +590,7 @@ def main() -> None: ) expect(uses_llm_detect(local), "production still allows local detect") os.environ.pop("KANSHO_ENV", None) + os.environ.pop("KANSHO_ALLOW_REMOTE_DETECT", None) mapped = [{"local_label": "Anna", "token": "PERSON:01"}] anna_manifest = mask_prompt("Anna war am Markt.", mapped, "journal_generate") diff --git a/docker-compose.dev-env.yml b/docker-compose.dev-env.yml index d68ecd9..447e050 100644 --- a/docker-compose.dev-env.yml +++ b/docker-compose.dev-env.yml @@ -37,6 +37,7 @@ services: ALLOWED_ORIGINS: "${ALLOWED_ORIGINS:-https://dev.kansho.jinkendo.de,http://192.168.2.49:3096,http://localhost:3096}" KANSHO_PROVIDER_KEY: "${KANSHO_PROVIDER_KEY:-}" KANSHO_DETECT_PROVIDER_KEY: "${KANSHO_DETECT_PROVIDER_KEY:-}" + KANSHO_ALLOW_REMOTE_DETECT: "${KANSHO_ALLOW_REMOTE_DETECT:-}" volumes: - dev-kansho-media:/app/data/media ports: diff --git a/docker-compose.yml b/docker-compose.yml index ef24387..72f5f11 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -39,6 +39,7 @@ services: ALLOWED_ORIGINS: "${ALLOWED_ORIGINS:-https://kansho.jinkendo.de}" KANSHO_PROVIDER_KEY: "${KANSHO_PROVIDER_KEY:-}" KANSHO_DETECT_PROVIDER_KEY: "${KANSHO_DETECT_PROVIDER_KEY:-}" + KANSHO_ALLOW_REMOTE_DETECT: "${KANSHO_ALLOW_REMOTE_DETECT:-}" volumes: - kansho-media:/app/data/media ports: diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index f9b1a83..e3c8fab 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -63,7 +63,7 @@ Watchtower bleibt aus. | `deploy-prod.yml` | Push `main` | Deploy nach `/home/lars/docker/kansho`, Health `localhost:8005` | | `test.yml` | nach erfolgreichem `Deploy Development` | pytest auf `kansho_test`, nicht-schreibender Smoke gegen die laufende Dev-API, Frontend-Build. Keine Live-Provider-Keys. | -Prod nur über Merge `develop` → `main`, kein direkter Prod-Schreibzugriff. +Prod nur über Merge-Commit `develop` → `main` (`git merge --no-ff develop` auf `main`), danach Push von `main`. Kein Fast-Forward, kein direkter Commit auf `main`, kein manuelles `git pull` auf dem Prod-Checkout. Gitea (`deploy-prod.yml`) ist der einzige Prod-Schreibpfad für Code. --- @@ -89,7 +89,7 @@ Windows-Vorbereitung (Kopie, nicht das einzige Backup): # Archiv prüfen, dann SQLite+Medien auf den Pi kopieren — nicht nach Gitea. ``` -4. Abnahme Dev: Profilanzahl, Journal-Days/Entries, Messages, ein Medien-GET, Gateway fail-closed, `KANSHO_ENV=production` ohne Klartext-Detect. +4. Abnahme Dev: Profilanzahl, Journal-Days/Entries, Messages, ein Medien-GET, Gateway fail-closed. Production ohne `KANSHO_ALLOW_REMOTE_DETECT` blockiert Klartext-Detect; der Operator-Übergang bis Ollama ist explizit und dokumentiert (`guardrails.md` §22.3). 5. Erst dann derselbe Import nach Prod (`--confirm`, leeres Volume). `--replace` nur auf einer bewussten Kopie. `debug.persist_traces` nach dem Import prüfen. diff --git a/docs/architecture/functional/guardrails.md b/docs/architecture/functional/guardrails.md index 478c7c6..c1a517c 100644 --- a/docs/architecture/functional/guardrails.md +++ b/docs/architecture/functional/guardrails.md @@ -693,6 +693,20 @@ Additiv. Keine Wortlisten und keine textbezogenen Ausnahmen. **Additiv 2026-08-28 (Prüffassung vs. Ausgabe):** Die interne Klartext-zu-Token-Normalisierung der Modellantwort bleibt die Prüffassung. Die sichtbare und gespeicherte Fassung rehydriert nur tatsächlich ausgegebene Platzhalter; modellseitiger Klartext behält seine Oberflächenform. +## 22.3 Übergang: externes Klartext-Detect in Production (2026-09-08) + +Additiv. Ändert nicht das Ziel. Technische Abbildung: `../technical/privacy_gateway.md` §9.6. + +**Entschieden (Endbetrieb):** Semantische Detection vor jedem persönlichen Generate bleibt Pflicht. Produktivziel ist ein lokales Detect-Modell. Das Privacy Gateway bleibt nicht abschaltbar. Generate sieht nur maskierten Kontext. Keys bleiben in der Umgebung, nicht in der Datenbank. + +**Entschieden (Übergang, explizite Operatoranforderung):** Bis ein lokales Detect (Ollama) angebunden ist, darf Production externes Klartext-Detect nutzen, wenn der Operator `KANSHO_ALLOW_REMOTE_DETECT` setzt. Development/Test erlauben remote Detect weiter ohne dieses Flag. Ohne Flag bleibt Production fail-closed, sobald Detect nicht lokal ist. + +Das ist kein Admin-Feature-Flag und kein Umdeuten von `KANSHO_ENV=production` nach Development. Default bleibt blockiert. Die Detect-Stufe sendet in diesem Übergang Klartext an den konfigurierten Detect-Endpunkt; das ist das bekannte Restrisiko der Testphase, nicht der Zielpfad. + +**Entschieden (Modellwahl):** Maskierung und Sprachmodell bleiben zwei getrennte Stufen. URL und Modell sind je Stufe in Admin → Schnittstellen wählbar. Das größere Modell gehört an Generate, nicht zwingend an Detect. + +**Später prüfen:** Flag entfernen, sobald lokales Detect auf Prod läuft. Dann wieder Production ohne Klartext-Detect. + --- # 23. Externe Referenzquellen diff --git a/docs/architecture/functional/mvp_stand_und_abgleich.md b/docs/architecture/functional/mvp_stand_und_abgleich.md index 583b4aa..2339600 100644 --- a/docs/architecture/functional/mvp_stand_und_abgleich.md +++ b/docs/architecture/functional/mvp_stand_und_abgleich.md @@ -137,6 +137,8 @@ Additiv zu Abschnitt 2. Kein horizontaler Slice, kein Target-Model-Vorbau. Korri Tests: `backend/tests/test_architecture_correction.py` plus Anpassungen in `test_mvp_journal.py` und `test_privacy_detect.py`. +**Additiv 2026-09-08:** Die Detect-Zeile in der Tabelle oben ist der Ist-Stand von 2026-08-25. Pattern-Fallback ist überholt. Production bleibt ohne lokales Detect default fail-closed; der Operator-Übergang `KANSHO_ALLOW_REMOTE_DETECT` ist in `guardrails.md` §22.3 festgehalten. + --- # 2.2 Profile Governance (2026-08-25) @@ -389,7 +391,7 @@ Er ist **noch nicht Kanshō im Sinne der Vision**. Die gefährlichen Drifts sind | Automatische Space-Erkennung | nein | **Bewusst offen** | | Semantisches Retrieval | nein | **Bewusst offen** | | Session Summary | nein | **Bewusst offen** | -| Zusätzliche Enrichment-Calls | Detect in Development/Test optional remote | **Abweichung** gegenüber Produktivziel „Detect lokal“; Produktivmodus lässt externes Klartext-Detect nicht als normalen Pfad zu | +| Zusätzliche Enrichment-Calls | Detect in Development/Test optional remote | **Abweichung** gegenüber Produktivziel „Detect lokal“; Produktivmodus lässt externes Klartext-Detect nicht als normalen Pfad zu. **Additiv 2026-09-08:** expliziter Operator-Übergang `KANSHO_ALLOW_REMOTE_DETECT`, Ziel bleibt lokal. | ## 4.4 Foundation Non-Goals diff --git a/docs/architecture/technical/admin_diagnostics.md b/docs/architecture/technical/admin_diagnostics.md index 9f87add..38c3d3c 100644 --- a/docs/architecture/technical/admin_diagnostics.md +++ b/docs/architecture/technical/admin_diagnostics.md @@ -51,6 +51,8 @@ Nicht übernehmen: Mitai-Admin für Körpertarife, Coupons, Training Types als K **Status: Code vorhanden.** Admin-Seite `/admin/providers` und `GET/PUT /api/admin/providers`. Zeigt URL, Modell, ZDR, No-Train, `ready` und `key_present` – niemals den Secret. Key-Schreiben aktualisiert nur `backend/.env`. Generate bleibt fail-closed ohne Key/ZDR/No-Train. Kein Gateway-Bypass. +**Additiv 2026-09-08:** Zwei Stufen mit je eigenem Modell. `GET /api/admin/providers/models` liefert den Katalog zur Auswahl (soft-fail). `remote_plaintext_reason` macht den Production-Übergang `KANSHO_ALLOW_REMOTE_DETECT` sichtbar, setzt ihn aber nicht. + ## 4.2 Implementierungsstand (Dialog-Testspur) **Additiv 2026-08-26:** Lokale Identitätsregistry unter `/admin/identities`. Detect-Vorschläge sind unbestätigt. Compact-Diagnose enthält Detect-Abdeckung, Chunks, Kosten und Laufzeit, aber keine Labels. diff --git a/docs/architecture/technical/mvp_implementation.md b/docs/architecture/technical/mvp_implementation.md index ec44905..a3c8795 100644 --- a/docs/architecture/technical/mvp_implementation.md +++ b/docs/architecture/technical/mvp_implementation.md @@ -179,7 +179,7 @@ Editor speichert Markdown, nicht HTML. Medien-Token bleiben lokal. Dirty: In-App | Schicht | Rolle | Call | |---|---|---| -| Maskierung | `detect` | Vollständige semantische Detection des Generate-Egress. Externes Klartext-Detect nur Development/Test (`KANSHO_ENV`). Produktiv: lokales Detect-Modell. Kein Pattern-Fallback. | +| Maskierung | `detect` | Vollständige semantische Detection des Generate-Egress. Externes Klartext-Detect in Development/Test; in Production default-off, optional Operator-Übergang `KANSHO_ALLOW_REMOTE_DETECT`. Ziel: lokales Detect-Modell. Kein Pattern-Fallback. | | Dialogzug | `generate` | Operation + Impuls | | Journalentwurf | `generate` | Explizit, getrennt | @@ -238,7 +238,7 @@ Nur die **technische** Lage. Bewertung gegen Spec und Vision: Fit-Gap. | Thema | Ist | Zielrahmen / Produktiv | |---|---|---| | DB | SQLite Datei | PostgreSQL | -| Detect | Semantische Request-Detection; Chunking; fail-closed ohne vollständige Abdeckung; Detect-Treffer nicht auto-persistiert. Externes Klartext-Detect nur Development/Test. | lokales Modell (Ollama) als Ziel | +| Detect | Semantische Request-Detection; Chunking; fail-closed ohne vollständige Abdeckung; Detect-Treffer nicht auto-persistiert. Externes Klartext-Detect in Development/Test; Production default-off, optional `KANSHO_ALLOW_REMOTE_DETECT` bis lokales Detect. | lokales Modell (Ollama) als Ziel | | Fehlerkörper | oft Objekt | `{detail: string}` | | `/dialog` | Admin-Harness, aus der Produkt-Nav entfernt | nicht MVP-Screen | | Response Validation | blockiert Klartext-Identität in der Rohantwort vor Demask; keine Quasi-Identifikatoren | `privacy_gateway.md` §9.2 voll | @@ -258,7 +258,7 @@ Additiv zum Slice, 2026-08-25. Kein Target-Model-Vorbau. - Provenance von Draft und Entry-Version ist relational (`journal_*_source_refs`); JSON-ID-Listen werden verlustfrei migriert. Source-Messages bleiben unangetastet. - Context Builder spricht Selection-Specs; Recency bleibt die aktuelle Retrieval-Heuristik, nicht die fachliche API. - Dialogzug kann begrenzte jüngere Original-Conversations desselben Space sehen, ohne denselben Journal Day voll zu laden. -- Externes Klartext-Detect ist kein Produktmodus (`KANSHO_ENV=production` ohne lokales Detect fail-closed). Lokales Detect bleibt austauschbare Rolle. Detect-Ausgabe persistiert keine aktive Identität. +- Externes Klartext-Detect ist kein Produktmodus (`KANSHO_ENV=production` ohne lokales Detect fail-closed). **Additiv 2026-09-08:** Operator darf mit `KANSHO_ALLOW_REMOTE_DETECT` den Übergang bis Ollama explizit öffnen; ohne Flag bleibt der Block. Lokales Detect bleibt austauschbare Rolle. Detect-Ausgabe persistiert keine aktive Identität. - Response Validation blockiert Klartext-Identität vor Demask. Dialog-Egress maskiert den ganzen Prompt inkl. Opening/Space-Kontext; ein Leak bricht den Zug nicht leer ab. Journal-Generate bricht bei einem Leak nach Retry nicht ab: lokales Quellenartefakt bzw. lokaler Entwurf, die leckende Modellantwort wird nicht verwendet. - Konsolidierung nutzt lokale Signale, nicht `count >= 2`. - `/dialog` ist Admin-Harness, nicht Produkt-IA. diff --git a/docs/architecture/technical/privacy_gateway.md b/docs/architecture/technical/privacy_gateway.md index 9a7ee8c..e0c9898 100644 --- a/docs/architecture/technical/privacy_gateway.md +++ b/docs/architecture/technical/privacy_gateway.md @@ -241,6 +241,16 @@ Bestätigte Registry-Zeilen und bestätigte Aliase werden im gesamten gerenderte Contract-Tests: `backend/tests/test_privacy_detect.py`, `backend/tests/test_identity_registry.py`, `backend/tests/test_privacy_response_integrity.py`, `backend/tests/test_detect_contract_retry.py`. Sie beweisen Schema, Fail-closed, span-genaue Maskierung und Datenfluss, nicht semantische Modellleistung. Opt-in: `python entity_detect_eval.py --live` mit synthetischen Sätzen und exakten erwarteten Spans. Ohne diesen Lauf bleibt die Live-Qualität unbestätigt. Das aktuell konfigurierte `openai/gpt-4.1-nano` gilt durch reale False-Positive-Vorschläge qualitativ nicht als zuverlässig bestätigt; das Modell wird deshalb nicht stillschweigend gewechselt. +## 9.6 Übergang: `KANSHO_ALLOW_REMOTE_DETECT` (2026-09-08) + +Additiv. Fachliches Home: `../functional/guardrails.md` §22.3. + +Default in `KANSHO_ENV=production`: externes Klartext-Detect bleibt fail-closed (`remote_detect_blocked_production`). Lokales Detect bleibt erlaubt. Development/Test erlauben remote Detect ohne Extra-Flag. + +Operator-Übergang bis Ollama: `KANSHO_ALLOW_REMOTE_DETECT=1` (Compose `.env`, Container neu anlegen). Das Gateway, die Pflicht-Detection vor Generate, ZDR/No-Train für Generate und die Key-Trennung bleiben. Admin kann das Flag nicht setzen. URL und Modell je Stufe bleiben in `provider_settings`. + +Status in `GET /api/admin/providers`: `remote_plaintext_allowed`, `remote_plaintext_reason` (`non_production` | `operator_override` | `blocked`). Modellauswahl: `GET /api/admin/providers/models` (Katalog, soft-fail). + ## 10. Offene Fragen Übernommen aus `guardrails.md` §21, hier nicht vorentschieden: Entitätstypen, Quasi-Identifikatoren, Pseudonym-Stabilität, Mehrnutzer-Trennung der Mappings, Verschlüsselung der Mapping-Tabelle, rechtliche DSFA bei Mehrbenutzer-Produktbetrieb. diff --git a/docs/architecture/technical/runtime_and_deploy.md b/docs/architecture/technical/runtime_and_deploy.md index 8ddb64b..22063d8 100644 --- a/docs/architecture/technical/runtime_and_deploy.md +++ b/docs/architecture/technical/runtime_and_deploy.md @@ -78,7 +78,7 @@ Mitai-Pipeline als Vorlage: ```text push develop → deploy-dev.yml → compose build/up → Healthcheck successful Dev deploy → test.yml (pytest on kansho_test, Live-Smoke /api/health, Frontend-Build) -merge in main → deploy-prod.yml +merge --no-ff develop in main → push main → deploy-prod.yml ``` **Status: entschieden.** Workflows liegen unter `.gitea/workflows/` (`deploy-dev.yml`, `deploy-prod.yml`, `test.yml`). Muster analog Kairo: `git reset --hard`, `build --no-cache`, Health `GET /api/health`. diff --git a/docs/work_orders/home_environment_setup.md b/docs/work_orders/home_environment_setup.md index 8e8cb71..05a0d7e 100644 --- a/docs/work_orders/home_environment_setup.md +++ b/docs/work_orders/home_environment_setup.md @@ -136,7 +136,7 @@ Nach Phase A zwingend, nach B/C erneut: 2. Einen Journal Day öffnen, kurzen Dialog, Generate nur wenn Keys gesetzt. 3. Opening-Invariante: abgeschlossener Vortag erzeugt keinen falschen Plan-Impuls. 4. Admin → Schnittstellen: Keys aus `.env`, nicht aus der DB. -5. Kein Klartext-Detect in `KANSHO_ENV=production`. +5. Kein Klartext-Detect in `KANSHO_ENV=production` ohne `KANSHO_ALLOW_REMOTE_DETECT`. Der Operator-Übergang bis Ollama ist explizit, kein Umdeuten nach Development. 6. Regression: `.\scripts\test-mvp.ps1` --- diff --git a/frontend/src/pages/AdminProvidersPage.jsx b/frontend/src/pages/AdminProvidersPage.jsx index ba35fe2..800210f 100644 --- a/frontend/src/pages/AdminProvidersPage.jsx +++ b/frontend/src/pages/AdminProvidersPage.jsx @@ -12,12 +12,19 @@ const emptyRole = () => ({ key: '' }) +const FALLBACK_MODELS = { + generate: ['openai/gpt-5.4', 'openai/gpt-4o', 'openai/gpt-4o-mini', 'anthropic/claude-sonnet-4'], + detect: ['openai/gpt-4.1-nano', 'openai/gpt-4o-mini', 'llama3.1'] +} + function statusLabel(role) { if (role.role === 'detect' && !role.url) return 'nicht konfiguriert, fail-closed' if (role.role === 'detect' && role.url && !role.local && role.ready) { - return role.remote_plaintext_allowed === false - ? 'extern konfiguriert, Produktiv blockiert' - : 'bereit (nur Development/Test, extern)' + if (role.remote_plaintext_allowed === false) return 'extern konfiguriert, Produktiv blockiert' + if (role.remote_plaintext_reason === 'operator_override') { + return 'bereit (Übergang: externes Klartext-Detect)' + } + return 'bereit (Development/Test, extern)' } if (role.ready) return 'bereit' if (!role.key_present && !role.local) return 'Key fehlt' @@ -25,10 +32,22 @@ function statusLabel(role) { return 'nicht bereit' } +function detectBanner(data) { + if (!data) return '' + if (data.runtime_env === 'production' && data.remote_plaintext_reason === 'operator_override') { + return 'Production erlaubt derzeit externes Klartext-Detect (KANSHO_ALLOW_REMOTE_DETECT). Das Gateway bleibt aktiv; Generate sieht nur maskierten Kontext. Ziel bleibt ein lokales Detect-Modell.' + } + if (data.runtime_env === 'production' && data.remote_plaintext_allowed === false) { + return 'Production blockiert externes Klartext-Detect. Maskierung braucht eine lokale URL oder die Operator-Freigabe, sonst entsteht kein Impuls.' + } + return '' +} + export default function AdminProvidersPage() { const { session } = useAuth() const [data, setData] = useState(null) const [forms, setForms] = useState({}) + const [catalog, setCatalog] = useState({ generate: [], detect: [] }) const [error, setError] = useState('') const [notice, setNotice] = useState('') const [saving, setSaving] = useState('') @@ -53,7 +72,22 @@ export default function AdminProvidersPage() { }) .catch((e) => setError(e.message)) - useEffect(() => { load() }, [session.token]) + const loadCatalog = () => + api('/api/admin/providers/models', { token: session.token }) + .then((payload) => { + setCatalog({ + generate: payload.roles?.generate || [], + detect: payload.roles?.detect || [] + }) + }) + .catch(() => { + setCatalog({ generate: [], detect: [] }) + }) + + useEffect(() => { + load() + loadCatalog() + }, [session.token]) const update = (role, field, value) => { setForms((prev) => ({ ...prev, [role]: { ...(prev[role] || emptyRole()), [field]: value } })) @@ -82,6 +116,7 @@ export default function AdminProvidersPage() { setData(payload) setForms((prev) => ({ ...prev, [role]: { ...prev[role], key: '' } })) setNotice(form.key.trim() ? `${role}: gespeichert, Key in .env geschrieben.` : `${role}: gespeichert.`) + loadCatalog() } catch (err) { setError(err.message) } finally { @@ -89,20 +124,33 @@ export default function AdminProvidersPage() { } } + const modelOptions = (role) => { + const fromCatalog = catalog[role] || [] + if (fromCatalog.length) return fromCatalog + return (FALLBACK_MODELS[role] || []).map((id) => ({ id, name: id })) + } + + const banner = detectBanner(data) + return (

Schnittstellen

- Zwei eigene Einträge: Maskierung (vollständige semantische Detection) und Sprachmodell (Dialogzug und Journalentwurf). - Externes Klartext-Detect ist nur in Development/Test zulässig. Produktiv ist ein lokales Detect-Modell nötig. Ohne Detection kein Generate. - Der Secret-Key wird nie angezeigt und nie in der Datenbank gespeichert. + Zwei Stufen, zwei eigene Modelle: Maskierung prüft den persönlichen Text, das Sprachmodell schreibt Dialogzug und Journalentwurf. + Pro Stufe URL und Modell getrennt wählen. Ohne Detection kein Generate. Der Secret-Key wird nie angezeigt und nie in der Datenbank gespeichert.

+ {data && ( +

Laufzeit: {data.runtime_env}

+ )} + {banner &&

{banner}

} {error &&

{error}

} {notice &&

{notice}

} {data && ( <> {data.roles.map((role) => { const form = forms[role.role] || emptyRole() + const options = modelOptions(role.role) + const listId = `kansho-models-${role.role}` return (