From c314285f077e91937c995d419b9fa40af9eb4981 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Tue, 3 Nov 2026 00:01:33 -0800 Subject: [PATCH] fix(pm): judge a build's hermes-agent markers on its own interpreter Round-3 P3 (marker environment): the gate evaluated requirement markers with default_environment() of the calling process, while the build's own marker evaluation asks the generation's interpreter (_target_environment), because PM can run under a bootstrap Python. A build now passes its interpreter, so it leaves out exactly what the loader (running in the generation) skips. Callers outside a build keep this process's markers: no probe at loader start. --- pm/packages.py | 3 ++- pm/workspace.py | 31 ++++++++++++++++++------------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/pm/packages.py b/pm/packages.py index f7d6531baa..b446c1c5d2 100644 --- a/pm/packages.py +++ b/pm/packages.py @@ -534,7 +534,8 @@ class Venv(StatePackage): # An update does evict them, loudly: it must not fail on another profile's config. from pm.plugins_state import enabled_plugins_ordered enabled_plugins_ordered(skip_invalid_secondary=skip_invalid_secondary) - members = [] if repair else (build_member_dirs(extras) if plugin_dirs is None else plugin_dirs) + members = [] if repair else (build_member_dirs(extras, environment.python) if plugin_dirs is None + else plugin_dirs) try: generation.mkdir(parents=True) (generation / ".lease-managed").touch() diff --git a/pm/workspace.py b/pm/workspace.py index c8317ca242..addbe5a626 100644 --- a/pm/workspace.py +++ b/pm/workspace.py @@ -315,12 +315,15 @@ def enabled_member_dirs(*, proposed_home=None, enabled=None, disabled=None) -> l return members -def build_member_dirs(extras: Iterable[str]) -> list[Path]: +def build_member_dirs(extras: Iterable[str], python: Path | None = None) -> list[Path]: """``enabled_member_dirs`` for a build selecting core *extras*: a plugin extra they select - can add a ``hermes-agent`` requirement the running Hermes misses, which sits out too.""" - extras = list(extras) - return [path for path in enabled_member_dirs() - if not core_requirement_error(read_python_declaration(path), extras)] + can add a ``hermes-agent`` requirement the running Hermes misses, which sits out too. + A build passes its interpreter *python*, whose markers the generation (and so the loader) + sees; PM itself may run under a bootstrap Python.""" + extras, members = list(extras), enabled_member_dirs() + target = _target_environment(python) if members and python is not None else None + return [path for path in members + if not core_requirement_error(read_python_declaration(path), extras, target)] def _member_key(identity: Path) -> str: @@ -539,16 +542,18 @@ def _recorded_extras() -> list[str]: return [extra for extra in extras if isinstance(extra, str)] if isinstance(extras, list) else [] -def core_requirement_error(declaration, extras: Iterable[str] | None = None) -> Optional[str]: +def core_requirement_error(declaration, extras: Iterable[str] | None = None, + target: Mapping[str, str] | None = None) -> Optional[str]: """Why the plugin's applicable requirement on ``hermes-agent`` rejects the running Hermes. PM satisfies ``hermes-agent`` from the checkout itself: a runtime requirement naming it is dropped, and a selected extra naming it binds to the workspace root, where uv ignores the specifier. So the specifier is judged here, PEP 440 like any requirement, under *extras* - (the core extras the build selects; ``None`` reads the recorded selection) and this host's - markers. The identity is the one ``requires_hermes`` reads (the committed pyproject - version is a placeholder); with none (``unknown``) nothing is judged. It is read only for - a plugin that names core: the read walks git, and every plugin passes here at startup. + (the core extras the build selects; ``None`` reads the recorded selection) and the markers + of *target* (the installing interpreter; ``None`` is this process, which the loader runs + in). The identity is the one ``requires_hermes`` reads (the committed pyproject version is + a placeholder); with none (``unknown``) nothing is judged. It is read only for a plugin + that names core: the read walks git, and every plugin passes here at startup. """ import tomllib @@ -560,15 +565,15 @@ def core_requirement_error(declaration, extras: Iterable[str] | None = None) -> if declaration.external: return None - target = default_environment() + environment = default_environment() if target is None else target specs, installed = list(declaration.requirements), set() if declaration.pyproject is not None: document = tomllib.loads(declaration.pyproject.read_text(encoding="utf-8-sig")) - installed = _selected_member_extras(document, _recorded_extras() if extras is None else extras, target)[0] + installed = _selected_member_extras(document, _recorded_extras() if extras is None else extras, environment)[0] specs += [spec for name, values in document["project"].get("optional-dependencies", {}).items() if _extra_key(name) in installed for spec in values] pins = [requirement for requirement in _self_references(specs, "hermes-agent") - if requirement.specifier and _reference_active(requirement, ("", *sorted(installed)), target)] + if requirement.specifier and _reference_active(requirement, ("", *sorted(installed)), environment)] if not pins: return None current = running_hermes_version()