77 lines
2.6 KiB
Python
77 lines
2.6 KiB
Python
"""Patch target and baseline hash checks for approval-gated adaptation."""
|
|
|
|
import hashlib
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
SCRIPT_INTERFACE = "internal-module"
|
|
SCRIPT_INTERFACE_REASON = "Shared adaptation patch target parsing and target-file baseline verification helpers."
|
|
|
|
BLOCKED_PATH_PARTS = {".git", "__pycache__", ".pytest_cache", "dist"}
|
|
ABSENT_FILE_SHA256 = "__absent__"
|
|
|
|
|
|
def sha256_file(path: Path) -> str:
|
|
digest = hashlib.sha256()
|
|
with path.open("rb") as handle:
|
|
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
|
digest.update(chunk)
|
|
return digest.hexdigest()
|
|
|
|
|
|
def target_file_sha256(skill_dir: Path, target_files: list[str]) -> dict[str, str]:
|
|
observed: dict[str, str] = {}
|
|
for target in target_files:
|
|
path = skill_dir / target
|
|
observed[target] = sha256_file(path) if path.exists() else ABSENT_FILE_SHA256
|
|
return observed
|
|
|
|
|
|
def normalize_patch_path(raw: str) -> str | None:
|
|
token = raw.strip().split("\t", 1)[0].split(" ", 1)[0]
|
|
if token == "/dev/null":
|
|
return None
|
|
if token.startswith("a/") or token.startswith("b/"):
|
|
token = token[2:]
|
|
path = Path(token)
|
|
if path.is_absolute() or ".." in path.parts or any(part in BLOCKED_PATH_PARTS for part in path.parts):
|
|
raise ValueError(f"Unsafe patch path: {raw}")
|
|
if not token or token == ".":
|
|
raise ValueError(f"Empty patch path: {raw}")
|
|
return token
|
|
|
|
|
|
def patch_target_files(patch_text: str) -> list[str]:
|
|
targets: set[str] = set()
|
|
for line in patch_text.splitlines():
|
|
if line.startswith("--- ") or line.startswith("+++ "):
|
|
raw = line[4:].strip()
|
|
path = normalize_patch_path(raw)
|
|
if path:
|
|
targets.add(path)
|
|
return sorted(targets)
|
|
|
|
|
|
def validate_target_file_sha256(
|
|
skill_dir: Path,
|
|
target_files: list[str],
|
|
expected_sha256: dict[str, Any],
|
|
) -> tuple[list[str], dict[str, str]]:
|
|
failures: list[str] = []
|
|
observed: dict[str, str] = {}
|
|
for target in target_files:
|
|
path = skill_dir / target
|
|
expected = str(expected_sha256.get(target, ""))
|
|
if not expected:
|
|
failures.append(f"Approval target_file_sha256 missing target: {target}")
|
|
continue
|
|
if path.exists() and not path.is_file():
|
|
failures.append(f"Patch target is not a file: {target}")
|
|
continue
|
|
current = sha256_file(path) if path.exists() else ABSENT_FILE_SHA256
|
|
observed[target] = current
|
|
if current != expected:
|
|
failures.append(f"Target file baseline sha256 does not match approval ledger: {target}")
|
|
return failures, observed
|