From 01e34cda7b445564bc314470f125eaeda971bd10 Mon Sep 17 00:00:00 2001 From: ethernet Date: Mon, 21 Sep 2026 19:38:22 -0400 Subject: [PATCH] fix(steward): one install-stamp resolver; identity fallback uses the process home steward.read_install_stamp read /install-stamp.json literally while version_info._resolve_stamp_file honoured HERMES_INSTALL_ROOT. A Nix package bakes its stamp outside the store's package dir, so `hermes --version` said nix while sealed_steward said "unknown" and uninstall printed "managed by unknown". steward.install_stamp_path now owns the location: the executing tree resolves through pm.paths.install_root, any other tree is literal. version_info._resolve_repo_dir fell back to the context-local profile home for a value cached process-wide; the running code's identity is a process fact, so it now reads the process home. --- hermes_cli/steward.py | 21 ++++++++++++++++++--- hermes_cli/version_info.py | 18 ++++++++---------- tests/hermes_cli/test_steward_detection.py | 21 +++++++++++++++++++++ 3 files changed, 47 insertions(+), 13 deletions(-) diff --git a/hermes_cli/steward.py b/hermes_cli/steward.py index aad8e175b8..2bdbed310a 100644 --- a/hermes_cli/steward.py +++ b/hermes_cli/steward.py @@ -165,6 +165,23 @@ def steward_uninstall_message(steward: str, platform: "str | None" = None) -> st return _STEWARD_UNINSTALL_FALLBACK.format(steward=steward) +def install_stamp_path(project_root: Path) -> Path: + """THE stamp location for ``project_root``, shared by every stamp reader. + + Beside the code in checkouts, Docker and desktop payloads. A Nix package + bakes the stamp outside the store's package dir and its wrapper carries + ``HERMES_INSTALL_ROOT`` for the executing tree only — so the executing + tree resolves through pm.paths.install_root, any other tree is taken + literally. Two resolvers here once classified Nix as "unknown". + """ + from pm.paths import install_root, repo_root + + root = Path(project_root) + if root.resolve() == repo_root(): + root = install_root() + return root / BUILD_INFO_NAME + + def read_install_stamp(project_root: Path) -> dict: """The build stamp of ``project_root``, or ``{}``. @@ -173,9 +190,7 @@ def read_install_stamp(project_root: Path) -> dict: refuses (see :func:`sealed_steward`), so garbage degrades safely. """ try: - data = json.loads( - (Path(project_root) / BUILD_INFO_NAME).read_text(encoding="utf-8-sig") - ) + data = json.loads(install_stamp_path(project_root).read_text(encoding="utf-8-sig")) except (OSError, ValueError): return {} return data if isinstance(data, dict) else {} diff --git a/hermes_cli/version_info.py b/hermes_cli/version_info.py index 50b643fc84..8dbec6d917 100644 --- a/hermes_cli/version_info.py +++ b/hermes_cli/version_info.py @@ -61,9 +61,11 @@ def _resolve_repo_dir() -> Path | None: repo_dir = Path(__file__).parent.parent.resolve() if (repo_dir / ".git").exists(): return repo_dir - from hermes_constants import get_hermes_home + # The PROCESS home: this is the running code's identity and is cached + # process-wide, so a profile's context-local override must not pick it. + from hermes_constants import get_process_hermes_home - candidate = get_hermes_home() / "hermes-agent" + candidate = get_process_hermes_home() / "hermes-agent" if (candidate / ".git").exists(): return candidate return None @@ -79,16 +81,12 @@ def _parse_nonnegative(value: str | None) -> int | None: # --- Install stamp reader --------------------------------------------------- -# The stamp file lives at the install root: beside the code in source -# checkouts and Docker (which writes it to the project root), and in the -# artifact's resources dir for sealed installs — whose processes carry -# HERMES_INSTALL_ROOT (the Nix wrapper points it at the store path's -# share/hermes-agent, where the stamp is baked). One resolution path for -# every steward; no stamp-specific env override. def _resolve_stamp_file() -> Path | None: - from pm.paths import install_root + """The executing tree's stamp (steward.install_stamp_path owns the location).""" + from hermes_cli.steward import install_stamp_path + from pm.paths import repo_root - p = install_root() / "install-stamp.json" + p = install_stamp_path(repo_root()) return p if p.is_file() else None diff --git a/tests/hermes_cli/test_steward_detection.py b/tests/hermes_cli/test_steward_detection.py index 629e34557a..890f07aadc 100644 --- a/tests/hermes_cli/test_steward_detection.py +++ b/tests/hermes_cli/test_steward_detection.py @@ -76,6 +76,27 @@ class TestStampDrivenDetection: assert sealed_steward(root) == "unknown" assert classify_install(root) == ("unknown", False) + def test_executing_nix_tree_reads_the_stamp_the_wrapper_points_at(self, tmp_path, monkeypatch): + """A Nix package bakes the stamp outside the store's package dir; its wrapper + carries HERMES_INSTALL_ROOT. The executing tree must classify as nix (not + "unknown"), the way version_info already reports it.""" + from hermes_cli.version_info import _resolve_stamp_file + + package = tmp_path / "store" / "lib" / "hermes-agent" + package.mkdir(parents=True) + share = tmp_path / "store" / "share" / "hermes-agent" + share.mkdir(parents=True) + _stamp(share, mechanism="external", distribution="nix") + monkeypatch.setattr("pm.paths.repo_root", lambda: package) + monkeypatch.setenv("HERMES_INSTALL_ROOT", str(share)) + + assert sealed_steward(package) == "nix" + assert _resolve_stamp_file() == share / "install-stamp.json" + # Another tree is taken literally: the wrapper speaks for the executing tree only. + other = tmp_path / "elsewhere" + other.mkdir() + assert sealed_steward(other) == "unknown" + def test_malformed_stamp_degrades_to_unknown(self, tmp_path): root = tmp_path / "install" root.mkdir()