Files
hermes-agent/hermes_cli/desktop_update_verify.py

119 lines
5.5 KiB
Python

"""Read-only verification at the Windows Desktop handoff receipt boundary."""
import json
from pathlib import Path, PurePosixPath
import re
import struct
from hermes_cli.main_desktop import (
_HTML_TAG_WITH_URL,
_MODULE_TAG,
_desktop_build_needed,
_desktop_exe_integrity_error,
_desktop_packaged_executable,
)
def _verify_packaged_entry(resources: Path) -> None:
"""Read ASAR's Pickle header and the entry declared by packaged package.json.
dist/** is unpacked by electron-builder. No Node install or application
launch is needed at this boundary; this is not a full dependency audit.
"""
archive = resources / "app.asar"
try:
with archive.open("rb") as stream:
size, header_size, payload_size, json_size = struct.unpack("<4I", stream.read(16))
if (size != 4 or header_size != payload_size + 4
or payload_size != 4 + ((json_size + 3) // 4) * 4
or not 0 < json_size <= 64 * 1024 * 1024
or 8 + header_size > archive.stat().st_size):
raise ValueError("invalid ASAR header")
header = json.loads(stream.read(json_size))
def read_member(name: str) -> bytes:
path = PurePosixPath(name)
if not name or path.is_absolute() or ".." in path.parts or "\\" in name or ":" in name:
raise ValueError("invalid ASAR entry path")
node = header
for part in path.parts:
node = node["files"][part]
length = node["size"]
if not isinstance(length, int) or length <= 0:
raise ValueError(f"empty ASAR entry: {name}")
if node.get("unpacked"):
data = (resources / "app.asar.unpacked" / path).read_bytes()
else:
offset = int(node["offset"])
if offset < 0 or 8 + header_size + offset + length > archive.stat().st_size:
raise ValueError(f"truncated ASAR entry: {name}")
stream.seek(8 + header_size + offset)
data = stream.read(length)
if len(data) != length or not data.strip():
raise ValueError(f"incomplete ASAR entry: {name}")
return data
package = json.loads(read_member("package.json"))
read_member(package["main"]).decode("utf-8")
except (OSError, ValueError, KeyError, TypeError, struct.error) as exc:
raise RuntimeError(f"The updated Desktop archive or main entry is invalid: {exc}") from exc
index = resources / "app.asar.unpacked" / "dist" / "index.html"
try:
html = index.read_text(encoding="utf-8-sig")
if not any(_MODULE_TAG.search(match.group(0))
and match.group(0).lower().startswith("<script")
and not re.match(r"^[a-z]+:|^//", match.group(1), re.IGNORECASE)
for match in _HTML_TAG_WITH_URL.finditer(html)):
raise ValueError("renderer has no local module entry")
except (OSError, ValueError) as exc:
raise RuntimeError(f"The updated Desktop renderer entry is invalid: {exc}") from exc
def checkout_root() -> Path:
"""The checkout this module was imported from: the only root the receipt may describe."""
return Path(__file__).resolve().parent.parent
def _root_to_verify(project_root: Path | None) -> Path:
"""The root the receipt may describe: a caller's root is a hint, not a contract.
The hand-off script is read before the pull, so an update that ships a fix to
the caller side cannot apply it to its own run: a pre-fix ``windows.ps1`` still
passes ``Path.cwd()``, which is HERMES_HOME and never holds a packaged Desktop
app. Honour a caller-supplied root only while it actually carries one, and fall
back to the checkout this module was imported from -- the tree the update just
wrote. A supplied root that does carry a packaged app is still verified as
given, so real damage there stays fail-closed.
"""
if project_root is None:
return checkout_root()
if _desktop_packaged_executable(project_root / "apps" / "desktop") is not None:
return project_root
return checkout_root()
def verify_windows_desktop_update(project_root: Path | None = None) -> None:
"""Raise when a zero-exit updater left an incomplete or stale packaged app.
The root defaults to the imported checkout, never the caller's cwd: the hand-off
is spawned from HERMES_HOME by the pre-update Desktop, and a cwd-derived root
reported a healthy install as "Desktop executable is missing" (Sep 2026). A
caller-supplied root with no packaged app falls back to that same checkout, so a
stale in-flight hand-off cannot fail a healthy install either.
"""
project_root = _root_to_verify(project_root)
desktop = project_root / "apps" / "desktop"
executable = _desktop_packaged_executable(desktop)
if executable is None:
raise RuntimeError("The updated Desktop executable is missing")
error = _desktop_exe_integrity_error(executable)
if error:
raise RuntimeError(f"The updated Desktop executable is invalid: {error}")
_verify_packaged_entry(executable.parent / "resources")
if _desktop_build_needed(desktop, project_root, source_mode=False):
raise RuntimeError("The updated Desktop build is stale, unstamped, or incomplete")
if __name__ == "__main__":
verify_windows_desktop_update()