diff --git a/scripts/lib/semantic-retrieval.py b/scripts/lib/semantic-retrieval.py index 5bd30e0..e45bfac 100644 --- a/scripts/lib/semantic-retrieval.py +++ b/scripts/lib/semantic-retrieval.py @@ -3,6 +3,7 @@ import hashlib import json import os from pathlib import Path +import re import shutil import subprocess import sys @@ -89,10 +90,15 @@ def check_runtime(target, state): path = target / name if path.is_symlink() or not path.is_file() or digest(path) != expected: raise ValueError(f'Owned semantic retrieval file changed: {name}') - allowed = set(state['files']) | {'.venv', 'model-cache'} + allowed = set(state['files']) | {'.venv', 'model-cache', '__pycache__'} for path in target.iterdir(): if path.is_symlink() or path.name not in allowed: raise ValueError(f'Foreign or linked semantic retrieval path: {path.name}') + if path.name == '__pycache__' and (not path.is_dir() or any( + child.is_symlink() or not child.is_file() or not re.fullmatch( + r'(?:server|benchmark)\.[A-Za-z0-9_-]+(?:\.opt-\d+)?\.pyc', child.name) + for child in path.iterdir())): + raise ValueError('Foreign or linked semantic retrieval path: __pycache__') def sync(root, home, clients, enabled, dry_run, update, summary=False): diff --git a/scripts/tests/test-semantic-retrieval.py b/scripts/tests/test-semantic-retrieval.py index cd3313c..6a7e77e 100644 --- a/scripts/tests/test-semantic-retrieval.py +++ b/scripts/tests/test-semantic-retrieval.py @@ -4,6 +4,7 @@ import io import json from contextlib import redirect_stdout from pathlib import Path +import py_compile import tempfile import unittest from unittest.mock import patch @@ -85,6 +86,25 @@ class SemanticRetrievalTests(unittest.TestCase): self.assertFalse(target.exists()) self.assertFalse(state_path.exists()) + def test_update_accepts_python_cache_but_rejects_foreign_cache_content(self): + with tempfile.TemporaryDirectory() as directory: + home = Path(directory) / 'home' + target = home / '.my-ai-configuration/semantic-retrieval' + target.mkdir(parents=True) + files = {} + for name in MODULE.FILES: + content = (ROOT / 'shared/retrieval' / name).read_bytes() + (target / name).write_bytes(content) + files[name] = hashlib.sha256(content).hexdigest() + MODULE.save_state(target.parent / 'semantic-retrieval.json', {'version': 1, 'clients': ['codex'], 'files': files}, False) + py_compile.compile(str(target / 'server.py'), doraise=True) + cache = target / '__pycache__' + with patch.object(MODULE, 'ensure_runtime', return_value=Path('python')), patch.object(MODULE, 'ensure_models'): + MODULE.sync(ROOT, home, ['codex'], True, False, True) + (cache / 'foreign.txt').write_text('keep') + with self.assertRaisesRegex(ValueError, '__pycache__'): + MODULE.sync(ROOT, home, ['codex'], True, False, True) + def test_negative_benchmark_keeps_active_runtime(self): with tempfile.TemporaryDirectory() as directory: home = Path(directory) / 'home'