Files
hermes-agent/scripts/bundles/payload.py
ethernet f6713f2762 perf(bundles): bake payload bytecode; ship only what the sealed runtime reads
First launch of a bundled payload paid a cold-compile stall: the launcher
redirects bytecode writes to a user-level cache (signature-breaking on
macOS, read-only mount on AppImage/MSIX), so every import compiled from
source. Now staging bakes the cache into the payload:

- compileall with the payload's OWN staged 3.14 interpreter, unchecked-
  hash pycs: repack mtimes cannot invalidate them, a stale source can
  never trigger a rewrite, and read-only pycs mean the macOS signature
  never observes a change. Dirs stay writable — in-place rebuilds
  rmtree the tree; asserted coverage plus unchecked-hash means no
  cache-miss write can target them.
- coverage is the perf contract: the bake FAILS if any parseable module
  lacks a pyc (empirically 0 unparseable files ship, so compileall is
  strict). Probe suite: py_compile/cache_from_source, PEP 552 flags,
  multi-root read, stale-source no-rewrite, read-only cache-dir import.
- launcher: the baked marker makes configure() leave sys.pycache_prefix
  UNSET — the prefix relocates reads too and would hide the baked pycs.
  Payload modules read their source-adjacent cache (Python's default
  multi-root lookup); plugin/user modules keep caching beside their own
  sources under HERMES_HOME. Unmarked payloads keep the old redirect.
- snapshot(): the sealed payload ships without tests/website/evals/
  .github/nix/docker/tests-js (~69MB, 46% of tracked bytes) and without
  apps/ui-tui/web/scripts — CI prebuilds those products, and
  is_bundled_payload routes sealed updates to the channel updater, so
  the rebuild graph never runs in a bundle (linux_desktop_entry degrades
  to the themed icon). Frontend product staging keeps the full tree.
- test_bundle_native now stages the FULL relocatable toolchain (a bare
  interpreter ELF falls back to its compile-time /install prefix and
  cannot create a venv), and runs on the real 3.14 for the first time
  this campaign — the whole battery had been running 3.12 against the
  3.14-pinned lock.
2026-09-14 19:26:04 -04:00

189 lines
7.8 KiB
Python

