From d5f8647ce5948942087236737053a881a5daef5a Mon Sep 17 00:00:00 2001 From: Google AI Agent Date: Tue, 31 Mar 2026 08:29:58 -0400 Subject: [PATCH] =?UTF-8?q?[sovereignty]=20Cut=20the=20Cloud=20Umbilical?= =?UTF-8?q?=20=E2=80=94=20Close=20#94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit THE BUG ======= Issue #94 flagged: the active config's fallback_model pointed to Google Gemini cloud. The enabled Health Monitor cron job had model=null, provider=null — so it inherited whatever the config defaulted to. If the default was ever accidentally changed back to cloud, every 5-minute cron tick would phone home. THE FIX ======= config.yaml: - fallback_model → local Ollama (hermes3:latest on localhost:11434) - Google Gemini custom_provider → renamed '(emergency only)' - tts.openai.model → disabled (use edge TTS locally) cron/jobs.json: - Health Monitor → explicit model/provider/base_url fields - No enabled job can ever inherit cloud defaults again tests/test_sovereignty_enforcement.py (NEW — 13 tests): - Default model is localhost - Fallback model is localhost (the #94 fix) - No enabled cron has null model/provider - No enabled cron uses cloud URLs - First custom_provider is local - TTS and STT default to local tests/test_local_runtime_defaults.py (UPDATED): - Now asserts fallback is Ollama, not Gemini WHAT STILL WORKS ================ Google Gemini is still available for explicit override: hermes --model gemini-2.5-pro It's just not automatic anymore. You have to ask for it. FULL SUITE ========== 36/36 pass. Zero regressions. Closes #94 Signed-off-by: gemini --- config.yaml | 14 +- cron/jobs.json | 3 + tests/test_local_runtime_defaults.py | 5 +- tests/test_sovereignty_enforcement.py | 202 ++++++++++++++++++++++++++ 4 files changed, 216 insertions(+), 8 deletions(-) create mode 100644 tests/test_sovereignty_enforcement.py diff --git a/config.yaml b/config.yaml index 1ab24d56..553014c3 100644 --- a/config.yaml +++ b/config.yaml @@ -114,7 +114,7 @@ tts: voice_id: pNInz6obpgDQGcFmaJgB model_id: eleven_multilingual_v2 openai: - model: gpt-4o-mini-tts + model: '' # disabled — use edge TTS locally voice: alloy neutts: ref_audio: '' @@ -189,7 +189,9 @@ custom_providers: base_url: http://localhost:8081/v1 api_key: none model: hermes4:14b -- name: Google Gemini +# ── Emergency cloud provider — not used by default or any cron job. +# Available for explicit override only: hermes --model gemini-2.5-pro +- name: Google Gemini (emergency only) base_url: https://generativelanguage.googleapis.com/v1beta/openai api_key_env: GEMINI_API_KEY model: gemini-2.5-pro @@ -213,7 +215,7 @@ mcp_servers: env: {} timeout: 30 fallback_model: - provider: custom - model: gemini-2.5-pro - base_url: https://generativelanguage.googleapis.com/v1beta/openai - api_key_env: GEMINI_API_KEY + provider: ollama + model: hermes3:latest + base_url: http://localhost:11434/v1 + api_key: '' diff --git a/cron/jobs.json b/cron/jobs.json index 3c5417db..9dc15ea9 100644 --- a/cron/jobs.json +++ b/cron/jobs.json @@ -60,6 +60,9 @@ "id": "a77a87392582", "name": "Health Monitor", "prompt": "Check Ollama is responding, disk space, memory, GPU utilization, process count", + "model": "hermes3:latest", + "provider": "ollama", + "base_url": "http://localhost:11434/v1", "schedule": { "kind": "interval", "minutes": 5, diff --git a/tests/test_local_runtime_defaults.py b/tests/test_local_runtime_defaults.py index 4ebf50e3..e06e291e 100644 --- a/tests/test_local_runtime_defaults.py +++ b/tests/test_local_runtime_defaults.py @@ -17,5 +17,6 @@ def test_config_defaults_to_local_llama_cpp_runtime() -> None: ) assert local_provider["model"] == "hermes4:14b" - assert config["fallback_model"]["provider"] == "custom" - assert config["fallback_model"]["model"] == "gemini-2.5-pro" + assert config["fallback_model"]["provider"] == "ollama" + assert config["fallback_model"]["model"] == "hermes3:latest" + assert "localhost" in config["fallback_model"]["base_url"] diff --git a/tests/test_sovereignty_enforcement.py b/tests/test_sovereignty_enforcement.py new file mode 100644 index 00000000..ee0ad9ca --- /dev/null +++ b/tests/test_sovereignty_enforcement.py @@ -0,0 +1,202 @@ +"""Sovereignty enforcement tests. + +These tests implement the acceptance criteria from issue #94: + [p0] Cut cloud inheritance from active harness config and cron + +Every test in this file catches a specific way that cloud +dependency can creep back into the active config. If any test +fails, Timmy is phoning home. + +These tests are designed to be run in CI and to BLOCK any commit +that reintroduces cloud defaults. +""" + +from __future__ import annotations + +import json +from pathlib import Path + +import yaml +import pytest + +REPO_ROOT = Path(__file__).parent.parent +CONFIG_PATH = REPO_ROOT / "config.yaml" +CRON_PATH = REPO_ROOT / "cron" / "jobs.json" + +# Cloud URLs that should never appear in default/fallback paths +CLOUD_URLS = [ + "generativelanguage.googleapis.com", + "api.openai.com", + "chatgpt.com", + "api.anthropic.com", + "openrouter.ai", +] + +CLOUD_MODELS = [ + "gpt-4", + "gpt-5", + "gpt-4o", + "claude", + "gemini", +] + + +@pytest.fixture +def config(): + return yaml.safe_load(CONFIG_PATH.read_text()) + + +@pytest.fixture +def cron_jobs(): + data = json.loads(CRON_PATH.read_text()) + return data.get("jobs", data) if isinstance(data, dict) else data + + +# ── Config defaults ────────────────────────────────────────────────── + +class TestDefaultModelIsLocal: + """The default model must point to localhost.""" + + def test_default_model_is_not_cloud(self, config): + """model.default should be a local model identifier.""" + model = config["model"]["default"] + for cloud in CLOUD_MODELS: + assert cloud not in model.lower(), \ + f"Default model '{model}' looks like a cloud model" + + def test_default_base_url_is_localhost(self, config): + """model.base_url should point to localhost.""" + base_url = config["model"]["base_url"] + assert "localhost" in base_url or "127.0.0.1" in base_url, \ + f"Default base_url '{base_url}' is not local" + + def test_default_provider_is_local(self, config): + """model.provider should be 'custom' or 'ollama'.""" + provider = config["model"]["provider"] + assert provider in ("custom", "ollama", "local"), \ + f"Default provider '{provider}' may route to cloud" + + +class TestFallbackIsLocal: + """The fallback model must also be local — this is the #94 fix.""" + + def test_fallback_base_url_is_localhost(self, config): + """fallback_model.base_url must point to localhost.""" + fb = config.get("fallback_model", {}) + base_url = fb.get("base_url", "") + if base_url: + assert "localhost" in base_url or "127.0.0.1" in base_url, \ + f"Fallback base_url '{base_url}' is not local — cloud leak!" + + def test_fallback_has_no_cloud_url(self, config): + """fallback_model must not contain any cloud API URLs.""" + fb = config.get("fallback_model", {}) + base_url = fb.get("base_url", "") + for cloud_url in CLOUD_URLS: + assert cloud_url not in base_url, \ + f"Fallback model routes to cloud: {cloud_url}" + + def test_fallback_model_name_is_local(self, config): + """fallback_model.model should not be a cloud model name.""" + fb = config.get("fallback_model", {}) + model = fb.get("model", "") + for cloud in CLOUD_MODELS: + assert cloud not in model.lower(), \ + f"Fallback model name '{model}' looks like cloud" + + +# ── Cron jobs ──────────────────────────────────────────────────────── + +class TestCronSovereignty: + """Enabled cron jobs must never inherit cloud defaults.""" + + def test_enabled_crons_have_explicit_model(self, cron_jobs): + """Every enabled cron job must have a non-null model field. + + When model is null, the job inherits from config.yaml's default. + Even if the default is local today, a future edit could change it. + Explicit is always safer than implicit. + """ + for job in cron_jobs: + if not isinstance(job, dict): + continue + if not job.get("enabled", False): + continue + + model = job.get("model") + name = job.get("name", job.get("id", "?")) + assert model is not None and model != "", \ + f"Enabled cron job '{name}' has null model — will inherit default" + + def test_enabled_crons_have_explicit_provider(self, cron_jobs): + """Every enabled cron job must have a non-null provider field.""" + for job in cron_jobs: + if not isinstance(job, dict): + continue + if not job.get("enabled", False): + continue + + provider = job.get("provider") + name = job.get("name", job.get("id", "?")) + assert provider is not None and provider != "", \ + f"Enabled cron job '{name}' has null provider — will inherit default" + + def test_no_enabled_cron_uses_cloud_url(self, cron_jobs): + """No enabled cron job should have a cloud base_url.""" + for job in cron_jobs: + if not isinstance(job, dict): + continue + if not job.get("enabled", False): + continue + + base_url = job.get("base_url", "") + name = job.get("name", job.get("id", "?")) + for cloud_url in CLOUD_URLS: + assert cloud_url not in (base_url or ""), \ + f"Cron '{name}' routes to cloud: {cloud_url}" + + +# ── Custom providers ───────────────────────────────────────────────── + +class TestCustomProviders: + """Cloud providers can exist but must not be the default path.""" + + def test_local_provider_exists(self, config): + """At least one custom provider must be local.""" + providers = config.get("custom_providers", []) + has_local = any( + "localhost" in p.get("base_url", "") or "127.0.0.1" in p.get("base_url", "") + for p in providers + ) + assert has_local, "No local custom provider defined" + + def test_first_provider_is_local(self, config): + """The first custom_provider should be the local one. + + Hermes resolves 'custom' provider by scanning the list in order. + If a cloud provider is listed first, it becomes the implicit default. + """ + providers = config.get("custom_providers", []) + if providers: + first = providers[0] + base_url = first.get("base_url", "") + assert "localhost" in base_url or "127.0.0.1" in base_url, \ + f"First custom_provider '{first.get('name')}' is not local" + + +# ── TTS/STT ────────────────────────────────────────────────────────── + +class TestVoiceSovereignty: + """Voice services should prefer local providers.""" + + def test_tts_default_is_local(self, config): + """TTS provider should be local (edge or neutts).""" + tts_provider = config.get("tts", {}).get("provider", "") + assert tts_provider in ("edge", "neutts", "local"), \ + f"TTS provider '{tts_provider}' may use cloud" + + def test_stt_default_is_local(self, config): + """STT provider should be local.""" + stt_provider = config.get("stt", {}).get("provider", "") + assert stt_provider in ("local", "whisper", ""), \ + f"STT provider '{stt_provider}' may use cloud"