"""Digest-pinned, owner-configured Python runtimes for bwrap.""" from __future__ import annotations import hashlib import json import os import stat from pathlib import Path RUNTIME_MOUNT = "/opt/sandboxer/runtime" def runtime_digest(root: Path) -> str: """Hash the complete artifact, including modes and internal symlink targets.""" if not root.is_absolute() or root.resolve(strict=True) != root or not root.is_dir(): raise ValueError("runtime path must be a canonical absolute directory") records = [] for path in sorted(root.rglob("*")): mode = path.lstat().st_mode if stat.S_ISLNK(mode): target = os.readlink(path) if Path(target).is_absolute() or not path.resolve(strict=True).is_relative_to(root): raise ValueError("runtime symlink escapes the artifact") value = ["symlink", target] elif stat.S_ISREG(mode): with path.open("rb") as stream: value = ["file", hashlib.file_digest(stream, "sha256").hexdigest()] elif stat.S_ISDIR(mode): value = ["directory"] else: raise ValueError("runtime contains an unsupported special file") records.append([path.relative_to(root).as_posix(), stat.S_IMODE(mode), *value]) return hashlib.sha256(json.dumps(records, separators=(",", ":")).encode()).hexdigest() def verified_runtime(config: dict) -> Path | None: """Only owner extension configuration can select a runtime artifact.""" runtime = config.get("runtime") if runtime is None: return None if not isinstance(runtime, dict) or set(runtime) != {"path", "sha256"}: raise ValueError("runtime requires exactly path and sha256") if not isinstance(runtime["path"], str) or not isinstance(runtime["sha256"], str): raise ValueError("runtime path and sha256 must be strings") root = Path(runtime["path"]) if (not (root / "pyvenv.cfg").is_file() or not (root / "bin/python3").is_file() or (root / ".git").exists()): raise ValueError("runtime must be a standalone Python environment, not a checkout") if runtime_digest(root) != runtime["sha256"]: raise ValueError("runtime artifact digest does not match owner configuration") return root