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 <cursoragent@cursor.com>
This commit is contained in:
parent
dd7858347c
commit
af6fe55577
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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.")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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)} />
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user