Files
PaperClipAI/packages/paperclip-runner/scripts/materialize-hermes.py
T
DottaandPaperclip 6788c30cf2 fix(hermes): make Mac runtime signing reproducible across hosts
Specify 16 KiB code-signing pages for the normalized Python library. The 4 KiB default reproduces the exact rejected cloud hash; 16 KiB reproduces the existing reviewed bytes. Keep closure pins and dependency locks unchanged.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-08 12:28:13 -05:00

80 lines
4.2 KiB
Python

"""Build a native closure after provision-hermes verifies source and resolves uv.lock.
This runs only during explicit provisioning, never in an agent's execution.
"""
import hashlib
import json
import os
import shutil
import sys
import sysconfig
import subprocess
from pathlib import Path
source, destination, provider = map(Path, sys.argv[1:])
if destination.exists():
raise ValueError("Hermes provisioning destination must be new")
destination.mkdir(parents=True, mode=0o700)
def copy_tree(origin, target, excluded=()):
def ignore(directory, names):
return [name for name in names if name in excluded or name == "__pycache__"
or name.endswith((".pyc", ".pyo"))]
shutil.copytree(origin, target, symlinks=False, ignore=ignore, dirs_exist_ok=True)
copy_tree(Path(sys.base_prefix), destination / "python", ("include", "share"))
# uv rewrites sysconfig to its installation home. That path differs between
# local consumers, root image builds and the Daytona runtime user. Normalize
# this build metadata so the verified closure is relocatable and reproducible.
prefixes = {sys.base_prefix, str(Path(sys.base_prefix).resolve()), sysconfig.get_config_var("prefix")}
for metadata in (destination / "python/lib/python3.12").glob("_sysconfigdata_*.py"):
text = metadata.read_text()
for prefix in sorted((p for p in prefixes if isinstance(p, str)), key=len, reverse=True):
text = text.replace(prefix, "/paperclip-hermes/python")
metadata.write_text(text)
if sys.platform == "darwin":
# uv also rewrites the Mach-O install name. Restore a relative identity and
# a deterministic ad-hoc signature after changing its load command.
library = destination / "python/lib/libpython3.12.dylib"
if not shutil.which("install_name_tool") or not shutil.which("codesign"):
raise ValueError("Hermes provisioning requires macOS Command Line Tools (install_name_tool and codesign)")
subprocess.run(["install_name_tool", "-id", "@rpath/libpython3.12.dylib", str(library)], check=True)
# macOS 15 and 26 default to different code-signing page granularities.
# Keep the reviewed library's 16 KiB hash slots on every provisioning host.
subprocess.run(["codesign", "--force", "--sign", "-", "--identifier", "paperclip.hermes.libpython3.12", "--timestamp=none", "--pagesize", "16384", str(library)], check=True)
site = destination / "python/lib/python3.12/site-packages"
site.mkdir(parents=True, exist_ok=True)
# Do not retain editable-install locators, virtualenv hooks, or provenance files
# containing the build machine's absolute paths. Hermes source is loaded below.
for item in (source / ".venv/lib/python3.12/site-packages").iterdir():
if item.name.startswith(("__editable__", "_virtualenv", "hermes_agent-")) or item.name == "__pycache__":
continue
if item.suffix == ".pth":
raise ValueError("Unreviewed Python site hook in Hermes dependencies")
if item.is_dir():
copy_tree(item, site / item.name)
elif item.suffix not in (".pyc", ".pyo"):
shutil.copy2(item, site / item.name)
copy_tree(source, destination / "app", (".venv", ".git", ".github", "tests", "website", "docs", ".pytest_cache"))
for name in ("bridge.py", "billing.py", "policy.py", "tool_process.py", "entry.py", "version.json"):
shutil.copy2(provider / name, destination / name)
# Installed RECORD and direct_url metadata can contain mutable build locations.
for item in destination.rglob("*.dist-info/direct_url.json"):
item.unlink()
for item in destination.rglob("*.dist-info/RECORD"):
item.unlink()
entries = []
for item in sorted(destination.rglob("*")):
if not item.is_file():
continue
data = item.read_bytes()
entries.append({"path": item.relative_to(destination).as_posix(), "sha256": hashlib.sha256(data).hexdigest(),
"size": len(data), "executable": bool(item.stat().st_mode & 0o111)})
entries.sort(key=lambda entry: entry["path"].encode("utf-16-be"))
serialized = json.dumps(entries, separators=(",", ":"), ensure_ascii=False).encode()
digest = hashlib.sha256(serialized).hexdigest()
(destination / "manifest.json").write_text(json.dumps({"entries": entries}, separators=(",", ":")))
print(json.dumps({"closureSha256": digest, "files": len(entries), "bytes": sum(e["size"] for e in entries)}))