diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e2465c..50c757a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/docs/CHATGPT_HANDOFF.md b/docs/CHATGPT_HANDOFF.md index fb79bc2..1822f67 100644 --- a/docs/CHATGPT_HANDOFF.md +++ b/docs/CHATGPT_HANDOFF.md @@ -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 `, 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. diff --git a/docs/NEXT_SESSION.md b/docs/NEXT_SESSION.md index 9335a55..9f913f1 100644 --- a/docs/NEXT_SESSION.md +++ b/docs/NEXT_SESSION.md @@ -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. diff --git a/docs/PROJECT_STATUS.md b/docs/PROJECT_STATUS.md index 446abe5..632f254 100644 --- a/docs/PROJECT_STATUS.md +++ b/docs/PROJECT_STATUS.md @@ -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. diff --git a/src/javis/core/provider_router.py b/src/javis/core/provider_router.py index b4ad77e..482e4a3 100644 --- a/src/javis/core/provider_router.py +++ b/src/javis/core/provider_router.py @@ -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 diff --git a/src/javis/interface/cli.py b/src/javis/interface/cli.py index 4165620..8c618a4 100644 --- a/src/javis/interface/cli.py +++ b/src/javis/interface/cli.py @@ -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": diff --git a/src/javis/security/privacy.py b/src/javis/security/privacy.py index a81ed0f..d44c627 100644 --- a/src/javis/security/privacy.py +++ b/src/javis/security/privacy.py @@ -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" ), ) diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py index 62a4fe4..a8e394c 100644 --- a/tests/unit/test_cli.py +++ b/tests/unit/test_cli.py @@ -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: diff --git a/tests/unit/test_privacy.py b/tests/unit/test_privacy.py index 7e9e483..bf9ebb5 100644 --- a/tests/unit/test_privacy.py +++ b/tests/unit/test_privacy.py @@ -9,10 +9,33 @@ class PrivacyRouterTests(unittest.TestCase): self.router = PrivacyRouter() def test_general_technical_question_is_allowed(self) -> None: - self.assertEqual( - self.router.classify("Wie funktioniert SQLite in Python?").policy, - CloudPolicy.ALLOWED, + 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(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( diff --git a/tests/unit/test_provider_router.py b/tests/unit/test_provider_router.py index 2fa8a93..6378c16 100644 --- a/tests/unit/test_provider_router.py +++ b/tests/unit/test_provider_router.py @@ -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__":