@@ -95,7 +95,9 @@ def register(ctx):
9595 "CommandType" ,
9696 "MiddlewareKind" ,
9797 "PluginSkill" ,
98+ "CapabilitySelection" ,
9899 "RegistrationSummary" ,
100+ "resolve_capability_selection" ,
99101 "load_plugin_config" ,
100102 "configure_stderr_logging" ,
101103 "register_all" ,
@@ -322,6 +324,14 @@ class PluginSkill:
322324 optional : bool = False
323325
324326
327+ @dataclass (frozen = True )
328+ class CapabilitySelection :
329+ """Validated capability names and their atomically expanded surfaces."""
330+
331+ capabilities : tuple [str , ...] = ()
332+ names : tuple [str , ...] = ()
333+
334+
325335@dataclass (frozen = True )
326336class RegistrationSummary :
327337 """Inventory of lifecycle surfaces registered by :func:`register_plugin`."""
@@ -336,6 +346,7 @@ class RegistrationSummary:
336346 image_gen_providers : tuple [str , ...] = ()
337347 video_gen_providers : tuple [str , ...] = ()
338348 cli_commands : tuple [str , ...] = ()
349+ capabilities : tuple [str , ...] = ()
339350
340351
341352class CommandType (str , Enum ):
@@ -361,7 +372,7 @@ def log_registration_summary(
361372 "commands=%s; cli_commands=%s; tools=%s; middlewares=%s; hooks=%s; "
362373 "skills=%s; skipped_optional_skills=%s; memory_providers=%s; "
363374 "image_gen_providers=%s; "
364- "video_gen_providers=%s" ,
375+ "video_gen_providers=%s; capabilities=%s " ,
365376 clean_plugin_name ,
366377 "," .join (summary .commands ) or "<none>" ,
367378 "," .join (summary .cli_commands ) or "<none>" ,
@@ -373,6 +384,7 @@ def log_registration_summary(
373384 "," .join (summary .memory_providers ) or "<none>" ,
374385 "," .join (summary .image_gen_providers ) or "<none>" ,
375386 "," .join (summary .video_gen_providers ) or "<none>" ,
387+ "," .join (summary .capabilities ) or "<none>" ,
376388 )
377389
378390
@@ -1858,6 +1870,77 @@ def wrapper(args: dict, **kwargs: Any) -> str:
18581870# Registration
18591871# ---------------------------------------------------------------------------
18601872
1873+ def _normalized_names (values : Iterable [str ], * , label : str ) -> frozenset [str ]:
1874+ names : set [str ] = set ()
1875+ for value in values :
1876+ if not isinstance (value , str ) or not value .strip ():
1877+ raise ValueError (f"{ label } must contain only non-empty strings" )
1878+ names .add (value .strip ())
1879+ return frozenset (names )
1880+
1881+
1882+ def resolve_capability_selection (
1883+ available_names : Iterable [str ],
1884+ * ,
1885+ capability_groups : Mapping [str , Iterable [str ]],
1886+ enabled_capabilities : Iterable [str ] | None = None ,
1887+ enabled_names : Iterable [str ] | None = None ,
1888+ ) -> CapabilitySelection :
1889+ """Validate and atomically expand capabilities into registered names.
1890+
1891+ Capability and explicit-name selection are mutually exclusive so a
1892+ deployment cannot accidentally mix an atomic contract with a partial
1893+ override. When neither is supplied, all available names are selected.
1894+ """
1895+ available = _normalized_names (available_names , label = "available_names" )
1896+ if enabled_capabilities is not None and enabled_names is not None :
1897+ raise ValueError (
1898+ "enabled_capabilities cannot be combined with enabled_names"
1899+ )
1900+
1901+ normalized_groups : dict [str , frozenset [str ]] = {}
1902+ for raw_capability , raw_members in capability_groups .items ():
1903+ if not isinstance (raw_capability , str ) or not raw_capability .strip ():
1904+ raise ValueError ("capability names must be non-empty strings" )
1905+ capability = raw_capability .strip ()
1906+ members = _normalized_names (
1907+ raw_members , label = f"capability { capability !r} members"
1908+ )
1909+ unknown_members = sorted (members - available )
1910+ if unknown_members :
1911+ raise ValueError (
1912+ f"capability { capability !r} references unknown names: "
1913+ + ", " .join (unknown_members )
1914+ )
1915+ normalized_groups [capability ] = members
1916+
1917+ if enabled_capabilities is not None :
1918+ capabilities = _normalized_names (
1919+ enabled_capabilities , label = "enabled_capabilities"
1920+ )
1921+ unknown_capabilities = sorted (capabilities - normalized_groups .keys ())
1922+ if unknown_capabilities :
1923+ raise ValueError (
1924+ "unknown capabilities: " + ", " .join (unknown_capabilities )
1925+ )
1926+ selected = frozenset ().union (
1927+ * (normalized_groups [name ] for name in capabilities )
1928+ )
1929+ return CapabilitySelection (
1930+ capabilities = tuple (sorted (capabilities )),
1931+ names = tuple (sorted (selected )),
1932+ )
1933+
1934+ if enabled_names is not None :
1935+ selected = _normalized_names (enabled_names , label = "enabled_names" )
1936+ unknown_names = sorted (selected - available )
1937+ if unknown_names :
1938+ raise ValueError ("unknown names: " + ", " .join (unknown_names ))
1939+ return CapabilitySelection (names = tuple (sorted (selected )))
1940+
1941+ return CapabilitySelection (names = tuple (sorted (available )))
1942+
1943+
18611944def register_all (ctx : Any , module : Any ) -> int :
18621945 """Register every ``@tool`` defined in *module* with *ctx*.
18631946
@@ -1897,26 +1980,25 @@ def _register_tool(ctx: Any, handler: Callable, spec: dict[str, Any]) -> None:
18971980 )
18981981
18991982
1900- def _register_generation_providers (
1983+ def _validate_generation_providers (
19011984 ctx : Any ,
19021985 providers : Iterable [Any ],
19031986 * ,
19041987 kind : str ,
19051988 registrar_name : str ,
1906- ) -> list [ str ]:
1989+ ) -> tuple [ Callable , tuple [ tuple [ str , Any ], ...] ]:
19071990 registrar = getattr (ctx , registrar_name , None )
19081991 if not callable (registrar ):
19091992 raise RuntimeError (f"this Hermes plugin context does not support { kind } providers" )
1910- registered : list [str ] = []
1993+ validated : list [tuple [ str , Any ] ] = []
19111994 for provider in providers :
19121995 name = getattr (provider , "name" , None )
19131996 if not isinstance (name , str ) or not name .strip ():
19141997 raise ValueError (f"{ kind } providers require a non-empty name" )
19151998 if not callable (getattr (provider , "generate" , None )):
19161999 raise ValueError (f"{ kind } provider { name !r} requires generate()" )
1917- registrar (provider )
1918- registered .append (name )
1919- return registered
2000+ validated .append ((name , provider ))
2001+ return registrar , tuple (validated )
19202002
19212003
19222004def register_plugin (
@@ -1927,6 +2009,7 @@ def register_plugin(
19272009 memory_providers : tuple [Any , ...] | list [Any ] = (),
19282010 image_gen_providers : tuple [Any , ...] | list [Any ] = (),
19292011 video_gen_providers : tuple [Any , ...] | list [Any ] = (),
2012+ capabilities : tuple [str , ...] | list [str ] = (),
19302013 plugin_name : str | None = None ,
19312014 logger : logging .Logger | None = None ,
19322015) -> RegistrationSummary :
@@ -1989,6 +2072,9 @@ def register_plugin(
19892072 if not isinstance (resolved_plugin_name , str ) or not resolved_plugin_name .strip ():
19902073 raise ValueError ("plugin_name must be a non-empty string" )
19912074 resolved_plugin_name = resolved_plugin_name .strip ()
2075+ resolved_capabilities = tuple (
2076+ sorted (_normalized_names (capabilities , label = "capabilities" ))
2077+ )
19922078
19932079 slash_commands : dict [str , Callable ] = {}
19942080 cli_commands : dict [str , Callable ] = {}
@@ -2067,6 +2153,54 @@ def register_plugin(
20672153 )
20682154 skipped_skills .append (name )
20692155
2156+ required_registrars = {
2157+ "register_command" : slash_commands ,
2158+ "register_cli_command" : cli_commands ,
2159+ "register_tool" : tools ,
2160+ "register_middleware" : middlewares ,
2161+ "register_hook" : hooks ,
2162+ "register_skill" : available_skills ,
2163+ }
2164+ for registrar_name , surfaces in required_registrars .items ():
2165+ if surfaces and not callable (getattr (ctx , registrar_name , None )):
2166+ raise RuntimeError (
2167+ f"this Hermes plugin context does not support { registrar_name } ()"
2168+ )
2169+
2170+ memory_registrar = getattr (ctx , "register_memory_provider" , None )
2171+ validated_memory_providers : list [tuple [str , Any ]] = []
2172+ if memory_providers and not callable (memory_registrar ):
2173+ raise RuntimeError (
2174+ "this Hermes plugin context does not support memory providers; "
2175+ "use the memory-provider discovery path"
2176+ )
2177+ for provider in memory_providers :
2178+ name = getattr (provider , "name" , None )
2179+ if not isinstance (name , str ) or not name .strip ():
2180+ raise ValueError ("memory providers require a non-empty name" )
2181+ validated_memory_providers .append ((name , provider ))
2182+
2183+ image_registrar , validated_image_providers = (
2184+ _validate_generation_providers (
2185+ ctx ,
2186+ image_gen_providers ,
2187+ kind = "image generation" ,
2188+ registrar_name = "register_image_gen_provider" ,
2189+ )
2190+ if image_gen_providers
2191+ else (None , ())
2192+ )
2193+ video_registrar , validated_video_providers = (
2194+ _validate_generation_providers (
2195+ ctx ,
2196+ video_gen_providers ,
2197+ kind = "video generation" ,
2198+ registrar_name = "register_video_gen_provider" ,
2199+ )
2200+ if video_gen_providers
2201+ else (None , ())
2202+ )
2203+
20702204 registered_slash_commands : list [str ] = []
20712205 for name in sorted (slash_commands ):
20722206 obj = slash_commands [name ]
@@ -2119,31 +2253,18 @@ def register_plugin(
21192253 registered_skills .append (skill .name )
21202254
21212255 registered_memory_providers : list [str ] = []
2122- register_memory_provider = getattr (ctx , "register_memory_provider" , None )
2123- if memory_providers and not callable (register_memory_provider ):
2124- raise RuntimeError (
2125- "this Hermes plugin context does not support memory providers; "
2126- "use the memory-provider discovery path"
2127- )
2128- for provider in memory_providers :
2129- name = getattr (provider , "name" , None )
2130- if not isinstance (name , str ) or not name .strip ():
2131- raise ValueError ("memory providers require a non-empty name" )
2132- register_memory_provider (provider )
2256+ for name , provider in validated_memory_providers :
2257+ memory_registrar (provider )
21332258 registered_memory_providers .append (name )
21342259
2135- registered_image_gen_providers = _register_generation_providers (
2136- ctx ,
2137- image_gen_providers ,
2138- kind = "image generation" ,
2139- registrar_name = "register_image_gen_provider" ,
2140- ) if image_gen_providers else []
2141- registered_video_gen_providers = _register_generation_providers (
2142- ctx ,
2143- video_gen_providers ,
2144- kind = "video generation" ,
2145- registrar_name = "register_video_gen_provider" ,
2146- ) if video_gen_providers else []
2260+ registered_image_gen_providers : list [str ] = []
2261+ for name , provider in validated_image_providers :
2262+ image_registrar (provider )
2263+ registered_image_gen_providers .append (name )
2264+ registered_video_gen_providers : list [str ] = []
2265+ for name , provider in validated_video_providers :
2266+ video_registrar (provider )
2267+ registered_video_gen_providers .append (name )
21472268
21482269 summary = RegistrationSummary (
21492270 commands = tuple (registered_slash_commands ),
@@ -2156,6 +2277,7 @@ def register_plugin(
21562277 memory_providers = tuple (registered_memory_providers ),
21572278 image_gen_providers = tuple (registered_image_gen_providers ),
21582279 video_gen_providers = tuple (registered_video_gen_providers ),
2280+ capabilities = resolved_capabilities ,
21592281 )
21602282 log_registration_summary (log , resolved_plugin_name , summary )
21612283 return summary
0 commit comments