"""Distribution snapshots, PM facts and portable link sealing."""
from __future__ import annotations
import argparse
import json
import os
import shutil
import subprocess
import sys
import tarfile
import tempfile
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
# Snapshot dirs the sealed agent payload never reads. Tests, docs and CI
# definitions never ship; the frontend/desktop sources and build scripts are
# prebuilt by CI into staged products (AgentInputs.frontends) — a sealed
# payload never re-enters the source-build graph (is_bundled_payload routes
# updates to the channel updater), and linux_desktop_entry degrades to the
# themed icon when apps/desktop/assets is absent. Excluded here means:
# not packaged, not compiled, not baked. Frontend product staging
# (scripts/bundles/stage.py) needs its full tree and passes no exclusions.
INERT_SNAPSHOT_DIRS = (
"tests", "tests-js", "website", "evals", ".github", "nix", "docker",
"apps", "ui-tui", "web", "scripts",
)
def snapshot(repo: Path, ref: str, destination: Path, exclude: tuple[str, ...] = ()) -> None:
"""Archive a resolved git revision without carrying checkout metadata."""
repo, destination = repo.resolve(), destination.resolve()
if repo == destination or repo.is_relative_to(destination):
raise ValueError("the snapshot destination must not contain the source checkout")
with tempfile.TemporaryDirectory(prefix="hermes-archive-") as temp:
archive = Path(temp) / "source.tar"
pathspecs = [f":(exclude){name}" for name in exclude]
subprocess.run(
["git", "archive", "--format=tar", "--output", str(archive), ref, "--", *pathspecs],
cwd=repo, check=True)
if destination.exists():
shutil.rmtree(destination)
destination.mkdir(parents=True)
with tarfile.open(archive) as source:
source.extractall(destination, filter="data")
def _payload_file(root: Path, relative: str) -> Path:
path = root / relative
if path.is_symlink() or not path.resolve().is_relative_to(root.resolve()):
raise ValueError(f"metadata path escapes payload or is symlinked: {path}")
return path
def _share_metadata(path: Path) -> None:
# Atomic PM publication stays private; only packaged, non-secret records
# cross this boundary. Windows installers own their ACL policy.
if os.name != "nt":
path.chmod(0o644)
def _discard_build_locks(root: Path) -> None:
for name in ("venv", "pm-runtime"):
_payload_file(root, f"{name}/.lock").unlink(missing_ok=True)
def record_tools(root: Path, lock_path: Path, target: str, entries: dict[str, str]) -> None:
from pm.lock import Facts, Lockfile
from pm.registry import get_package
from pm.store import tree_digest
store = root / "tools"
facts, lock = Facts(_payload_file(root, "tools/facts.json")), Lockfile(lock_path)
for name, entry_name in entries.items():
entry = store / entry_name
version, artifacts = lock.version(name), lock.artifacts(name, target)
if not entry.is_dir() or not version or not artifacts:
raise ValueError(f"incomplete payload tool: {name}")
facts.record(name, version, entry_name, get_package(name).env(entry, target), store,
target=target, artifacts=[a["sha256"] for a in artifacts], digest=tree_digest(entry))
if entries:
_share_metadata(facts.path)
def rehash_tools(root: Path) -> int:
"""Record final tool bytes before the enclosing package is signed."""
from pm.lock import Facts
store = root / "tools"
facts = Facts(_payload_file(root, "tools/facts.json"), strict=True)
count = facts.refresh_digests(store)
_share_metadata(facts.path)
return count
def seal_pm_runtime(root: Path, python: Path) -> dict:
"""Record a resident PM runtime without Windows' CWD-bound redirector.
Sealed workers execute the base interpreter with -I -S and add only the
recorded site directory. Its paths remain valid after the payload moves.
"""
root, python = root.resolve(), python.resolve()
if not python.is_relative_to(root) or not python.is_file():
raise ValueError(f"PM interpreter must belong to the payload: {python}")
runtime = root / "pm-runtime"
sites = list(runtime.glob("lib/python*/site-packages")) + list(runtime.glob("Lib/site-packages"))
if len(sites) != 1:
raise ValueError(f"PM dependency directory missing or ambiguous: {runtime}")
marker = {
"python": Path(os.path.relpath(python, runtime)).as_posix(),
"sitePackages": sites[0].relative_to(runtime).as_posix(),
}
cfg = _payload_file(root, "pm-runtime/pyvenv.cfg")
marker_path = _payload_file(root, "pm-runtime/pm-runtime.json")
lines = cfg.read_text(encoding="utf-8-sig").splitlines()
lines = [line for line in lines if line.partition("=")[0].strip() not in
{"home", "executable", "base-executable", "base-prefix", "base-exec-prefix", "command"}]
lines.insert(0, f"home = {os.path.relpath(python.parent, runtime)}")
cfg.write_text("\n".join(lines) + "\n", encoding="utf-8")
# A sealed runtime is not activated, and copied Windows redirectors cannot
# follow its relative home from arbitrary working directories.
bindir = runtime / ("Scripts" if os.name == "nt" else "bin")
for entry in bindir.iterdir():
if os.name == "nt" or not entry.is_symlink():
if entry.is_file():
entry.unlink()
_relativize_bin_links(root, runtime / "bin")
marker_path.write_text(json.dumps(marker, indent=2) + "\n", encoding="utf-8")
_share_metadata(marker_path)
_discard_build_locks(root)
return marker
def relativize_links(root: Path) -> int:
"""Only dependency-venv links move; framework links belong to codesign."""
root = root.resolve()
_discard_build_locks(root)
return sum(_relativize_bin_links(root, root / name / "bin") for name in ("venv", "pm-runtime"))
def _relativize_bin_links(root: Path, directory: Path) -> int:
count = 0
if not directory.is_dir():
return count
for link in directory.iterdir():
if not link.is_symlink():
continue
target = os.readlink(link)
if not os.path.isabs(target):
# Keep sibling chains intact; their absolute store link is rewritten separately.
if not Path(os.path.abspath(directory / target)).is_relative_to(root):
raise ValueError(f"link escapes payload: {link} -> {target}")
continue
resolved = (directory / target).resolve()
if not resolved.is_relative_to(root):
parts = Path(target).parts
if "tools" not in parts:
raise ValueError(f"link escapes payload: {link} -> {target}")
resolved = root.joinpath(*parts[parts.index("tools"):]).resolve()
if not resolved.is_relative_to(root) or not resolved.exists():
raise ValueError(f"missing payload link target: {link} -> {target}")
relative = os.path.relpath(resolved, directory)
if relative != target:
link.unlink()
link.symlink_to(relative)
count += 1
for link in directory.iterdir():
if link.is_symlink() and (not link.resolve().is_relative_to(root) or not link.exists()):
raise ValueError(f"invalid relative payload link: {link}")
return count
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("action", choices=["relocate", "rehash"])
parser.add_argument("payload", type=Path)
args = parser.parse_args()
if args.action == "rehash":
print(f"rehashed {rehash_tools(args.payload)} payload tools")
else:
relativize_links(args.payload)
if __name__ == "__main__":
main()