Files
hermes-agent/pm/features.py
ethernet 02eb115f60 feat(pm): frozen bundle feature set — enabled-features.json
Lazy installs OFF is now a real contract, not just 'refuse
everything': the bundle's EXACT extras list is the frozen feature set
(settled 2026-09-02 plan, task 5).

- pm/features.py: write_features()/read_features() over
  enabled-features.json at the payload root (bundle-written, beside
  manifest.json; at runtime store_root().parent — the same relative
  location on both install kinds). installed_extras() records what
  `uv sync --all-extras` ACTUALLY installed on the target: every
  declared extra whose pm anchor resolves in the staged venv —
  marker-gated extras show up as missing anchors, the honest
  per-platform record.
- cmd_bundle: after the staged venv sync, write the features file into
  the payload ('✓ enabled-features.json (N extras recorded)').
- sync_venv: when security.allow_lazy_installs is false and the file
  exists, requested extras OUTSIDE the frozen set are refused with a
  message naming the policy.
- Venv.apply: plugin members + lazy-off + frozen file = loud InstallError
  (the bundle IS the install; never union plugin deps into it).

tests/pm/test_features.py: 7 tests (roundtrip, absent/garbage reads,
payload-root path, anchor-truth installed_extras, frozen refusal,
in-set pass). tests/pm: 162 passed 0 failed.
2026-09-02 19:54:34 -04:00

112 lines
3.9 KiB
Python

"""The enabled-features file: the exact extras the install carries.
The bundle is built with `uv sync --all-extras`, but WHICH extras
actually resolve differs per platform (markers gate some off). The
shipped default is the EXACT list that installed — written at bundle
time beside the payload, read at sync time:
- lazy installs OFF (security.allow_lazy_installs: false): the feature
list is FROZEN to this file; pm sync never deviates from it and never
installs a plugin dep.
- lazy installs ON: the file is the baseline; extras union on top.
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Optional
FEATURES_FILENAME = "enabled-features.json"
def features_path(base_dir: Optional[Path] = None) -> Path:
"""Where the enabled-features file lives. In a bundle, the payload
root (beside manifest.json — bundle-written, sealed-shipped). At
runtime, the runtime dir (beside the byte store — per-install,
writable on every install kind). Same relative location on both:
a sealed install's store_root() is <payload>/tools, and the file
sits at the payload root = store_root().parent."""
if base_dir is not None:
return base_dir / FEATURES_FILENAME
from pm.paths import store_root
return store_root().parent / FEATURES_FILENAME
def write_features(extras: list[str], base_dir: Optional[Path] = None) -> Path:
"""Record the exact extras an install carries. Sorted, deduped."""
path = features_path(base_dir)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(
json.dumps({"schema": 1, "extras": sorted(set(extras))}, indent=2) + "\n",
encoding="utf-8",
)
return path
def read_features() -> Optional[list[str]]:
"""The frozen/baseline feature list; None when no file exists (source
installs without a bundle — the baseline is the recorded venv
state)."""
try:
data = json.loads(features_path().read_text(encoding="utf-8-sig"))
except (OSError, ValueError):
return None
extras = data.get("extras")
if not isinstance(extras, list):
return None
return sorted({str(e) for e in extras})
def declared_extras(repo_dir: Path) -> list[str]:
"""Every extra the repo's pyproject declares."""
import tomllib
with (repo_dir / "pyproject.toml").open("rb") as f:
data = tomllib.load(f)
return sorted(data.get("project", {}).get("optional-dependencies", {}))
def installed_extras(repo_dir: Path, venv_dir: Path) -> list[str]:
"""The extras that ACTUALLY installed on this target: every declared
extra whose pm anchor import resolves in the venv. Markers-gated
extras show up as missing anchors — the honest per-target record of
what `uv sync --all-extras` put on THIS machine."""
from pm.extras import ANCHORS
installed: list[str] = []
for extra in declared_extras(repo_dir):
anchor = ANCHORS.get(extra, extra.replace("-", "_"))
if isinstance(anchor, str):
anchor = (anchor,)
if all(_importable_in(anchor_member, venv_dir) for anchor_member in anchor):
installed.append(extra)
return sorted(installed)
def _importable_in(anchor: str, venv_dir: Path) -> bool:
"""Does the anchor import resolve inside venv_dir's site-packages?"""
import importlib.util
import sys
import sysconfig
from pathlib import Path as _P
sites = set()
pure = sysconfig.get_paths(vars={"base": str(venv_dir)}).get("purelib")
if pure:
sites.add(_P(pure))
plat = sysconfig.get_paths(vars={"base": str(venv_dir)}).get("platlib")
if plat:
sites.add(_P(plat))
saved = list(sys.path)
try:
sys.path = [str(s) for s in sites]
try:
return importlib.util.find_spec(anchor) is not None
except (ImportError, ValueError):
return False
finally:
sys.path = saved