Files
hermes-agent/pm/plugin_inputs.py
ethernet 9b4ed5f5ec refactor(pm): one plugin-input request for sync_venv and venv_is_current
sync_venv took four mutually exclusive plugin kwargs (plugin_dirs,
extra_plugin_dirs, selection, staged_plugin) and venv_is_current two, with
runtime ValueErrors guarding the combinations. Replace them with a single
`plugins=` argument typed as one of pm.plugin_inputs.Members, Candidates,
Selection or StagedUpdate, so a conflicting request cannot be expressed.
The module also owns the worker wire encoding that client.py and worker.py
each duplicated.

pm.install.sync_venv is split into cohesive helpers (feature policy,
install lock, publication snapshot, target selection, commit) with the
same ordering, receipts and recovery; its CC drops from 54 to 16.

All in-tree callers and tests move to the new argument.
2026-09-24 14:08:21 -04:00

72 lines
2.5 KiB
Python

"""What a venv sync or currency probe should take as its plugin members.
Each request names exactly one source, so conflicting member inputs cannot be
expressed. Stdlib only: the client, the worker and the engine all import it.
"""
from __future__ import annotations
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from pathlib import Path
@dataclass(frozen=True)
class Members:
"""Build exactly these members and skip config discovery.
A mapping keys each member's installed identity to the tree that supplies it."""
dirs: Sequence[Path] | Mapping[Path, Path]
@dataclass(frozen=True)
class Candidates:
"""Discover the enabled members and add these candidate directories."""
dirs: Sequence[Path]
@dataclass(frozen=True)
class Selection:
"""Publish a proposed plugin enablement with the members it implies."""
data: Mapping
@dataclass(frozen=True)
class StagedUpdate:
"""Publish a staged plugin tree over its install."""
data: Mapping
PluginInput = Members | Candidates | Selection | StagedUpdate
def encode(plugins: PluginInput | None) -> dict | None:
"""JSON form for the worker request; paths become absolute strings."""
if plugins is None:
return None
if isinstance(plugins, Members):
if isinstance(plugins.dirs, Mapping):
return {"kind": "members", "sources": [[str(Path(key).absolute()), str(Path(source).absolute())]
for key, source in plugins.dirs.items()]}
return {"kind": "members", "paths": [str(Path(path).absolute()) for path in plugins.dirs]}
if isinstance(plugins, Candidates):
return {"kind": "candidates", "paths": [str(Path(path).absolute()) for path in plugins.dirs]}
kind = "selection" if isinstance(plugins, Selection) else "staged"
return {"kind": kind, "data": dict(plugins.data)}
def decode(value: dict | None) -> PluginInput | None:
if value is None:
return None
kind = value["kind"]
if kind == "members":
if "sources" in value:
return Members({Path(identity): Path(source) for identity, source in value["sources"]})
return Members([Path(path) for path in value["paths"]])
if kind == "candidates":
return Candidates([Path(path) for path in value["paths"]])
if kind == "selection":
return Selection(value["data"])
if kind == "staged":
return StagedUpdate(value["data"])
raise ValueError(f"unknown plugin input kind: {kind!r}")