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:
@@ -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 {}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user