Files
hermes-agent/tests/hermes_cli/test_desktop_install_after_update.py
Yuan Li cf0291065c test(desktop): pin macOS signing-downgrade guard and no-ad-hoc-fallback contract
Red on upstream/main, green with the fix:

- _install_rebuilt_macos_bundles refuses to replace a Team-ID-signed
  install with a locally signed build, a different Team ID, a different
  bundle identifier, or an unverifiable rebuild; matching publisher
  identities and ad-hoc installs keep swapping.
- _desktop_macos_relaunchable_fixup keeps the existing signature when the
  configured desktop.macos_signing_identity fails instead of degrading
  to legacy ad-hoc.
2026-09-29 19:43:22 -05:00

215 lines
9.5 KiB
Python

"""``hermes update`` refreshes the installed macOS ``Hermes.app`` from the rebuilt bundle (#52339).
``hermes desktop --build-only`` only packages into ``apps/desktop/release/``; Finder launches the
copy in ``/Applications``. These pin the contract of ``_install_rebuilt_macos_bundles``: a stale
or missing installed copy is (re)installed, a current one and a running one are never touched, and
a failed swap leaves the previous bundle launchable. Which paths count as installed is decided by
``_installed_desktop_apps``: a recorded copy that went missing comes back until Hermes' own GUI
uninstall drops the record.
"""
import shutil
from pathlib import Path
import pytest
from hermes_cli import main_desktop
def _bundle(root: Path, asar: bytes) -> Path:
app = root / "Hermes.app"
(app / "Contents" / "MacOS").mkdir(parents=True)
(app / "Contents" / "MacOS" / "Hermes").write_bytes(b"\xcf\xfa\xed\xfe")
(app / "Contents" / "Resources").mkdir()
(app / "Contents" / "Resources" / "app.asar").write_bytes(asar)
return app
def _asar(app: Path) -> bytes:
return (app / "Contents" / "Resources" / "app.asar").read_bytes()
@pytest.fixture
def rebuilt(tmp_path, monkeypatch):
monkeypatch.setattr(
main_desktop, "_stage_macos_bundle_copy",
lambda src, dst: shutil.copytree(src, dst, symlinks=True))
return _bundle(tmp_path / "apps" / "desktop" / "release" / "mac-arm64", b"rebuilt")
def test_stale_and_missing_bundles_are_installed_current_and_running_are_left_alone(rebuilt, tmp_path):
stale = _bundle(tmp_path / "Applications", b"stale")
current = _bundle(tmp_path / "home" / "Applications", b"rebuilt")
running = _bundle(tmp_path / "Volumes" / "Applications", b"older")
current_marker = current / "Contents" / "marker"
current_marker.write_text("untouched", encoding="utf-8")
missing = tmp_path / "missing" / "Hermes.app"
installed, problems = main_desktop._install_rebuilt_macos_bundles(
rebuilt, [stale, current, running, missing], running={running.resolve()})
assert installed == [stale, missing]
assert _asar(stale) == b"rebuilt" and _asar(missing) == b"rebuilt"
assert not (stale.parent / "Hermes.app.hermes-update-old").exists()
assert not (stale.parent / "Hermes.app.hermes-update-new").exists()
assert current_marker.read_text(encoding="utf-8") == "untouched"
# A live app is reported, never swapped under.
assert _asar(running) == b"older"
assert len(problems) == 1 and str(running) in problems[0]
def _patch_sigs(monkeypatch, summaries):
"""Stub the codesign probe: ``summaries(app) -> dict | None``."""
monkeypatch.setattr(
main_desktop.shutil, "which",
lambda name: "/usr/bin/codesign" if name == "codesign" else None)
monkeypatch.setattr(main_desktop, "_macos_signature_summary", lambda codesign, app: summaries(app))
def _publisher_sig(app, team="TEAM0123", ident="com.nousresearch.hermes", verified=True):
return {"team": team, "identifier": ident, "verified": verified}
def test_publisher_signed_install_is_never_replaced_by_local_build(rebuilt, tmp_path, monkeypatch):
"""#123748: a Team-ID-signed installation must survive an ad-hoc local rebuild.
Swapping a publisher-signed app for a locally signed build invalidates the
code-hash-bound keychain ACLs safeStorage credentials live under — the
update must retain the working app and report it instead.
"""
stale = _bundle(tmp_path / "Applications", b"stale")
_patch_sigs(monkeypatch, lambda app: None if app == rebuilt else _publisher_sig(stale))
installed, problems = main_desktop._install_rebuilt_macos_bundles(rebuilt, [stale], running=set())
assert installed == []
assert len(problems) == 1
assert "publisher-signed app (Team ID TEAM0123)" in problems[0]
assert "kept the existing app" in problems[0]
assert _asar(stale) == b"stale"
assert not (stale.parent / "Hermes.app.hermes-update-new").exists()
assert not (stale.parent / "Hermes.app.hermes-update-old").exists()
def test_team_id_mismatch_refuses_swap(rebuilt, tmp_path, monkeypatch):
stale = _bundle(tmp_path / "Applications", b"stale")
sigs = {rebuilt: _publisher_sig(rebuilt, team="TEAM9999"), stale: _publisher_sig(stale)}
_patch_sigs(monkeypatch, lambda app: sigs.get(app))
installed, problems = main_desktop._install_rebuilt_macos_bundles(rebuilt, [stale], running=set())
assert installed == [] and len(problems) == 1
assert "Team ID TEAM0123 does not match rebuilt TEAM9999" in problems[0]
assert _asar(stale) == b"stale"
def test_bundle_identifier_mismatch_refuses_swap(rebuilt, tmp_path, monkeypatch):
stale = _bundle(tmp_path / "Applications", b"stale")
sigs = {
rebuilt: _publisher_sig(rebuilt, ident="com.other.hermes"),
stale: _publisher_sig(stale),
}
_patch_sigs(monkeypatch, lambda app: sigs.get(app))
installed, problems = main_desktop._install_rebuilt_macos_bundles(rebuilt, [stale], running=set())
assert installed == [] and len(problems) == 1
assert "bundle identifier" in problems[0]
assert _asar(stale) == b"stale"
def test_unverifiable_rebuilt_refuses_swap(rebuilt, tmp_path, monkeypatch):
stale = _bundle(tmp_path / "Applications", b"stale")
sigs = {
rebuilt: _publisher_sig(rebuilt, verified=False),
stale: _publisher_sig(stale),
}
_patch_sigs(monkeypatch, lambda app: sigs.get(app))
installed, problems = main_desktop._install_rebuilt_macos_bundles(rebuilt, [stale], running=set())
assert installed == [] and len(problems) == 1
assert "strict signature verification" in problems[0]
assert _asar(stale) == b"stale"
def test_matching_publisher_signing_swaps(rebuilt, tmp_path, monkeypatch):
"""A rebuilt bundle with the SAME publisher identity is a legitimate update: swap."""
stale = _bundle(tmp_path / "Applications", b"stale")
sigs = {rebuilt: _publisher_sig(rebuilt), stale: _publisher_sig(stale)}
_patch_sigs(monkeypatch, lambda app: sigs.get(app))
installed, problems = main_desktop._install_rebuilt_macos_bundles(rebuilt, [stale], running=set())
assert installed == [stale] and problems == []
assert _asar(stale) == b"rebuilt"
def test_adhoc_installed_still_swaps(rebuilt, tmp_path, monkeypatch):
"""Ad-hoc installs (the local development flow) keep swapping regardless of rebuild sig."""
stale = _bundle(tmp_path / "Applications", b"stale")
sigs = {stale: {"team": None, "identifier": "com.nousresearch.hermes", "verified": True}}
_patch_sigs(monkeypatch, lambda app: sigs.get(app))
installed, problems = main_desktop._install_rebuilt_macos_bundles(rebuilt, [stale], running=set())
assert installed == [stale] and problems == []
assert _asar(stale) == b"rebuilt"
def test_failed_swap_keeps_the_previous_bundle_launchable(rebuilt, tmp_path, monkeypatch):
stale = _bundle(tmp_path / "Applications", b"stale")
real_rename = Path.rename
def fail_final_rename(self, target):
if self.name.endswith(".hermes-update-new"):
raise OSError("simulated rename failure")
return real_rename(self, target)
monkeypatch.setattr(Path, "rename", fail_final_rename)
installed, problems = main_desktop._install_rebuilt_macos_bundles(rebuilt, [stale], running=set())
assert installed == []
assert len(problems) == 1
assert stale.is_dir() and _asar(stale) == b"stale"
assert not (stale.parent / "Hermes.app.hermes-update-new").exists()
@pytest.mark.platforms("macos")
def test_recorded_app_that_went_missing_comes_back_until_gui_uninstall(rebuilt, tmp_path, monkeypatch):
from hermes_cli import gui_uninstall
from hermes_cli import main as hermes_main
home = tmp_path / "hermes-home"
(home / "hermes-agent").mkdir(parents=True)
monkeypatch.setenv("HERMES_HOME", str(home))
monkeypatch.setattr(hermes_main, "PROJECT_ROOT", home / "hermes-agent")
(rebuilt / "Contents" / "Resources" / "install-stamp.json").write_text('{"updateMechanism": "self"}', encoding="utf-8")
app = tmp_path / "Applications" / "Hermes.app"
shutil.copytree(rebuilt, app)
user_app = tmp_path / "user" / "Applications" / "Hermes.app"
user_app.parent.mkdir(parents=True)
monkeypatch.setattr(gui_uninstall, "packaged_gui_app_paths", lambda: [app, user_app])
monkeypatch.setattr(gui_uninstall, "desktop_userdata_dir", lambda: tmp_path / "userdata")
monkeypatch.setattr(main_desktop, "_desktop_packaged_executable",
lambda _d: rebuilt / "Contents" / "MacOS" / "Hermes")
monkeypatch.setattr(main_desktop, "_running_macos_app_bundles", set)
main_desktop._refresh_installed_desktop_apps(tmp_path) # current copy: recorded, untouched
app.rename(user_app) # moved by the user: found by its stamp, never doubled
main_desktop._refresh_installed_desktop_apps(tmp_path)
assert not app.exists()
shutil.rmtree(user_app)
main_desktop._refresh_installed_desktop_apps(tmp_path)
assert _asar(user_app) == b"rebuilt" and not app.exists()
gui_uninstall.desktop_install_record().unlink() # the reinstalled copy carries its own stamp,
main_desktop._refresh_installed_desktop_apps(tmp_path) # so it is recorded again from the copy
shutil.rmtree(user_app)
main_desktop._refresh_installed_desktop_apps(tmp_path)
assert _asar(user_app) == b"rebuilt"
gui_uninstall.uninstall_gui(tmp_path / "profile-home") # the record is machine-wide
main_desktop._refresh_installed_desktop_apps(tmp_path)
assert not user_app.exists()
assert not gui_uninstall.desktop_install_record().exists()