Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 66 additions & 16 deletions cpex/framework/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ def __init__(
) -> None:
"""Initialize a plugin with a configuration and context.

The plugin receives the config directly. When the plugin is
registered with the Manager, the PluginRef retains the
authoritative config and gives the plugin a defensive copy,
so the Manager never trusts config read back from the plugin.

Args:
config: The plugin configuration
hook_payloads: optional mapping of hookpoints to payloads for the plugin.
Expand Down Expand Up @@ -267,11 +272,18 @@ class PluginRef:
['ref', 'test']
"""

def __init__(self, plugin: Plugin):
def __init__(self, plugin: Plugin, trusted_config: PluginConfig | None = None):
"""Initialize a plugin reference.

Stores the authoritative config separately from the plugin.
The Manager reads policy-sensitive fields (capabilities, mode,
on_error) from the trusted config, never from the plugin.

Args:
plugin: The plugin to reference.
trusted_config: The authoritative config retained by the
Manager. If not provided, falls back to plugin.config
(for backward compatibility in tests).

Examples:
>>> from cpex.framework import PluginConfig
Expand All @@ -293,6 +305,7 @@ def __init__(self, plugin: Plugin):
True
"""
self._plugin = plugin
self._trusted_config = trusted_config or plugin.config
self._uuid = uuid.uuid4()

@property
Expand All @@ -304,6 +317,15 @@ def plugin(self) -> Plugin:
"""
return self._plugin

@property
def trusted_config(self) -> PluginConfig:
"""Return the authoritative config held by the Manager.

Returns:
The trusted PluginConfig (not the plugin's copy).
"""
return self._trusted_config

@property
def uuid(self) -> str:
"""Return the plugin's UUID.
Expand All @@ -320,7 +342,7 @@ def priority(self) -> int:
Returns:
Plugin's priority.
"""
return self._plugin.priority
return self._trusted_config.priority

@property
def name(self) -> str:
Expand All @@ -329,7 +351,7 @@ def name(self) -> str:
Returns:
Plugin's name.
"""
return self._plugin.name
return self._trusted_config.name

@property
def hooks(self) -> list[str]:
Expand All @@ -338,7 +360,7 @@ def hooks(self) -> list[str]:
Returns:
Plugin's configured hooks.
"""
return self._plugin.hooks
return self._trusted_config.hooks

@property
def tags(self) -> list[str]:
Expand All @@ -347,7 +369,7 @@ def tags(self) -> list[str]:
Returns:
Plugin's tags.
"""
return self._plugin.tags
return self._trusted_config.tags

@property
def conditions(self) -> list[PluginCondition] | None:
Expand All @@ -356,7 +378,7 @@ def conditions(self) -> list[PluginCondition] | None:
Returns:
Plugin's conditions for operation.
"""
return self._plugin.conditions
return self._trusted_config.conditions

@property
def mode(self) -> PluginMode:
Expand All @@ -365,7 +387,7 @@ def mode(self) -> PluginMode:
Returns:
Plugin's mode.
"""
return self.plugin.mode
return self._trusted_config.mode

@property
def on_error(self) -> OnError:
Expand All @@ -374,7 +396,16 @@ def on_error(self) -> OnError:
Returns:
Plugin's on_error behavior.
"""
return self.plugin.config.on_error
return self._trusted_config.on_error

@property
def capabilities(self) -> frozenset[str]:
"""Return the plugin's declared capabilities.

Returns:
The authoritative capability set from the trusted config.
"""
return self._trusted_config.capabilities


class HookRef:
Expand Down Expand Up @@ -439,20 +470,26 @@ def __init__(self, hook: str, plugin_ref: PluginRef):
)

# Validate hook method signature (parameter count and async)
self._validate_hook_signature(hook, self._func, plugin_ref.plugin.name)
param_count = self._validate_hook_signature(hook, self._func, plugin_ref.plugin.name)

# Store whether the plugin accepts extensions as a third argument
self._accepts_extensions = param_count == 3

def _validate_hook_signature(self, hook: str, func: Callable, plugin_name: str) -> None:
def _validate_hook_signature(self, hook: str, func: Callable, plugin_name: str) -> int:
"""Validate that the hook method has the correct signature.

Checks:
1. Method accepts correct number of parameters (self, payload, context)
1. Method accepts 2 parameters (payload, context) or 3 (payload, context, extensions)
2. Method is async (returns coroutine)

Args:
hook: The hook type being validated
func: The hook method to validate
plugin_name: Name of the plugin (for error messages)

Returns:
The number of parameters (2 or 3).

Raises:
PluginError: If the signature is invalid
"""
Expand All @@ -462,14 +499,16 @@ def _validate_hook_signature(self, hook: str, func: Callable, plugin_name: str)
sig = inspect.signature(func)
params = list(sig.parameters.values())

# Check parameter count (should be: payload, context)
# Check parameter count (should be: payload, context[, extensions])
# Note: 'self' is not included in bound method signatures
if len(params) != 2:
if len(params) not in (2, 3):
raise PluginError(
error=PluginErrorModel(
message=f"Plugin '{plugin_name}' hook '{hook}' has invalid signature. "
f"Expected 2 parameters (payload, context), got {len(params)}: {list(sig.parameters.keys())}. "
f"Correct signature: async def {hook}(self, payload: PayloadType, context: PluginContext) -> ResultType",
f"Expected 2 or 3 parameters (payload, context[, extensions]), "
f"got {len(params)}: {list(sig.parameters.keys())}. "
f"Correct signature: async def {hook}(self, payload: PayloadType, "
f"context: PluginContext[, extensions: Extensions]) -> ResultType",
plugin_name=plugin_name,
)
)
Expand All @@ -485,6 +524,8 @@ def _validate_hook_signature(self, hook: str, func: Callable, plugin_name: str)
)
)

return len(params)

# ========== OPTIONAL: Type Hint Validation ==========
# Uncomment to enable strict type checking of payload and return types.
# This validates that type hints match the expected types from the hook registry.
Expand Down Expand Up @@ -608,7 +649,16 @@ def name(self) -> str:
return self._hook

@property
def hook(self) -> Callable[[PluginPayload, PluginContext], Awaitable[PluginResult]] | None:
def accepts_extensions(self) -> bool:
"""Whether the hook method accepts extensions as a third argument.

Returns:
True if the hook signature has 3 parameters (payload, context, extensions).
"""
return self._accepts_extensions

@property
def hook(self) -> Callable[..., Awaitable[PluginResult]] | None:
"""The hooking function that can be invoked within the reference.

Returns:
Expand Down
10 changes: 10 additions & 0 deletions cpex/framework/cmf/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
"""Location: ./cpex/framework/cmf/__init__.py
Copyright 2025
SPDX-License-Identifier: Apache-2.0
Authors: Teryl Taylor

Common Message Format (CMF) Package.
Provides the canonical, provider-agnostic message representation
for interactions between users, agents, tools, and language models.
"""
Loading
Loading