refactor(cli/plugins): unify manifest.key-or-name into manifest_key()/PluginContext.plugin_id (33 sites)

This commit is contained in:
Teknium
2026-09-02 15:44:28 -07:00
parent f0cfbb33e1
commit c7d7e669c9
4 changed files with 45 additions and 47 deletions

View File

@@ -39,6 +39,7 @@ from hermes_cli.plugin_capabilities import ( # noqa: F401 — re-exported
)
from hermes_cli.relay_plugin_cutover import RELAY_PLUGINS_CONFIG_ENV, legacy_relay_plugin_keys
from hermes_cli.plugins_manifest import ( # noqa: F401 — re-exported
manifest_key,
parse_manifest_file,
portable_plugin_manifest,
_CONFIG_SCHEMA_TYPES,
@@ -350,7 +351,7 @@ class PluginContext:
@property
def plugin_id(self) -> str:
"""Return the effective registry id used for this plugin's namespaces."""
return self.manifest.key or self.manifest.name
return manifest_key(self.manifest)
def has_plugin(self, plugin_id: str) -> bool:
"""Return True when another plugin is loaded and enabled (runtime probe for advisory
@@ -536,8 +537,7 @@ class PluginContext:
``plugins.entries.<plugin_id>.llm.*``."""
if self._llm is None:
from agent.plugin_llm import PluginLlm
plugin_id = self.manifest.key or self.manifest.name
self._llm = PluginLlm(plugin_id=plugin_id)
self._llm = PluginLlm(plugin_id=self.plugin_id)
return self._llm
@property
@@ -604,7 +604,7 @@ class PluginContext:
self._manager.register_approval_transport(
name,
present_fn,
plugin_id=self.manifest.key or self.manifest.name,
plugin_id=self.plugin_id,
)
# Record ownership so unload/force-reload removes this transport. Duplicate names are
# rejected above (raise), so there is never a displaced previous entry to restore.
@@ -639,11 +639,10 @@ class PluginContext:
could silently replace a privileged built-in like ``write_file``.
"""
if override and not self._tool_override_allowed(name):
plugin_id = self.manifest.key or self.manifest.name
raise PluginToolOverrideError(
f"Plugin {self.manifest.name!r} cannot override built-in tool "
f"{name!r}. Set "
f"plugins.entries.{plugin_id}.allow_tool_override: true "
f"plugins.entries.{self.plugin_id}.allow_tool_override: true "
f"in config.yaml to allow this plugin to replace built-in tools."
)
@@ -702,8 +701,7 @@ class PluginContext:
source = getattr(self.manifest, "source", "") or ""
if source == "bundled" and capability == "tools.override":
return True
plugin_id = self.manifest.key or self.manifest.name
return plugin_capability_granted(plugin_id, capability)
return plugin_capability_granted(self.plugin_id, capability)
# -- capability-gated MCP access ----------------------------------------
@@ -721,13 +719,12 @@ class PluginContext:
raise ``PermissionError``. ``timeout`` clamps to 1–600s. Returns ``{"ok": True, "result"}`` or
``{"ok": False, "error"}``; results over ~64KB are truncated with a marker.
"""
plugin_id = self.manifest.key or self.manifest.name
allowlist = self._mcp_allowlist(plugin_id)
allowlist = self._mcp_allowlist(self.plugin_id)
if server not in allowlist:
raise PermissionError(
f"Plugin {self.manifest.name!r} is not allowed to call MCP "
f"server {server!r}. Add it to "
f"plugins.entries.{plugin_id}.mcp_allowlist in config.yaml "
f"plugins.entries.{self.plugin_id}.mcp_allowlist in config.yaml "
f"to grant access (default is no MCP access)."
)
@@ -811,10 +808,9 @@ class PluginContext:
cfg = load_config() or {}
except Exception:
return False # fail closed: better to break the override than silently grant it
plugin_id = self.manifest.key or self.manifest.name
# Pass THIS manager's profile-scoped config so a multi-profile process never consults the
# active profile's consent state instead.
return plugin_capability_granted(plugin_id, "tools.override", config=cfg)
return plugin_capability_granted(self.plugin_id, "tools.override", config=cfg)
# -- message injection --------------------------------------------------
@@ -846,12 +842,11 @@ class PluginContext:
logger.warning("inject_message: gateway mode requires an existing session_key")
return False
if not self._gateway_injection_allowed():
plugin_id = self.manifest.key or self.manifest.name
logger.warning(
"inject_message: gateway injection denied for plugin %s; set "
"plugins.entries.%s.allow_gateway_injection: true to allow it",
plugin_id,
plugin_id,
self.plugin_id,
self.plugin_id,
)
return False
@@ -859,19 +854,18 @@ class PluginContext:
logger.warning("inject_message: no live gateway is available")
return False
plugin_id = self.manifest.key or self.manifest.name
try:
return bool(
self._manager.inject_gateway_message(
session_key=session_key,
content=msg,
plugin_id=plugin_id,
plugin_id=self.plugin_id,
)
)
except Exception:
logger.warning(
"inject_message: gateway scheduling failed for plugin %s",
plugin_id,
self.plugin_id,
exc_info=True,
)
return False
@@ -883,13 +877,12 @@ class PluginContext:
except Exception:
return False
plugin_id = self.manifest.key or self.manifest.name
return (
cfg_get(
cfg,
"plugins",
"entries",
plugin_id,
self.plugin_id,
"allow_gateway_injection",
default=False,
)
@@ -917,7 +910,7 @@ class PluginContext:
"setup_fn": setup_fn,
"handler_fn": handler_fn,
"plugin": self.manifest.name,
"plugin_key": self.manifest.key or self.manifest.name,
"plugin_key": self.plugin_id,
}
handle = self._track_mapping_entry(
"cli_command", name, self._manager._cli_commands, entry, previous
@@ -972,7 +965,7 @@ class PluginContext:
"handler": handler,
"description": description or "Plugin command",
"plugin": self.manifest.name,
"plugin_key": self.manifest.key or self.manifest.name,
"plugin_key": self.plugin_id,
"args_hint": hint,
"argument_mode": mode,
}
@@ -1297,7 +1290,7 @@ class PluginContext:
)
# Owner is the canonical id ``ctx.llm`` is bound to, so agent/plugin_llm.py can match it.
owner_id = self.manifest.key or self.manifest.name
owner_id = self.plugin_id
existing = self._manager._aux_tasks.get(key)
if existing is not None and existing.get("plugin") != owner_id:
@@ -1416,13 +1409,12 @@ class PluginContext:
f"system prompt section {id!r} is already registered by "
f"plugin {existing.plugin!r}"
)
plugin_id = self.manifest.key or self.manifest.name
section = PluginSystemPromptSection(
id=id,
content=content,
position=position,
max_chars=max_chars,
plugin=plugin_id,
plugin=self.plugin_id,
)
handle = self._track_mapping_entry(
"system_prompt_section", id, self._manager._system_prompt_sections, section, existing
@@ -1440,7 +1432,7 @@ class PluginContext:
raises ``ValueError``. Delivery is fire-and-forget via a single-worker queue: order
preserved, a blocking subscriber cannot stall the emitter.
"""
plugin_key = self.manifest.key or self.manifest.name
plugin_key = self.plugin_id
if not event or not isinstance(event, str):
logger.warning("Plugin '%s' tried to emit an invalid event name %r", plugin_key, event)
raise ValueError(f"Plugin '{plugin_key}' emit() requires a non-empty event name")
@@ -1470,7 +1462,7 @@ class PluginContext:
f"Plugin '{self.manifest.name}' subscribe() requires a "
f"non-empty event name"
)
plugin_key = self.manifest.key or self.manifest.name
plugin_key = self.plugin_id
self._manager._subscribe_event(plugin_key, event, callback)
logger.debug("Plugin %s subscribed to event: %s", self.manifest.name, event)
@@ -1517,7 +1509,7 @@ class PluginContext:
entry = {
"path": path,
"plugin": namespace,
"plugin_key": self.manifest.key or self.manifest.name,
"plugin_key": self.plugin_id,
"bare_name": name,
"description": description,
"frontmatter": dict(frontmatter or {}),
@@ -1737,7 +1729,7 @@ class PluginManager(PluginLoaderMixin, PluginDispatchMixin):
attribution but NOT in ``_registration_order``, so a routine unload cannot dispose them;
the handle still releases on explicit ``dispose()``.
"""
plugin_key = manifest.key or manifest.name
plugin_key = manifest_key(manifest)
registration = PluginRegistration(
kind=kind,
key=key,
@@ -1907,7 +1899,7 @@ class PluginManager(PluginLoaderMixin, PluginDispatchMixin):
@staticmethod
def _resolve_plugin_key(plugin: Union[str, PluginManifest, LoadedPlugin]) -> str:
if isinstance(plugin, LoadedPlugin):
return plugin.manifest.key or plugin.manifest.name
return manifest_key(plugin.manifest)
if isinstance(plugin, PluginManifest):
return plugin.key or plugin.name
return str(plugin)
@@ -2158,7 +2150,7 @@ class PluginManager(PluginLoaderMixin, PluginDispatchMixin):
)
# Later sources win on key collision (project > user > bundled); gate the winners, then
# load survivors in requires_plugins order (see resolve_plugin_load_order).
winners = {m.key or m.name: m for m in manifests}
winners = {manifest_key(m): m for m in manifests}
to_load = {
key: manifest for key, manifest in winners.items()
if self._gate_manifest(manifest, disabled, enabled)
@@ -2183,7 +2175,7 @@ class PluginManager(PluginLoaderMixin, PluginDispatchMixin):
loaded = LoadedPlugin(manifest=manifest, enabled=enabled)
if error is not None:
loaded.error = error
self._plugins[manifest.key or manifest.name] = loaded
self._plugins[manifest_key(manifest)] = loaded
def _gate_manifest(
self,
@@ -2265,11 +2257,11 @@ class PluginManager(PluginLoaderMixin, PluginDispatchMixin):
if not enabled:
return False
winners = {m.key or m.name: m for m in self._collect_directory_manifests()}
winners = {manifest_key(m): m for m in self._collect_directory_manifests()}
for manifest in winners.values():
if not manifest.portable:
continue
lookup_key = manifest.key or manifest.name
lookup_key = manifest_key(manifest)
if lookup_key in disabled or manifest.name in disabled:
continue
if lookup_key not in enabled and manifest.name not in enabled:
@@ -2328,7 +2320,7 @@ class PluginManager(PluginLoaderMixin, PluginDispatchMixin):
result.append(
{
"name": loaded.manifest.name,
"key": loaded.manifest.key or loaded.manifest.name,
"key": manifest_key(loaded.manifest),
"kind": loaded.manifest.kind,
"version": loaded.manifest.version,
"description": loaded.manifest.description,

View File

@@ -19,6 +19,7 @@ from hermes_cli.plugin_capabilities import parse_declared_capabilities as _parse
from hermes_cli.plugins_manifest import (
PluginManifest,
_detect_kind_from_source,
manifest_key,
_resolve_module_source,
parse_manifest_file,
portable_plugin_manifest,
@@ -228,7 +229,7 @@ def gate_manifest(
"""Decide how one winning manifest is handled. Gate order matters: legacy relay refusal, explicit
disable, category-owned kinds (exclusive / model-provider), bundled auto-loads (backend now,
platform deferred), then ``plugins.enabled`` opt-in (path-derived key or legacy bare name)."""
lookup_key = manifest.key or manifest.name
lookup_key = manifest_key(manifest)
name = manifest.name
# Relay lifecycle is core-owned; an old plugin copy would compete for its registries.
if lookup_key in LEGACY_RELAY_PLUGIN_KEYS or name in LEGACY_RELAY_PLUGIN_KEYS:

View File

@@ -24,7 +24,7 @@ from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional
from hermes_constants import get_hermes_home, reset_hermes_home_override, set_hermes_home_override
from registration_lifecycle import replacement_coordinator
from hermes_cli.plugins_discovery import ENTRY_POINTS_GROUP, _select_entry_point_group
from hermes_cli.plugins_manifest import PluginManifest, validate_config_schema
from hermes_cli.plugins_manifest import PluginManifest, manifest_key, validate_config_schema
if TYPE_CHECKING: # pragma: no cover
from hermes_cli.plugins import LoadedPlugin
@@ -86,7 +86,7 @@ class PluginLoaderMixin:
in ``hermes plugins list`` until then."""
from hermes_cli.plugins import LoadedPlugin
lookup_key = manifest.key or manifest.name
lookup_key = manifest_key(manifest)
platform_name = self._platform_name_from_manifest(manifest)
loaded = LoadedPlugin(manifest=manifest, enabled=True)
@@ -150,7 +150,7 @@ class PluginLoaderMixin:
if not manifest.provides_tools:
return
lookup_key = manifest.key or manifest.name
lookup_key = manifest_key(manifest)
plugin_dir = Path(manifest.path) if manifest.path else None
if plugin_dir is None or not (plugin_dir / "tools.py").is_file():
# Declared but undeliverable — staying quiet reproduces the very symptom this fixes.
@@ -225,7 +225,7 @@ class PluginLoaderMixin:
deps = manifest.python_dependencies
if not deps:
return
key = manifest.key or manifest.name
key = manifest_key(manifest)
missing: List[str] = []
for req in deps:
# Best-effort presence probe on the distribution name.
@@ -253,7 +253,7 @@ class PluginLoaderMixin:
"""Warn (never block) on plugins.entries.<id> settings that violate config_schema."""
if not manifest.config_schema:
return
plugin_id = manifest.key or manifest.name
plugin_id = manifest_key(manifest)
settings: Mapping[str, Any] = {}
try:
from hermes_cli.config import load_config
@@ -285,7 +285,7 @@ class PluginLoaderMixin:
loaded = LoadedPlugin(manifest=manifest)
logger.debug(
"Loading plugin '%s' (source=%s, kind=%s, path=%s)",
manifest.key or manifest.name, manifest.source, manifest.kind, manifest.path,
manifest_key(manifest), manifest.source, manifest.kind, manifest.path,
)
if manifest.portable:
@@ -293,7 +293,7 @@ class PluginLoaderMixin:
return
registration_start = len(self._registration_order)
plugin_key = manifest.key or manifest.name
plugin_key = manifest_key(manifest)
_module_name = self._policy_module_name(manifest)
self._track_tool_override_policy(manifest, _module_name)
try:
@@ -336,7 +336,7 @@ class PluginLoaderMixin:
# so discovery-time pre-registrations are gone too.
if not loaded.enabled:
self._predeclared_tools.pop(plugin_key, None)
self._plugins[manifest.key or manifest.name] = loaded
self._plugins[manifest_key(manifest)] = loaded
def _track_tool_override_policy(self, manifest: PluginManifest, module_name: str) -> None:
"""Install the plugin's tool-override policy in tools.registry as a ledger-owned lease."""
@@ -406,7 +406,7 @@ class PluginLoaderMixin:
"""Load validated portable components without importing Python code."""
from hermes_cli.plugins import PluginContext
lookup_key = manifest.key or manifest.name
lookup_key = manifest_key(manifest)
try:
from hermes_cli.agent_plugins import load_agent_plugin
@@ -455,7 +455,7 @@ class PluginLoaderMixin:
def _directory_module_name(self, manifest: PluginManifest) -> str:
"""Return a profile-safe import namespace for a directory plugin."""
key = manifest.key or manifest.name
key = manifest_key(manifest)
slug = key.replace("/", "__").replace("-", "_")
bare_name = f"{_NS_PARENT}.{slug}"
with _MODULE_NAMESPACE_LOCK:

View File

@@ -373,6 +373,11 @@ def _resolve_module_source(module_name: str, limit: int = 8192) -> str:
return _read_source_from_origin(resolve_module_origin(module_name), limit)
def manifest_key(manifest: "PluginManifest") -> str:
"""Registry id of a manifest: the path-derived ``key`` when set, else the bare ``name``."""
return manifest.key or manifest.name
@dataclass
class PluginManifest:
"""Parsed representation of a plugin.yaml manifest."""