Files
HAL9000 cef70ff98a fix(security): add Robot integration test and CONTRIBUTORS for #7476
Add Robot Framework integration test verifying that load_from_entry_points
does not call ep.load() for entry points with disallowed module prefixes
(security regression test for issue #7476).

Also add HAL 9000 to CONTRIBUTORS.md per CONTRIBUTING.md process rules.

ISSUES CLOSED: #7476
2026-04-15 06:37:18 +00:00

393 lines
18 KiB
Gherkin

@extensibility @plugin_architecture
Feature: Plugin Architecture Framework with module:ClassName resolution
As a CleverAgents developer
I want a plugin architecture that loads custom implementations via
module:ClassName resolution and entry-point discovery
So that tools, strategies, backends, and pipeline components can be
extended without modifying core code
# ---------------------------------------------------------------------------
# PluginState enum
# ---------------------------------------------------------------------------
@plugin_state
Scenario: PluginState enum has all required values
Given the PluginState enum is available
Then it should have values "discovered", "activated", "executing", "deactivated", "errored"
@plugin_state
Scenario: PluginState values are strings
Given the PluginState enum is available
Then each PluginState value should be a string
# ---------------------------------------------------------------------------
# ExtensionPoint model
# ---------------------------------------------------------------------------
@extension_point
Scenario: ExtensionPoint creation with required fields
Given I create an ExtensionPoint with name "test-ep" and a protocol type
Then the ExtensionPoint name should be "test-ep"
And the ExtensionPoint should have a protocol_type
@extension_point
Scenario: ExtensionPoint is frozen
Given I create an ExtensionPoint with name "frozen-ep" and a protocol type
Then attempting to mutate the ExtensionPoint name should raise an error
@extension_point
Scenario: ExtensionPoint rejects empty name
When I attempt to create an ExtensionPoint with an empty name
Then a plugin validation error should be raised
# ---------------------------------------------------------------------------
# PluginDescriptor model
# ---------------------------------------------------------------------------
@plugin_descriptor
Scenario: PluginDescriptor creation with defaults
Given I create a PluginDescriptor with name "my-plugin"
Then the descriptor state should be "discovered"
And the descriptor version should be "0.0.0"
And the descriptor dependencies should be empty
@plugin_descriptor
Scenario: PluginDescriptor with full metadata
Given I create a PluginDescriptor with full metadata
Then the descriptor should have name "full-plugin"
And the descriptor should have version "1.2.3"
And the descriptor should have author "TestAuthor"
And the descriptor should have module_path "cleveragents.test"
And the descriptor should have class_name "TestClass"
@plugin_descriptor
Scenario: PluginDescriptor state is mutable
Given I create a PluginDescriptor with name "mutable-plugin"
When I set the descriptor state to "activated"
Then the descriptor state should be "activated"
@plugin_descriptor
Scenario: PluginDescriptor rejects empty name
When I attempt to create a PluginDescriptor with an empty name
Then a plugin validation error should be raised
# ---------------------------------------------------------------------------
# Plugin exceptions
# ---------------------------------------------------------------------------
@plugin_exceptions
Scenario: PluginError is base exception
Then PluginLoadError should be a subclass of PluginError
And PluginNotFoundError should be a subclass of PluginError
And ProtocolMismatchError should be a subclass of PluginError
@plugin_exceptions
Scenario: PluginLoadError carries message
When I raise a PluginLoadError with message "test error"
Then the exception message should contain "test error"
@plugin_exceptions
Scenario: PluginNotFoundError carries message
When I raise a PluginNotFoundError with message "not found"
Then the exception message should contain "not found"
@plugin_exceptions
Scenario: ProtocolMismatchError carries message
When I raise a ProtocolMismatchError with message "mismatch"
Then the exception message should contain "mismatch"
# ---------------------------------------------------------------------------
# PluginLoader — load_class success and failure
# ---------------------------------------------------------------------------
@plugin_loader @success
Scenario: PluginLoader loads a valid class
Given a PluginLoader with default prefixes
When I load class "InMemoryTextBackend" from module "cleveragents.domain.models.acms.stubs"
Then the loaded class should not be None
And the loaded class name should be "InMemoryTextBackend"
@plugin_loader @success
Scenario: PluginLoader loads another valid class
Given a PluginLoader with default prefixes
When I load class "InMemoryVectorBackend" from module "cleveragents.domain.models.acms.stubs"
Then the loaded class should not be None
@plugin_loader @failure
Scenario: PluginLoader rejects module outside allowed prefix
Given a PluginLoader with default prefixes
When I attempt to load class "Path" from module "pathlib"
Then a PluginLoadError should be raised
And the plugin error message should contain "not in the allowed prefix list"
@plugin_loader @failure
Scenario: PluginLoader fails on nonexistent module
Given a PluginLoader with default prefixes
When I attempt to load class "Foo" from module "cleveragents.nonexistent_module_xyz"
Then a PluginLoadError should be raised
And the plugin error message should contain "Cannot import module"
@plugin_loader @failure
Scenario: PluginLoader fails on nonexistent class
Given a PluginLoader with default prefixes
When I attempt to load class "NonExistentClassXyz" from module "cleveragents.domain.models.acms.stubs"
Then a PluginLoadError should be raised
And the plugin error message should contain "not found in module"
@plugin_loader @security
Scenario: PluginLoader with custom prefixes allows matching module
Given a PluginLoader with allowed prefixes "cleveragents.,mypackage."
When I load class "InMemoryTextBackend" from module "cleveragents.domain.models.acms.stubs"
Then the loaded class should not be None
@plugin_loader @security
Scenario: PluginLoader with empty prefix allowlist allows anything
Given a PluginLoader with empty prefix allowlist
When I load class "Path" from module "pathlib"
Then the loaded class should not be None
@plugin_loader
Scenario: PluginLoader allowed_prefixes property
Given a PluginLoader with default prefixes
Then the loader allowed_prefixes should contain "cleveragents."
@plugin_loader @failure
Scenario: PluginLoader rejects non-class attribute
Given a PluginLoader with default prefixes
When I attempt to load class "logger" from module "cleveragents.infrastructure.plugins.loader"
Then a PluginLoadError should be raised
And the plugin error message should contain "is not a class"
# ---------------------------------------------------------------------------
# Protocol validation
# ---------------------------------------------------------------------------
@protocol_validation @success
Scenario: validate_protocol passes for conforming class
Given a class that implements TextBackend protocol
When I validate it against the TextBackend protocol
Then the validation should return True
@protocol_validation @failure
Scenario: validate_protocol fails for non-conforming class
Given a class that does not implement any protocol
When I attempt to validate it against the TextBackend protocol
Then a ProtocolMismatchError should be raised
# ---------------------------------------------------------------------------
# Entry-point discovery
# ---------------------------------------------------------------------------
@entry_points
Scenario: load_from_entry_points returns empty for unknown group
Given a PluginLoader with default prefixes
When I discover plugins from entry point group "cleveragents.nonexistent_group_xyz"
Then the discovered plugin list should be empty
@entry_points @mock
Scenario: load_from_entry_points discovers mocked entry points
Given a PluginLoader with default prefixes
And a mocked entry point group "cleveragents.plugins" with entry "test-ep=cleveragents.domain.models.acms.stubs:InMemoryTextBackend"
When I discover plugins from the mocked entry point group
Then the discovered plugin list should have 1 entry
And the first descriptor name should be "test-ep"
@entry_points @security @tdd_issue @tdd_issue_7476
Scenario: Entry point with disallowed prefix is skipped without loading module
Given a PluginLoader with default prefixes
And a mocked entry point group "cleveragents.plugins" with raw entry "malicious=maliciouspkg.attack:Exploit"
When I discover plugins from the mocked entry point group
Then the discovered plugin list should be empty
And the mocked entry point load should not be called
And a security warning should be emitted for disallowed entry point "malicious"
# ---------------------------------------------------------------------------
# PluginManager — lifecycle
# ---------------------------------------------------------------------------
@plugin_manager @registration
Scenario: PluginManager registers and retrieves a plugin
Given a fresh PluginManager instance
And a PluginDescriptor for "test-plugin" with module "cleveragents.domain.models.acms.stubs" and class "InMemoryTextBackend"
When I register the plugin descriptor
Then get_plugin should return the descriptor for "test-plugin"
And the plugin state should be "discovered"
@plugin_manager @registration
Scenario: PluginManager rejects duplicate registration
Given a fresh PluginManager instance
And a PluginDescriptor for "dup-plugin" with module "cleveragents.domain.models.acms.stubs" and class "InMemoryTextBackend"
When I register the plugin descriptor
And I attempt to register the same descriptor again
Then a PluginError should be raised
@plugin_manager @listing
Scenario: PluginManager lists all registered plugins
Given a fresh PluginManager instance
And I register plugins named "alpha", "beta", "gamma"
Then list_plugins should return 3 descriptors
@plugin_manager @not_found
Scenario: PluginManager raises on unknown plugin
Given a fresh PluginManager instance
When I attempt to get plugin "nonexistent"
Then a PluginNotFoundError should be raised
@plugin_manager @activate
Scenario: PluginManager activates a registered plugin
Given a fresh PluginManager instance
And a PluginDescriptor for "act-plugin" with module "cleveragents.domain.models.acms.stubs" and class "InMemoryTextBackend"
When I register the plugin descriptor
And I activate the plugin "act-plugin"
Then the plugin state should be "activated"
And get_plugin_class for "act-plugin" should not be None
And get_plugin_instance for "act-plugin" should not be None
@plugin_manager @activate
Scenario: PluginManager rejects activating already-activated plugin
Given a fresh PluginManager instance
And a PluginDescriptor for "double-act" with module "cleveragents.domain.models.acms.stubs" and class "InMemoryTextBackend"
When I register the plugin descriptor
And I activate the plugin "double-act"
And I attempt to activate the plugin "double-act" again
Then a PluginError should be raised
@plugin_manager @activate @failure
Scenario: PluginManager transitions to ERRORED on activation failure
Given a fresh PluginManager instance
And a PluginDescriptor for "bad-plugin" with module "cleveragents.nonexistent_module_xyz" and class "BadClass"
When I register the plugin descriptor
And I attempt to activate the plugin "bad-plugin"
Then a PluginLoadError should be raised
And the plugin "bad-plugin" state should be "errored"
@plugin_manager @activate @failure
Scenario: PluginManager fails activation with empty module_path
Given a fresh PluginManager instance
And a PluginDescriptor for "no-path" with empty module_path
When I register the plugin descriptor
And I attempt to activate the plugin "no-path"
Then a PluginLoadError should be raised
@plugin_manager @deactivate
Scenario: PluginManager deactivates an activated plugin
Given a fresh PluginManager instance
And a PluginDescriptor for "deact-plugin" with module "cleveragents.domain.models.acms.stubs" and class "InMemoryTextBackend"
When I register the plugin descriptor
And I activate the plugin "deact-plugin"
And I deactivate the plugin "deact-plugin"
Then the plugin state should be "deactivated"
And get_plugin_class for "deact-plugin" should be None
And get_plugin_instance for "deact-plugin" should be None
@plugin_manager @deactivate
Scenario: PluginManager rejects deactivating a discovered plugin
Given a fresh PluginManager instance
And a PluginDescriptor for "disc-plugin" with module "cleveragents.domain.models.acms.stubs" and class "InMemoryTextBackend"
When I register the plugin descriptor
And I attempt to deactivate the plugin "disc-plugin"
Then a PluginError should be raised
@plugin_manager @deactivate
Scenario: PluginManager can deactivate an errored plugin
Given a fresh PluginManager instance
And a PluginDescriptor for "err-plugin" with module "cleveragents.nonexistent_module_xyz" and class "BadClass"
When I register the plugin descriptor
And I attempt to activate the plugin "err-plugin" ignoring error
And I deactivate the plugin "err-plugin"
Then the plugin state should be "deactivated"
# ---------------------------------------------------------------------------
# Config-driven registration
# ---------------------------------------------------------------------------
@plugin_manager @config
Scenario: PluginManager registers from config dict
Given a fresh PluginManager instance
When I register a plugin from config with custom_module "cleveragents.domain.models.acms.stubs" and custom_class "InMemoryTextBackend"
Then the registered plugin descriptor should not be None
And the plugin should be in the manager registry
@plugin_manager @config
Scenario: PluginManager ignores config without plugin keys
Given a fresh PluginManager instance
When I register a plugin from config with no custom_module or custom_class
Then the registered plugin descriptor should be None
@plugin_manager @config
Scenario: PluginManager returns existing for duplicate config
Given a fresh PluginManager instance
When I register a plugin from config with custom_module "cleveragents.domain.models.acms.stubs" and custom_class "InMemoryTextBackend"
And I register the same plugin from config again
Then list_plugins should return 1 descriptor
@plugin_manager @config
Scenario: PluginManager batch registers from config list
Given a fresh PluginManager instance
When I register plugins from a config list of 3 entries
Then list_plugins should return 3 descriptors
# ---------------------------------------------------------------------------
# Extension point management
# ---------------------------------------------------------------------------
@plugin_manager @extension_points
Scenario: PluginManager registers and lists extension points
Given a fresh PluginManager instance
When I register an extension point named "text-backend"
Then list_extension_points should return 1 entry
# ---------------------------------------------------------------------------
# Clear / cleanup
# ---------------------------------------------------------------------------
@plugin_manager @clear
Scenario: PluginManager clear removes all plugins
Given a fresh PluginManager instance
And I register plugins named "a", "b", "c"
When I clear the plugin manager
Then list_plugins should return 0 descriptors
# ---------------------------------------------------------------------------
# Thread safety
# ---------------------------------------------------------------------------
@thread_safety
Scenario: PluginManager supports concurrent registration
Given a fresh PluginManager instance
When 10 threads register plugins concurrently
Then list_plugins should return 10 descriptors
@thread_safety
Scenario: PluginManager supports concurrent activate and deactivate
Given a fresh PluginManager with 5 registered plugins
When 5 threads activate plugins concurrently
Then all 5 plugins should be in "activated" state
# ---------------------------------------------------------------------------
# DI container integration
# ---------------------------------------------------------------------------
@di_container
Scenario: DI container provides PluginManager singleton
When I resolve PluginManager from the DI container
Then the resolved PluginManager should not be None
And resolving PluginManager again should return the same instance
# ---------------------------------------------------------------------------
# Discovery integration
# ---------------------------------------------------------------------------
@discover
Scenario: PluginManager discover with no entry points
Given a fresh PluginManager instance
When I call discover with group "cleveragents.nonexistent_ep_group_xyz"
Then the discovered list should be empty
@discover
Scenario: PluginManager discover registers newly found plugins
Given a fresh PluginManager instance
And a mocked entry point group for discover
When I call discover through the manager
Then newly discovered plugins should be in the registry