Skip to content

Commit db35e14

Browse files
committed
Merge branch 'main' into plugin-repo-1
2 parents 3c475d5 + a42ef02 commit db35e14

59 files changed

Lines changed: 11988 additions & 153 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

cpex/framework/base.py

Lines changed: 66 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -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

380411
class 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:

cpex/framework/cmf/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# -*- coding: utf-8 -*-
2+
"""Location: ./cpex/framework/cmf/__init__.py
3+
Copyright 2025
4+
SPDX-License-Identifier: Apache-2.0
5+
Authors: Teryl Taylor
6+
7+
Common Message Format (CMF) Package.
8+
Provides the canonical, provider-agnostic message representation
9+
for interactions between users, agents, tools, and language models.
10+
"""

0 commit comments

Comments
 (0)