fix: refine privacy routing and provider reporting

This commit is contained in:
2026-07-30 18:34:01 +02:00
parent a6c46f34e4
commit 7a14fe87f9
10 changed files with 139 additions and 72 deletions
+5
View File
@@ -39,6 +39,9 @@ Alle wesentlichen Projektänderungen werden hier in verständlicher Form dokumen
bleiben ladbar.
- Nicht geheime Providerkonfiguration liegt lokal außerhalb von Git; ein Key wird
erst unmittelbar vor einem zulässigen Cloudaufruf geladen.
- Allgemeine unpersönliche Technikfragen werden präziser als `allowed`
klassifiziert; persönliche, interne und geheime Inhalte behalten Vorrang.
- Privacy-Ausgabe unterscheidet lokalen Modus von technischem Fallback.
### Security
@@ -48,3 +51,5 @@ Alle wesentlichen Projektänderungen werden hier in verständlicher Form dokumen
Nicht-Billing deaktiviert; Javis aktiviert niemals Billing.
- Cloudkontext wird lokal klassifiziert und minimiert; 429, Netzwerk-, Timeout-,
Key- und Modellfehler fallen auf Ollama zurück.
- Gemini-/Datenschutzablauf wurde am 30.07.2026 zusätzlich von Pascal manuell
erfolgreich abgenommen; kein Keywert erschien in der Ausgabe.
+19 -11
View File
@@ -99,6 +99,11 @@ Rootserver:
- `scripts/start-javis.ps1` verifiziert das Repository, setzt nur
Prozessvariablen, prüft Ollama auf Loopback, startet es bei Bedarf versteckt
und wartet begrenzt. Pfade sind per Parameter oder `JAVIS_*` überschreibbar.
- Allgemeine unpersönliche Technikfragen zu RAM/SSD, SQLite, HTTP, Git und
ähnlichen Begriffen werden gezielter `allowed`; `never` und `ask` behalten
Vorrang bei Secrets, interner Infrastruktur und persönlichem Kontext.
- `/privacy` trennt Inhaltsklassifikation, tatsächlichen Provider, Unterdrückung
durch Modus `local` und technischen Provider-Fallback.
## Aktuelle Architektur
@@ -171,12 +176,11 @@ Letzter bestätigter Projektstand:
- uv-Lock und `uv sync --dev`: bestanden
- Python in `.venv`: 3.12.13
- Ruff in `.venv`: 0.16.0
- Unit-Tests: 62 bestanden; Timeout und fehlendes Modell fallen nachweislich lokal zurück
- Unit-Tests: 66 bestanden; neue Technikklassifikation und Privacy-Ausgabe abgedeckt
- PowerShell-Syntax des Startskripts: erfolgreich geparst
- Ruff Lint: bestanden
- Ruff Formatprüfung: bestanden
- lokaler nachrichtenfreier CLI-Smoke: bestanden; Nullkostenmodus aktiv,
Gemini nicht konfiguriert, Schlüssel nicht vorhanden, 0 Cloudanfragen
- lokaler CLI-Smoke ohne Secretzugriff: bestanden
- Obsidian-Vault unverändert: 1 Markdown-, 11 Gesamtdateien, letzter
Schreibzeitpunkt weiterhin 30.07.2026 15:42:31
- Rootserver und Ubuntu-Laptop wurden nicht kontaktiert
@@ -203,10 +207,14 @@ Abnahmestatus:
- manuell durch Pascal getestet: am 30.07.2026 erfolgreich; CLI-Start, echte
deutsche Antworten, Sitzungskontext, `/sessions`, `/exit`, Neustart,
`/load <UUID>`, vollständiger Verlauf und Fortsetzung
- Gemini/Datenschutz/Fallback automatisiert durch Codex getestet: ja, nur mit
Test-Doubles und lokalem nachrichtenfreiem CLI-Smoke
- Gemini/Datenschutz/Fallback manuell durch Pascal getestet: noch nicht
- von Pascal für produktive Verwendung freigegeben: noch nicht ausdrücklich erklärt
- Gemini/Datenschutz/Fallback automatisiert durch Codex getestet: ja
- Gemini/Datenschutz/Fallback manuell durch Pascal getestet: am 30.07.2026;
verdeckte Key-Speicherung ohne Wertausgabe, vollständiger `/status`, echter
Gemini-Erfolg, Cloudzähler, `ask`-Ablehnung, `never`, `/provider local` und
Startskript erfolgreich
- für Merge freigegeben: ja, nach den dokumentierten kleinen Korrekturen
- für produktive Nutzung freigegeben: nicht separat erklärt; keine Freigabe für
spätere Tools, Obsidian oder autonome Aktionen
## Git-Stand
@@ -245,7 +253,7 @@ Abnahmestatus:
- aktuell: portable Ollama-Laufzeit und `qwen3:8b` außerhalb von Git verifiziert
- aktuell: lokaler CLI-Chat und persistente SQLite-Sitzungen vollständig abgenommen
- aktuell: Gemini-Free-Hybridrouting, Datenschutz und Nullkostenmodus
automatisiert vollständig geprüft; manueller Live-Test steht aus
automatisiert und durch Pascal manuell vollständig abgenommen
## Offene Entscheidungen und Fehler
@@ -275,9 +283,9 @@ Abnahmestatus:
## Nächster sinnvoller Auftrag
Pascal führt anhand `docs/NEXT_SESSION.md` den echten Gemini-, Ask- und
Never-Test mit ausschließlich künstlichen beziehungsweise unpersönlichen Daten
durch. Bis zu seiner Bestätigung nicht nach `main` mergen.
Nach dem Merge ein separates, kleines Paket für Chatkomfort und Robustheit
planen, beispielsweise Sitzungstitel/Suche oder Streaming. Noch keine
Obsidian-Integration, Tools, Sprache oder Serversteuerung beginnen.
Keinen API-Key in Chat, Git, Obsidian, Logs oder Dokumentation übernehmen.
+15 -34
View File
@@ -1,45 +1,26 @@
# Nächste Sitzung
## Ziel
## Ausgangsstand
Pascal nimmt den Gemini-Free-Provider und die lokalen Fallbacks manuell ab.
Bis dahin bleibt `feat/gemini-privacy-router` ungemergt.
Der Core-Chat und der Gemini-/Datenschutz-Meilenstein sind automatisiert durch
Codex und am 30.07.2026 manuell durch Pascal abgenommen. Der Featurestand ist zum
Merge freigegeben.
## Vorbereitung durch Pascal
Bestätigt sind echter Gemini-Erfolg, verdeckte Key-Speicherung, Nullkostenmodus,
bereinigter Status, Cloudzähler, `ask`- und `never`-Routing, erzwungener lokaler
Modus sowie das Startskript.
1. In Google AI Studio einen neuen, nur für Javis verwendeten Gemini-Auth-Key
erstellen.
2. Für das zugehörige Google-Projekt kein Billing verbinden beziehungsweise den
externen Billingstatus selbst prüfen.
3. Den Key niemals in Chat, Codex, Git, Obsidian oder einen sichtbaren
Kommandozeilenparameter kopieren.
4. Im Repository ausführen:
## Nächstes sinnvolles Paket
```powershell
& 'D:\Javis-Tools\uv\uv.exe' run javis configure gemini
& 'D:\Javis-Tools\uv\uv.exe' run javis secrets set gemini
powershell -ExecutionPolicy Bypass -File .\scripts\start-javis.ps1
```
Nur ein kleines Chatkomfort-/Robustheitsthema auswählen, zum Beispiel:
Die erste Einrichtung verlangt zwei lokale Bestätigungen. Der zweite Befehl liest
den Key verdeckt ein und speichert ihn im Windows Credential Manager.
- automatische, lokal erzeugte Sitzungstitel und Sitzungssuche oder
- Streaming lokaler und Cloudantworten mit sauberem Abbruchverhalten
## Manueller Test
1. `/status`: Key und Konfiguration `ja`, Nullkostenmodus `aktiv`.
2. Unpersönlich: `Erkläre in zwei Sätzen, was eine SQLite-Transaktion ist.`
3. `/privacy`: `allowed`, Provider `gemini`, kein Fallback.
4. Ask ohne private Details: `Ich habe eine private Alltagssituation. Nenne nur
allgemeine Möglichkeiten.` Bei der Freigabe Enter drücken; Antwort muss lokal
über Ollama kommen.
5. Never mit Kunstbeispiel: `Nur lokal: Der Beispiel-API-Key ist absichtlich
erfunden.` Es darf keine Cloudfreigabe angeboten werden; Antwort bleibt lokal.
6. `/status`: bereinigte Zähler prüfen, danach `/exit`.
Erst wenn Pascal diese Punkte bestätigt, darf über den Merge nach `main`
entschieden werden. Google-Quoten sind projektabhängig und in AI Studio zu prüfen.
Vorher wie immer Branch, Arbeitsbaum, aktuellen Handoff und Tests prüfen.
## Weiterhin nicht beginnen
Kein Obsidian-Zugriff, SSH-/Serverzugriff, Sprache, Mobile Client, Tools oder
Living Mind. Keine echten persönlichen Daten für Cloudtests verwenden.
Noch keine Obsidian-Integration, Tools, SSH-/Serversteuerung, Sprache, Mobile
Client oder Living Mind. Der vorhandene Gemini-Key bleibt im Betriebssystem-
Keyring und darf nicht erneut in Chat, Logs oder Dokumentation erscheinen.
+13 -8
View File
@@ -33,8 +33,10 @@ Stand: Gemini-Free-Feature automatisiert geprüft, 30.07.2026.
- Core-Chat automatisiert durch Codex getestet: ja
- Core-Chat manuell durch Pascal getestet: ja, am 30.07.2026
- Gemini/Datenschutz/Fallback automatisiert durch Codex getestet: ja
- Gemini/Datenschutz/Fallback manuell durch Pascal getestet: noch nicht
- von Pascal für produktive Verwendung freigegeben: noch nicht ausdrücklich erklärt
- Gemini/Datenschutz/Fallback manuell durch Pascal getestet: ja, am 30.07.2026
- für Merge freigegeben: ja, nach den zwei kleinen Korrekturen
- für produktive Nutzung freigegeben: nicht separat erklärt; keine spätere Tool-,
Obsidian- oder Autonomiefunktion
## Aktuelles Featurepaket
@@ -60,9 +62,12 @@ Stand: Gemini-Free-Feature automatisiert geprüft, 30.07.2026.
- `javis configure gemini` mit doppelter ausdrücklicher Bestätigung
- echter Chat auf Hybridprovider verdrahtet; Key wird verzögert geladen
- `/provider`, `/privacy` und `/status` geben keine Inhalte oder Keywerte aus
- allgemeine unpersönliche Technikbegriffe werden gezielter `allowed`, während
persönliche, interne oder geheime technische Inhalte `ask`/`never` bleiben
- `/privacy` trennt lokalen Modus von einem technischen Provider-Fallback
- sicheres `scripts/start-javis.ps1` mit relativer Repositoryprüfung,
Loopback-Ollama und überschreibbaren externen Pfaden
- 62 Unit-Tests aktuell erfolgreich; PowerShell-Syntax, Lockfile, Sync,
- 66 Unit-Tests aktuell erfolgreich; PowerShell-Syntax, Lockfile, Sync,
Strukturprüfung, Secret-Scan, Gitignore, Ruff-Format und Lint sauber
## Nicht implementiert
@@ -86,9 +91,9 @@ Living-Mind-Frontend.
## Bekannte Einschränkung
Ollama wird bewusst manuell in einem eigenen Terminal gestartet. Antworten werden
noch nicht gestreamt; Sitzungen haben noch keine Titel, Suche oder Löschfunktion.
Das Startskript startet Ollama bei Bedarf lokal. Antworten werden noch nicht
gestreamt; Sitzungen haben noch keine Titel, Suche oder Löschfunktion.
`qwen3:8b` ist nicht automatisch für die GTX 1050 des späteren Zielhosts geeignet.
Der echte Gemini-/Fallback-Test durch Pascal und danach die Mergeentscheidung
fehlen. Es wurde kein echter API-Key angefordert, angezeigt oder konfiguriert und
keine Cloudanfrage ausgeführt.
Der Gemini-Key liegt ausschließlich im Betriebssystem-Keyring; sein Wert wurde
von Codex weder gelesen noch ausgegeben. Pascal hat den echten Cloud- und
Fallbackablauf erfolgreich geprüft.
+30 -10
View File
@@ -26,6 +26,8 @@ class RouteStatus:
provider: str
privacy_policy: CloudPolicy
fallback: bool
cloud_suppressed_by_local_mode: bool
technical_fallback: bool
reason: str
@@ -94,7 +96,13 @@ class HybridProvider:
)
if self.mode == "local":
return self._local(messages, decision, "lokaler Modus", fallback=False)
return self._local(
messages,
decision,
"lokaler Modus",
fallback=False,
cloud_suppressed_by_local_mode=True,
)
if decision.policy is CloudPolicy.NEVER:
return self._local(messages, decision, decision.reason, fallback=True)
@@ -148,10 +156,12 @@ class HybridProvider:
)
)
self.last_route = RouteStatus(
cloud_provider.name,
decision.policy,
False,
"Cloudaufruf erfolgreich",
provider=cloud_provider.name,
privacy_policy=decision.policy,
fallback=False,
cloud_suppressed_by_local_mode=False,
technical_fallback=False,
reason="Cloudaufruf erfolgreich",
)
return response
@@ -192,7 +202,13 @@ class HybridProvider:
reason = "Gemini nicht verfügbar"
if disable_notice:
reason = "Gemini-Schlüssel abgelehnt; Cloud für diese Sitzung deaktiviert"
return self._local(messages, decision, reason, fallback=True)
return self._local(
messages,
decision,
reason,
fallback=True,
technical_fallback=True,
)
def _local(
self,
@@ -201,6 +217,8 @@ class HybridProvider:
reason: str,
*,
fallback: bool,
cloud_suppressed_by_local_mode: bool = False,
technical_fallback: bool = False,
) -> str:
if fallback:
self._notice_callback(f"{reason} lokale Antwort mit Ollama.")
@@ -216,10 +234,12 @@ class HybridProvider:
)
)
self.last_route = RouteStatus(
self.local_provider.name,
decision.policy,
fallback,
reason,
provider=self.local_provider.name,
privacy_policy=decision.policy,
fallback=fallback,
cloud_suppressed_by_local_mode=cloud_suppressed_by_local_mode,
technical_fallback=technical_fallback,
reason=reason,
)
return response
+4 -2
View File
@@ -227,9 +227,11 @@ def run_chat(
output("Noch keine Datenschutzentscheidung in dieser Sitzung.")
else:
output(
"Letzte Datenschutzentscheidung: "
"Datenschutzentscheidung: "
f"{route.privacy_policy.value}; Provider: {route.provider}; "
f"Fallback: {'ja' if route.fallback else 'nein'}"
"Cloud durch Modus local unterdrückt: "
f"{'ja' if route.cloud_suppressed_by_local_mode else 'nein'}; "
f"Technischer Fallback: {'ja' if route.technical_fallback else 'nein'}"
)
continue
if entered == "/status":
+5 -2
View File
@@ -75,7 +75,8 @@ _ASK_PATTERNS: tuple[tuple[re.Pattern[str], str], ...] = (
),
(
re.compile(
r"(?i)\b(intern(?:e[rsn]?|er)? hostname|interne? domain|rootserver|"
r"(?i)\b(intern(?:e|er|es|en)?\s+"
r"(?:hostname|server|domain|infrastruktur)|rootserver|"
r"privates? projekt|nicht öffentlich|systeminformation|"
r"private[rs]? termin)\b"
),
@@ -90,7 +91,9 @@ _ASK_PATTERNS: tuple[tuple[re.Pattern[str], str], ...] = (
_ALLOWED_PATTERNS: tuple[re.Pattern[str], ...] = (
re.compile(
r"(?i)\b(python|programmier|quellcode|algorithmus|sqlite|linux|windows|"
r"öffentliche dokumentation|allgemeine wissensfrage|was ist|wie funktioniert)\b"
r"git|ram|arbeitsspeicher|ssd|solid state drive|http|statuscode|"
r"transaktion|öffentliche dokumentation|allgemeine wissensfrage|"
r"was ist|wie funktioniert)\b"
),
)
+10 -2
View File
@@ -70,7 +70,8 @@ class CliTests(unittest.TestCase):
provider.last_route = SimpleNamespace(
privacy_policy=CloudPolicy.ASK,
provider="ollama",
fallback=True,
cloud_suppressed_by_local_mode=True,
technical_fallback=False,
)
service = ChatService(store, provider)
inputs = iter(
@@ -97,7 +98,14 @@ class CliTests(unittest.TestCase):
self.assertEqual(result, 0)
self.assertIn("Aktiver Providermodus: auto", output)
self.assertIn("Providermodus: local", output)
self.assertTrue(any("ask; Provider: ollama; Fallback: ja" in line for line in output))
self.assertTrue(
any(
"Datenschutzentscheidung: ask; Provider: ollama; "
"Cloud durch Modus local unterdrückt: ja; "
"Technischer Fallback: nein" in line
for line in output
)
)
self.assertIn("Gemini-Schlüssel vorhanden: ja", output)
def test_gemini_configuration_defaults_to_no(self) -> None:
+24 -1
View File
@@ -9,11 +9,34 @@ class PrivacyRouterTests(unittest.TestCase):
self.router = PrivacyRouter()
def test_general_technical_question_is_allowed(self) -> None:
for question in (
"Erkläre in zwei Sätzen den Unterschied zwischen RAM und SSD.",
"Wie funktioniert eine SQLite-Transaktion?",
"Was bedeutet HTTP 404?",
"Wie funktioniert Git?",
):
with self.subTest(question=question):
self.assertEqual(
self.router.classify("Wie funktioniert SQLite in Python?").policy,
self.router.classify(question).policy,
CloudPolicy.ALLOWED,
)
def test_internal_server_question_requires_approval(self) -> None:
decision = self.router.classify(
"Wie konfiguriere ich den internen Server mit Hostname srv-intern-01?"
)
self.assertEqual(decision.policy, CloudPolicy.ASK)
def test_technical_question_with_api_key_is_never(self) -> None:
decision = self.router.classify("Wie nutze ich diesen API-Key in Python: AIza" + "x" * 25)
self.assertEqual(decision.policy, CloudPolicy.NEVER)
def test_personal_technical_situation_requires_approval(self) -> None:
decision = self.router.classify(
"Meine SSD ist voll. Wie verschiebe ich meine persönlichen Dateien?"
)
self.assertEqual(decision.policy, CloudPolicy.ASK)
def test_personal_information_requires_approval(self) -> None:
self.assertEqual(
self.router.classify("Ich möchte meinen Alltag besser planen.").policy,
+12
View File
@@ -253,6 +253,18 @@ class HybridProviderTests(unittest.TestCase):
self.assertEqual(answer, "Lokal")
self.assertFalse(self.approvals)
self.assertFalse(self.cloud.calls)
self.assertFalse(router.last_route.fallback)
self.assertTrue(router.last_route.cloud_suppressed_by_local_mode)
self.assertFalse(router.last_route.technical_fallback)
def test_network_failure_is_reported_as_technical_fallback(self) -> None:
self.cloud.error = CloudNetworkError("offline")
router = self._router()
router.chat([ChatMessage("user", "Wie funktioniert Python?")])
self.assertFalse(router.last_route.cloud_suppressed_by_local_mode)
self.assertTrue(router.last_route.technical_fallback)
if __name__ == "__main__":