Keep downloads bound to their remote representation and publish through atomic destination-local staging. Serialize shared partial ownership. Keep explicit CA trust scoped to provider probes. Preserve checkpoint history and edited files, validate all profile inputs before dependency publication, and separate data removal from installed runtime ownership. Exclude machine-specific PM state from portable transfers. Keep plugin files and nested skill tools intact. Preserve native test isolation. Focused native Windows receipts cover the individual repairs and their integration. This commit does not claim a full-suite or release build.
95 lines
3.7 KiB
Python
95 lines
3.7 KiB
Python
"""Guard Python filesystem calls in tests, not arbitrary native/subprocess I/O."""
|
|
from __future__ import annotations
|
|
|
|
import builtins
|
|
from functools import wraps
|
|
import io
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import sqlite3
|
|
import threading
|
|
|
|
|
|
class HomeIOGuard:
|
|
def __init__(self, roots):
|
|
self.roots = roots
|
|
self.checking = threading.local()
|
|
self.directories: dict[int, Path] = {}
|
|
|
|
def check(self, value, *, dir_fd=None):
|
|
if value is None or isinstance(value, int) or getattr(self.checking, "active", False):
|
|
return
|
|
self.checking.active = True
|
|
try:
|
|
candidate = Path(os.fsdecode(value)).expanduser()
|
|
if dir_fd is not None and not candidate.is_absolute():
|
|
parent = self.directories.get(dir_fd)
|
|
if parent is None:
|
|
raise AssertionError("TEST BUG: untracked dir_fd in guarded filesystem I/O")
|
|
candidate = parent / candidate
|
|
absolute = Path(os.path.abspath(candidate))
|
|
roots = self.roots()
|
|
# Check the lexical path first: resolving must not probe a protected
|
|
# tree merely to decide that the original path was forbidden.
|
|
if any(absolute.is_relative_to(root) for root in roots):
|
|
self.refuse(value)
|
|
resolved = absolute.resolve()
|
|
if any(resolved.is_relative_to(root) for root in roots):
|
|
self.refuse(value)
|
|
finally:
|
|
self.checking.active = False
|
|
|
|
@staticmethod
|
|
def refuse(value):
|
|
raise AssertionError(
|
|
f"TEST BUG: file I/O against the REAL hermes home: {value}\n"
|
|
"Use the isolated HERMES_HOME or a temporary fixture instead."
|
|
)
|
|
|
|
def install(self, monkeypatch):
|
|
def wrap(module, name, parameters):
|
|
original = getattr(module, name)
|
|
|
|
@wraps(original)
|
|
def guarded(*args, **kwargs):
|
|
for index, (parameter, descriptor) in enumerate(parameters):
|
|
value = args[index] if index < len(args) else kwargs.get(parameter)
|
|
self.check(value, dir_fd=kwargs.get(descriptor) if descriptor else None)
|
|
return original(*args, **kwargs)
|
|
|
|
monkeypatch.setattr(module, name, guarded)
|
|
|
|
for module in (builtins, io):
|
|
wrap(module, "open", (("file", None),))
|
|
for name in ("mkdir", "stat", "lstat", "unlink", "remove", "rmdir", "chmod", "utime", "readlink", "access"):
|
|
wrap(os, name, (("path", "dir_fd"),))
|
|
for name in ("makedirs", "listdir", "scandir"):
|
|
wrap(os, name, (("name" if name == "makedirs" else "path", None),))
|
|
for name in ("rename", "replace"):
|
|
wrap(os, name, (("src", "src_dir_fd"), ("dst", "dst_dir_fd")))
|
|
wrap(shutil, "rmtree", (("path", "dir_fd"),))
|
|
wrap(sqlite3, "connect", (("database", None),))
|
|
|
|
original_open, original_close = os.open, os.close
|
|
|
|
@wraps(original_open)
|
|
def guarded_open(path, flags, *args, **kwargs):
|
|
self.check(path, dir_fd=kwargs.get("dir_fd"))
|
|
fd = original_open(path, flags, *args, **kwargs)
|
|
candidate = Path(os.fsdecode(path))
|
|
if kwargs.get("dir_fd") is not None and not candidate.is_absolute():
|
|
candidate = self.directories[kwargs["dir_fd"]] / candidate
|
|
self.directories[fd] = candidate.absolute()
|
|
return fd
|
|
|
|
@wraps(original_close)
|
|
def guarded_close(fd):
|
|
try:
|
|
return original_close(fd)
|
|
finally:
|
|
self.directories.pop(fd, None)
|
|
|
|
monkeypatch.setattr(os, "open", guarded_open)
|
|
monkeypatch.setattr(os, "close", guarded_close)
|