diff --git a/.gitea/workflows/ci-deploy.yml b/.gitea/workflows/ci-deploy.yml index 11670b4..86c18ac 100644 --- a/.gitea/workflows/ci-deploy.yml +++ b/.gitea/workflows/ci-deploy.yml @@ -37,6 +37,11 @@ jobs: cache: 'npm' cache-dependency-path: job-tracker-ui/package-lock.json + - name: Test repository safety scripts + # Standard-library only and plan-only: this validates the synthetic benchmark harness + # without contacting Ollama, pulling a model, or requiring package installation. + run: python3 scripts/test-ollama-evaluation.py + - name: Build backend run: dotnet build JobTrackerApi/JobTrackerApi.csproj --configuration Release diff --git a/scripts/run-ollama-evaluation.py b/scripts/run-ollama-evaluation.py index 84aeecc..77474c4 100644 --- a/scripts/run-ollama-evaluation.py +++ b/scripts/run-ollama-evaluation.py @@ -40,6 +40,22 @@ TASK_INSTRUCTIONS = { } +class NoRedirectHandler(urllib.request.HTTPRedirectHandler): + """Keep a validated Ollama origin from redirecting a request elsewhere.""" + + def redirect_request(self, req, fp, code, msg, headers, newurl): # noqa: ANN001 + return None + + +def build_http_opener() -> urllib.request.OpenerDirector: + # Do not inherit HTTP(S)_PROXY from the operator shell and never follow redirects. The + # explicitly validated loopback/private origin is the only network boundary this tool may use. + return urllib.request.build_opener(urllib.request.ProxyHandler({}), NoRedirectHandler()) + + +HTTP_OPENER = build_http_opener() + + def sha256_text(value: str) -> str: return hashlib.sha256(value.encode("utf-8")).hexdigest() @@ -115,7 +131,7 @@ def request_json(base_url: str, path: str, payload: dict[str, Any] | None = None headers={"Content-Type": "application/json"}, method="GET" if data is None else "POST", ) - with urllib.request.urlopen(request, timeout=timeout) as response: + with HTTP_OPENER.open(request, timeout=timeout) as response: return json.loads(response.read().decode("utf-8")) @@ -130,7 +146,7 @@ def stream_generate(base_url: str, payload: dict[str, Any], timeout: float) -> t first_token_at: float | None = None parts: list[str] = [] final: dict[str, Any] = {} - with urllib.request.urlopen(request, timeout=timeout) as response: + with HTTP_OPENER.open(request, timeout=timeout) as response: for raw_line in response: if not raw_line.strip(): continue diff --git a/scripts/test-ollama-evaluation.py b/scripts/test-ollama-evaluation.py index f4ec890..396fd08 100644 --- a/scripts/test-ollama-evaluation.py +++ b/scripts/test-ollama-evaluation.py @@ -4,7 +4,9 @@ import importlib.util import json import tempfile import unittest +import urllib.request from pathlib import Path +from unittest import mock SCRIPT = Path(__file__).with_name("run-ollama-evaluation.py") @@ -54,6 +56,13 @@ class OllamaEvaluationTests(unittest.TestCase): MODULE.validate_base_url("http://ollama:11434", True) self.assertEqual("http://192.168.1.20:11434", MODULE.validate_base_url("http://192.168.1.20:11434", True)) + def test_http_opener_disables_proxies_and_redirects(self): + with mock.patch.object(urllib.request, "getproxies", side_effect=AssertionError("proxy discovery must stay disabled")): + opener = MODULE.build_http_opener() + self.assertFalse(any(isinstance(handler, urllib.request.ProxyHandler) for handler in opener.handlers)) + self.assertTrue(any(isinstance(handler, MODULE.NoRedirectHandler) for handler in MODULE.HTTP_OPENER.handlers)) + self.assertIsNone(MODULE.NoRedirectHandler().redirect_request(None, None, 302, "Found", {}, "http://example.invalid")) + if __name__ == "__main__": unittest.main()