@@ -65,6 +65,11 @@ def __init__(
6565 ) -> None :
6666 """Initialize a plugin with a configuration and context.
6767
68+ The plugin receives the config directly. When the plugin is
69+ registered with the Manager, the PluginRef retains the
70+ authoritative config and gives the plugin a defensive copy,
71+ so the Manager never trusts config read back from the plugin.
72+
6873 Args:
6974 config: The plugin configuration
7075 hook_payloads: optional mapping of hookpoints to payloads for the plugin.
@@ -267,11 +272,18 @@ class PluginRef:
267272 ['ref', 'test']
268273 """
269274
270- def __init__ (self , plugin : Plugin ):
275+ def __init__ (self , plugin : Plugin , trusted_config : PluginConfig | None = None ):
271276 """Initialize a plugin reference.
272277
278+ Stores the authoritative config separately from the plugin.
279+ The Manager reads policy-sensitive fields (capabilities, mode,
280+ on_error) from the trusted config, never from the plugin.
281+
273282 Args:
274283 plugin: The plugin to reference.
284+ trusted_config: The authoritative config retained by the
285+ Manager. If not provided, falls back to plugin.config
286+ (for backward compatibility in tests).
275287
276288 Examples:
277289 >>> from cpex.framework import PluginConfig
@@ -293,6 +305,7 @@ def __init__(self, plugin: Plugin):
293305 True
294306 """
295307 self ._plugin = plugin
308+ self ._trusted_config = trusted_config or plugin .config
296309 self ._uuid = uuid .uuid4 ()
297310
298311 @property
@@ -304,6 +317,15 @@ def plugin(self) -> Plugin:
304317 """
305318 return self ._plugin
306319
320+ @property
321+ def trusted_config (self ) -> PluginConfig :
322+ """Return the authoritative config held by the Manager.
323+
324+ Returns:
325+ The trusted PluginConfig (not the plugin's copy).
326+ """
327+ return self ._trusted_config
328+
307329 @property
308330 def uuid (self ) -> str :
309331 """Return the plugin's UUID.
@@ -320,7 +342,7 @@ def priority(self) -> int:
320342 Returns:
321343 Plugin's priority.
322344 """
323- return self ._plugin .priority
345+ return self ._trusted_config .priority
324346
325347 @property
326348 def name (self ) -> str :
@@ -329,7 +351,7 @@ def name(self) -> str:
329351 Returns:
330352 Plugin's name.
331353 """
332- return self ._plugin .name
354+ return self ._trusted_config .name
333355
334356 @property
335357 def hooks (self ) -> list [str ]:
@@ -338,7 +360,7 @@ def hooks(self) -> list[str]:
338360 Returns:
339361 Plugin's configured hooks.
340362 """
341- return self ._plugin .hooks
363+ return self ._trusted_config .hooks
342364
343365 @property
344366 def tags (self ) -> list [str ]:
@@ -347,7 +369,7 @@ def tags(self) -> list[str]:
347369 Returns:
348370 Plugin's tags.
349371 """
350- return self ._plugin .tags
372+ return self ._trusted_config .tags
351373
352374 @property
353375 def conditions (self ) -> list [PluginCondition ] | None :
@@ -356,7 +378,7 @@ def conditions(self) -> list[PluginCondition] | None:
356378 Returns:
357379 Plugin's conditions for operation.
358380 """
359- return self ._plugin .conditions
381+ return self ._trusted_config .conditions
360382
361383 @property
362384 def mode (self ) -> PluginMode :
@@ -365,7 +387,7 @@ def mode(self) -> PluginMode:
365387 Returns:
366388 Plugin's mode.
367389 """
368- return self .plugin .mode
390+ return self ._trusted_config .mode
369391
370392 @property
371393 def on_error (self ) -> OnError :
@@ -374,7 +396,16 @@ def on_error(self) -> OnError:
374396 Returns:
375397 Plugin's on_error behavior.
376398 """
377- return self .plugin .config .on_error
399+ return self ._trusted_config .on_error
400+
401+ @property
402+ def capabilities (self ) -> frozenset [str ]:
403+ """Return the plugin's declared capabilities.
404+
405+ Returns:
406+ The authoritative capability set from the trusted config.
407+ """
408+ return self ._trusted_config .capabilities
378409
379410
380411class HookRef :
@@ -424,7 +455,7 @@ def __init__(self, hook: str, plugin_ref: PluginRef):
424455
425456 # Check for @hook decorator metadata
426457 metadata = get_hook_metadata (method )
427- if metadata and metadata .hook_type == hook :
458+ if metadata and metadata .matches ( hook ) :
428459 self ._func = method
429460 break
430461
@@ -439,20 +470,26 @@ def __init__(self, hook: str, plugin_ref: PluginRef):
439470 )
440471
441472 # Validate hook method signature (parameter count and async)
442- self ._validate_hook_signature (hook , self ._func , plugin_ref .plugin .name )
473+ param_count = self ._validate_hook_signature (hook , self ._func , plugin_ref .plugin .name )
474+
475+ # Store whether the plugin accepts extensions as a third argument
476+ self ._accepts_extensions = param_count == 3
443477
444- def _validate_hook_signature (self , hook : str , func : Callable , plugin_name : str ) -> None :
478+ def _validate_hook_signature (self , hook : str , func : Callable , plugin_name : str ) -> int :
445479 """Validate that the hook method has the correct signature.
446480
447481 Checks:
448- 1. Method accepts correct number of parameters (self, payload, context)
482+ 1. Method accepts 2 parameters (payload, context) or 3 ( payload, context, extensions )
449483 2. Method is async (returns coroutine)
450484
451485 Args:
452486 hook: The hook type being validated
453487 func: The hook method to validate
454488 plugin_name: Name of the plugin (for error messages)
455489
490+ Returns:
491+ The number of parameters (2 or 3).
492+
456493 Raises:
457494 PluginError: If the signature is invalid
458495 """
@@ -462,14 +499,16 @@ def _validate_hook_signature(self, hook: str, func: Callable, plugin_name: str)
462499 sig = inspect .signature (func )
463500 params = list (sig .parameters .values ())
464501
465- # Check parameter count (should be: payload, context)
502+ # Check parameter count (should be: payload, context[, extensions] )
466503 # Note: 'self' is not included in bound method signatures
467- if len (params ) != 2 :
504+ if len (params ) not in ( 2 , 3 ) :
468505 raise PluginError (
469506 error = PluginErrorModel (
470507 message = f"Plugin '{ plugin_name } ' hook '{ hook } ' has invalid signature. "
471- f"Expected 2 parameters (payload, context), got { len (params )} : { list (sig .parameters .keys ())} . "
472- f"Correct signature: async def { hook } (self, payload: PayloadType, context: PluginContext) -> ResultType" ,
508+ f"Expected 2 or 3 parameters (payload, context[, extensions]), "
509+ f"got { len (params )} : { list (sig .parameters .keys ())} . "
510+ f"Correct signature: async def { hook } (self, payload: PayloadType, "
511+ f"context: PluginContext[, extensions: Extensions]) -> ResultType" ,
473512 plugin_name = plugin_name ,
474513 )
475514 )
@@ -485,13 +524,7 @@ def _validate_hook_signature(self, hook: str, func: Callable, plugin_name: str)
485524 )
486525 )
487526
488- # ========== OPTIONAL: Type Hint Validation ==========
489- # Uncomment to enable strict type checking of payload and return types.
490- # This validates that type hints match the expected types from the hook registry.
491- # Pros: Catches type errors at plugin load time instead of runtime
492- # Cons: Requires all plugins to have type hints, adds validation overhead
493- #
494- # self._validate_type_hints(hook, func, params, plugin_name)
527+ return len (params )
495528
496529 def _validate_type_hints (self , hook : str , func : Callable , params : list , plugin_name : str ) -> None :
497530 """Validate that type hints match expected payload and result types.
@@ -608,7 +641,16 @@ def name(self) -> str:
608641 return self ._hook
609642
610643 @property
611- def hook (self ) -> Callable [[PluginPayload , PluginContext ], Awaitable [PluginResult ]] | None :
644+ def accepts_extensions (self ) -> bool :
645+ """Whether the hook method accepts extensions as a third argument.
646+
647+ Returns:
648+ True if the hook signature has 3 parameters (payload, context, extensions).
649+ """
650+ return self ._accepts_extensions
651+
652+ @property
653+ def hook (self ) -> Callable [..., Awaitable [PluginResult ]] | None :
612654 """The hooking function that can be invoked within the reference.
613655
614656 Returns:
0 commit comments