Merge develop into main for production deploy.
All checks were successful
Deploy Production / deploy (push) Successful in 40s

This commit is contained in:
Lars 2026-09-08 10:07:57 +02:00
commit 032eeb54fb
23 changed files with 236 additions and 27 deletions

View File

@ -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=

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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:

View File

@ -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

View File

@ -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(),
}

View File

@ -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:

View File

@ -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:

View File

@ -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")

View File

@ -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.")

View File

@ -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")

View File

@ -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:

View File

@ -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:

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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`.

View File

@ -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`
---

View File

@ -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 (
<section className="card">
<h1>Schnittstellen</h1>
<p className="muted">
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.
</p>
{data && (
<p className="muted">Laufzeit: {data.runtime_env}</p>
)}
{banner && <p>{banner}</p>}
{error && <p className="error">{error}</p>}
{notice && <p>{notice}</p>}
{data && (
<>
{data.roles.map((role) => {
const form = forms[role.role] || emptyRole()
const options = modelOptions(role.role)
const listId = `kansho-models-${role.role}`
return (
<form
key={role.role}
@ -132,8 +180,20 @@ export default function AdminProvidersPage() {
/>
</label>
<label>
Modell
<input value={form.model} onChange={(e) => update(role.role, 'model', e.target.value)} />
Modell für diese Stufe
<input
list={listId}
value={form.model}
onChange={(e) => update(role.role, 'model', e.target.value)}
placeholder={role.role === 'detect' ? 'openai/gpt-4.1-nano oder lokales Ollama-Modell' : 'openai/gpt-5.4'}
/>
<datalist id={listId}>
{options.map((item) => (
<option key={item.id} value={item.id}>
{item.name && item.name !== item.id ? item.name : item.id}
</option>
))}
</datalist>
</label>
<label className="check">
<input type="checkbox" checked={form.zdr} onChange={(e) => update(role.role, 'zdr', e.target.checked)} />