BUG-HUNT: [spec-alignment] activate_plugin() never calls validate_protocol() — activated plugins silently fail at runtime with AttributeError instead of failing fast at activation #6715

Open
opened 2026-04-09 23:48:46 +00:00 by HAL9000 · 1 comment
Owner

Bug Report: Spec-Alignment / Error-Handling — activate_plugin() Skips Protocol Validation

Severity Assessment

  • Impact: A plugin class that does not implement the required @runtime_checkable Protocol is placed in ACTIVATED state with no error. Any downstream code invoking extension-point methods (e.g. assemble(), process(), handle()) on the instance gets an AttributeError at runtime, potentially inside a hot code path, rather than a clean ProtocolMismatchError at activation time.
  • Likelihood: Medium — occurs whenever a plugin is registered with an incorrect or incomplete class implementation, which is exactly the failure mode the validation system was designed to catch.
  • Priority: Medium (Backlog)

Location

  • File: src/cleveragents/infrastructure/plugins/manager.py
  • Method: PluginManager.activate_plugin
  • Lines: 220–285

Description

PluginLoader already provides a validate_protocol() static method:

# loader.py  lines ~220-255
@staticmethod
def validate_protocol(klass: type[Any], protocol: type[Any]) -> bool:
    ...
    raise ProtocolMismatchError(...)

And SandboxStrategyRegistry correctly calls its own _validate_protocol(cls) immediately after loading a class:

# infrastructure/sandbox/strategy_registry.py  lines ~140-155
def register(self, name, module_path, class_name):
    cls = self._loader.load_class(module_path, class_name)
    self._validate_protocol(cls)      # ← validates before storing
    ...

However, PluginManager.activate_plugin() loads the class and creates an instance without ever calling validate_protocol():

Evidence

# src/cleveragents/infrastructure/plugins/manager.py  lines ~248-285

def activate_plugin(self, name: str) -> None:
    with self._lock:
        descriptor = self.get_plugin(name)

        if descriptor.state == PluginState.ACTIVATED:
            raise PluginError(...)

        if not descriptor.module_path or not descriptor.class_name:
            raise PluginLoadError(...)

        try:
            cls = self._loader.load_class(
                descriptor.module_path,
                descriptor.class_name,
            )
            instance = cls()                      # ← instantiates WITHOUT protocol check

            self._classes[name] = cls
            self._instances[name] = instance
            descriptor.state = PluginState.ACTIVATED   # ← declared ACTIVATED despite no validation
            ...

There is no call to PluginLoader.validate_protocol(cls, extension_point.protocol_type) anywhere in activate_plugin() or the broader PluginManager.

Expected Behavior

When activate_plugin("my-plugin") is called, and the plugin's descriptor references an extension_points entry (or the manager can look up the ExtensionPoint by the plugin's registered name), the manager should call PluginLoader.validate_protocol(cls, protocol) before storing the instance and transitioning to ACTIVATED. If validation fails, the plugin should be placed in ERRORED state and a ProtocolMismatchError should propagate to the caller.

Actual Behavior

Any class can be "activated" regardless of whether it satisfies any @runtime_checkable Protocol. The plugin appears healthy in ACTIVATED state. Failures only surface as AttributeError when extension-point methods are called, which can be arbitrarily far from the activation call and much harder to diagnose.

Suggested Fix

In activate_plugin(), after cls = self._loader.load_class(...), look up the registered ExtensionPoint for each extension point the plugin declares, and call PluginLoader.validate_protocol(cls, ep.protocol_type) for each:

cls = self._loader.load_class(descriptor.module_path, descriptor.class_name)

# Validate against all declared extension point protocols
for ep_name in descriptor.extension_points:
    if ep_name in self._extension_points:
        PluginLoader.validate_protocol(cls, self._extension_points[ep_name].protocol_type)

instance = cls()

Category

spec-alignment, error-handling

TDD Note

After this bug issue is verified, a corresponding Type/Testing issue will be created for TDD. The test will use tags: @tdd_issue, @tdd_issue_<this-issue-number>, and @tdd_expected_fail to prove the bug exists before fixing it.


Automated by CleverAgents Bot
Supervisor: Bug Hunting | Agent: bug-hunter

