@@ -49,11 +49,13 @@ def register(ctx):
4949
5050from __future__ import annotations
5151
52+ import copy
5253import functools
5354import importlib
5455import inspect
5556import json
5657import logging
58+ import os
5759import re
5860import sys
5961import threading
@@ -82,6 +84,8 @@ def register(ctx):
8284 "MiddlewareKind" ,
8385 "PluginSkill" ,
8486 "RegistrationSummary" ,
87+ "load_plugin_config" ,
88+ "configure_stderr_logging" ,
8589 "register_all" ,
8690 "build_schema" ,
8791 "tool_name" ,
@@ -150,6 +154,83 @@ class MiddlewareKind(str, Enum):
150154 LLM_EXECUTION = "llm_execution"
151155
152156
157+ def load_plugin_config (
158+ ctx : Any ,
159+ plugin_name : str ,
160+ * ,
161+ config_loader : Callable [[], dict [str , Any ]] | None = None ,
162+ ) -> dict [str , Any ]:
163+ """Return one plugin's effective Hermes config without mutating host state.
164+
165+ Current Hermes ``PluginManifest`` objects do not carry runtime profile
166+ configuration. ``manifest.config`` remains a compatibility seam for tests
167+ and older hosts; otherwise this reads ``plugins.<plugin_name>`` through
168+ Hermes' read-only effective config loader. A deep copy prevents plugin code
169+ from mutating the host config cache through nested mappings or lists.
170+ """
171+ clean_name = str (plugin_name or "" ).strip ()
172+ if not clean_name :
173+ raise ValueError ("plugin_name must be a non-empty string" )
174+ manifest = getattr (ctx , "manifest" , None )
175+ manifest_config = getattr (manifest , "config" , None )
176+ if isinstance (manifest_config , dict ) and manifest_config :
177+ return copy .deepcopy (manifest_config )
178+ if config_loader is None :
179+ try :
180+ from hermes_cli .config import load_config_readonly
181+ except (ImportError , AttributeError ):
182+ return {}
183+ config_loader = load_config_readonly
184+ try :
185+ effective = config_loader ()
186+ except Exception as exc :
187+ logging .getLogger (__name__ ).warning (
188+ "hermes_plugin_kit: effective config read failed for %s: %s" ,
189+ clean_name ,
190+ exc ,
191+ )
192+ return {}
193+ plugins = effective .get ("plugins" ) if isinstance (effective , dict ) else None
194+ plugin_config = plugins .get (clean_name ) if isinstance (plugins , dict ) else None
195+ return copy .deepcopy (plugin_config ) if isinstance (plugin_config , dict ) else {}
196+
197+
198+ def configure_stderr_logging (
199+ logger : logging .Logger ,
200+ * ,
201+ env_var : str ,
202+ default : bool = False ,
203+ ) -> logging .Handler | None :
204+ """Enable one idempotent INFO stderr handler from an operator env flag."""
205+ if not isinstance (logger , logging .Logger ):
206+ raise TypeError ("logger must be a logging.Logger" )
207+ clean_env_var = str (env_var or "" ).strip ()
208+ if not clean_env_var :
209+ raise ValueError ("env_var must be a non-empty string" )
210+ raw = os .environ .get (clean_env_var )
211+ enabled = default if raw is None else raw .strip ().lower () in {
212+ "1" ,
213+ "true" ,
214+ "yes" ,
215+ "on" ,
216+ }
217+ if not enabled :
218+ return None
219+ for handler in logger .handlers :
220+ if getattr (handler , "_hpk_stderr_env_var" , None ) == clean_env_var :
221+ return handler
222+ handler = logging .StreamHandler (sys .stderr )
223+ handler .setLevel (logging .INFO )
224+ handler .setFormatter (
225+ logging .Formatter ("%(asctime)s %(levelname)s %(name)s %(message)s" )
226+ )
227+ handler ._hpk_stderr_env_var = clean_env_var # type: ignore[attr-defined]
228+ logger .addHandler (handler )
229+ if logger .level == logging .NOTSET or logger .level > logging .INFO :
230+ logger .setLevel (logging .INFO )
231+ return handler
232+
233+
153234class MediaType (str , Enum ):
154235 """Hermes-agent ``send_message`` media directive modes."""
155236
0 commit comments