fix(steward): one install-stamp resolver; identity fallback uses the process home

steward.read_install_stamp read <root>/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.
This commit is contained in:
ethernet
2026-09-21 19:38:22 -04:00
parent 525c09142f
commit 01e34cda7b
3 changed files with 47 additions and 13 deletions

View File

@@ -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 {}

View File

@@ -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

View File

@@ -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()