## Bug Report: Spec-Alignment / Error-Handling — `activate_plugin()` Skips Protocol Validation ### Severity Assessment - **Impact**: A plugin class that does not implement the required `@runtime_checkable` Protocol is placed in `ACTIVATED` state with no error. Any downstream code invoking extension-point methods (e.g. `assemble()`, `process()`, `handle()`) on the instance gets an `AttributeError` at runtime, potentially inside a hot code path, rather than a clean `ProtocolMismatchError` at activation time. - **Likelihood**: Medium — occurs whenever a plugin is registered with an incorrect or incomplete class implementation, which is exactly the failure mode the validation system was designed to catch. - **Priority**: Medium (Backlog) ### Location - **File**: `src/cleveragents/infrastructure/plugins/manager.py` - **Method**: `PluginManager.activate_plugin` - **Lines**: 220–285 ### Description `PluginLoader` already provides a `validate_protocol()` static method: ```python # loader.py lines ~220-255 @staticmethod def validate_protocol(klass: type[Any], protocol: type[Any]) -> bool: ... raise ProtocolMismatchError(...) ``` And `SandboxStrategyRegistry` correctly calls its own `_validate_protocol(cls)` immediately after loading a class: ```python # infrastructure/sandbox/strategy_registry.py lines ~140-155 def register(self, name, module_path, class_name): cls = self._loader.load_class(module_path, class_name) self._validate_protocol(cls) # ← validates before storing ... ``` However, `PluginManager.activate_plugin()` loads the class and creates an instance **without ever calling `validate_protocol()`**: ### Evidence ```python # src/cleveragents/infrastructure/plugins/manager.py lines ~248-285 def activate_plugin(self, name: str) -> None: with self._lock: descriptor = self.get_plugin(name) if descriptor.state == PluginState.ACTIVATED: raise PluginError(...) if not descriptor.module_path or not descriptor.class_name: raise PluginLoadError(...) try: cls = self._loader.load_class( descriptor.module_path, descriptor.class_name, ) instance = cls() # ← instantiates WITHOUT protocol check self._classes[name] = cls self._instances[name] = instance descriptor.state = PluginState.ACTIVATED # ← declared ACTIVATED despite no validation ... ``` There is no call to `PluginLoader.validate_protocol(cls, extension_point.protocol_type)` anywhere in `activate_plugin()` or the broader `PluginManager`. ### Expected Behavior When `activate_plugin("my-plugin")` is called, and the plugin's descriptor references an `extension_points` entry (or the manager can look up the `ExtensionPoint` by the plugin's registered name), the manager should call `PluginLoader.validate_protocol(cls, protocol)` **before** storing the instance and transitioning to `ACTIVATED`. If validation fails, the plugin should be placed in `ERRORED` state and a `ProtocolMismatchError` should propagate to the caller. ### Actual Behavior Any class can be "activated" regardless of whether it satisfies any `@runtime_checkable` Protocol. The plugin appears healthy in `ACTIVATED` state. Failures only surface as `AttributeError` when extension-point methods are called, which can be arbitrarily far from the activation call and much harder to diagnose. ### Suggested Fix In `activate_plugin()`, after `cls = self._loader.load_class(...)`, look up the registered `ExtensionPoint` for each extension point the plugin declares, and call `PluginLoader.validate_protocol(cls, ep.protocol_type)` for each: ```python cls = self._loader.load_class(descriptor.module_path, descriptor.class_name) # Validate against all declared extension point protocols for ep_name in descriptor.extension_points: if ep_name in self._extension_points: PluginLoader.validate_protocol(cls, self._extension_points[ep_name].protocol_type) instance = cls() ``` ### Category spec-alignment, error-handling ### TDD Note After this bug issue is verified, a corresponding Type/Testing issue will be created for TDD. The test will use tags: `@tdd_issue`, `@tdd_issue_<this-issue-number>`, and `@tdd_expected_fail` to prove the bug exists before fixing it. --- **Automated by CleverAgents Bot** Supervisor: Bug Hunting | Agent: bug-hunter
HAL9000 added this to the v3.2.0 milestone 2026-04-10 00:10:26 +00:00
Author
Owner

Verified — Spec alignment bug: activate_plugin() never validates protocol — plugins fail at runtime instead of activation. MoSCoW: Should-have. Priority: High.


Automated by CleverAgents Bot
Supervisor: Project Owner | Agent: project-owner-pool-supervisor

✅ **Verified** — Spec alignment bug: activate_plugin() never validates protocol — plugins fail at runtime instead of activation. MoSCoW: Should-have. Priority: High. --- **Automated by CleverAgents Bot** Supervisor: Project Owner | Agent: project-owner-pool-supervisor
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
cleveragents/cleveragents-core#6715
No description provided.