feat(plugin): register spec-defined extension points in PluginManager

Registered all 30 spec-defined extension points in the ExtensionPointRegistry
with typed Protocol interfaces for each category: context strategies (12),
output renderers (3), validation runners (2), tool providers (2),
skill providers (2), resource handlers (2), A2A transports (2),
event handlers (2), config sources (2), and safety guardrails (1).

Added extension_protocols.py with Protocol definitions and
extension_catalog.py with the registration catalog. Wired context
subsystem to query context.* extension points at initialization.

ISSUES CLOSED: #939
This commit is contained in:
Luis Mendes
2026-03-31 03:58:43 +00:00
parent 532ea100e3
commit 3f5ca4e869
7 changed files with 1738 additions and 1 deletions
+229
View File
@@ -0,0 +1,229 @@
Feature: Plugin Extension Points Registration
The plugin infrastructure must register all 30 spec-defined extension
points with typed Protocol interfaces in the PluginManager.
Based on issue #939.
# -----------------------------------------------------------------------
# All 30 extension points are registered
# -----------------------------------------------------------------------
Scenario: All 30 extension points are registered via register_all_extension_points
Given a fresh PluginManager for extension point registration
When I register all spec-defined extension points
Then exactly 30 extension points should be registered
Scenario: Extension point definitions are available as a tuple
When I retrieve the extension point definitions
Then I should get exactly 30 extension point definitions
And each definition should have a non-empty name
And each definition should have a protocol_type
And each definition should have a non-empty description
# -----------------------------------------------------------------------
# Extension point lookup by name
# -----------------------------------------------------------------------
Scenario: Extension point lookup by name returns the correct point
Given a PluginManager with all extension points registered
When I look up extension point "context.strategy"
Then the looked-up extension point name should be "context.strategy"
Scenario: Extension point lookup for pipeline component by name
Given a PluginManager with all extension points registered
When I look up extension point "context.pipeline_component.scorer"
Then the looked-up extension point name should be "context.pipeline_component.scorer"
Scenario: Extension point lookup for safety guardrail by name
Given a PluginManager with all extension points registered
When I look up extension point "safety.guardrail"
Then the looked-up extension point name should be "safety.guardrail"
Scenario: Lookup of non-existent extension point returns None
Given a PluginManager with all extension points registered
When I look up extension point "nonexistent.point"
Then the looked-up extension point should be None
# -----------------------------------------------------------------------
# Extension point lookup by category
# -----------------------------------------------------------------------
Scenario: Extension points grouped by category have correct counts
When I retrieve extension points grouped by category
Then the "context" category should have 12 extension points
And the "output" category should have 3 extension points
And the "validation" category should have 2 extension points
And the "tool" category should have 2 extension points
And the "skill" category should have 2 extension points
And the "resource" category should have 2 extension points
And the "a2a" category should have 2 extension points
And the "event" category should have 2 extension points
And the "config" category should have 2 extension points
And the "safety" category should have 1 extension points
# -----------------------------------------------------------------------
# Protocol type correctness
# -----------------------------------------------------------------------
Scenario: Context strategy extension point has correct protocol type
Given a PluginManager with all extension points registered
When I look up extension point "context.strategy"
Then the protocol type name should be "ContextStrategyExtension"
Scenario: Output renderer extension point has correct protocol type
Given a PluginManager with all extension points registered
When I look up extension point "output.renderer"
Then the protocol type name should be "OutputRendererExtension"
Scenario: Safety guardrail extension point has correct protocol type
Given a PluginManager with all extension points registered
When I look up extension point "safety.guardrail"
Then the protocol type name should be "SafetyGuardrailExtension"
# -----------------------------------------------------------------------
# Plugin registration via entry points
# -----------------------------------------------------------------------
Scenario: Plugin registration via entry points discovers no plugins by default
Given a fresh PluginManager for extension point registration
When I discover extension point plugins from group "cleveragents.ext.test"
Then the entry point discovered list should be empty
# -----------------------------------------------------------------------
# TOTAL_EXTENSION_POINTS constant
# -----------------------------------------------------------------------
Scenario: TOTAL_EXTENSION_POINTS constant equals 30
Then the TOTAL_EXTENSION_POINTS constant should equal 30
# -----------------------------------------------------------------------
# Context extension points wired to ACMSPipeline
# -----------------------------------------------------------------------
Scenario: ACMSPipeline reports context extension points when plugin manager is wired
Given a PluginManager with all extension points registered
And an ACMSPipeline wired with the plugin manager
Then the pipeline should report 12 context extension points
Scenario: ACMSPipeline reports no context extension points when no plugin manager
Given an ACMSPipeline without a plugin manager
Then the pipeline should report 0 context extension points
# -----------------------------------------------------------------------
# All extension protocols are runtime-checkable
# -----------------------------------------------------------------------
Scenario: All extension protocol types are runtime-checkable
When I retrieve all extension protocol types
Then every protocol should be runtime-checkable
# -----------------------------------------------------------------------
# Idempotent registration
# -----------------------------------------------------------------------
Scenario: Registering extension points twice does not duplicate
Given a fresh PluginManager for extension point registration
When I register all spec-defined extension points
And I register all spec-defined extension points again
Then exactly 30 extension points should be registered
# -----------------------------------------------------------------------
# Extension point registry_key matches category
# -----------------------------------------------------------------------
Scenario: Each extension point registry_key matches its category
When I retrieve the extension point definitions
Then each definition registry_key should match its name category prefix
# -----------------------------------------------------------------------
# Protocol implementation conformance
# -----------------------------------------------------------------------
Scenario: A concrete context strategy satisfies ContextStrategyExtension
Given a concrete ContextStrategyExtension implementation
Then it should be recognized as a ContextStrategyExtension instance
And calling its protocol methods should succeed
Scenario: A concrete pipeline component satisfies ContextPipelineComponentExtension
Given a concrete ContextPipelineComponentExtension implementation
Then it should be recognized as a ContextPipelineComponentExtension instance
And calling its pipeline component methods should succeed
Scenario: A concrete storage backend satisfies ContextStorageBackendExtension
Given a concrete ContextStorageBackendExtension implementation
Then it should be recognized as a ContextStorageBackendExtension instance
And calling its storage methods should succeed
Scenario: A concrete output renderer satisfies OutputRendererExtension
Given a concrete OutputRendererExtension implementation
Then it should be recognized as an OutputRendererExtension instance
And calling its renderer methods should succeed
Scenario: A concrete output materializer satisfies OutputMaterializerExtension
Given a concrete OutputMaterializerExtension implementation
Then it should be recognized as an OutputMaterializerExtension instance
Scenario: A concrete output format satisfies OutputFormatExtension
Given a concrete OutputFormatExtension implementation
Then it should be recognized as an OutputFormatExtension instance
Scenario: A concrete validation runner satisfies ValidationRunnerExtension
Given a concrete ValidationRunnerExtension implementation
Then it should be recognized as a ValidationRunnerExtension instance
Scenario: A concrete validation rule provider satisfies ValidationRuleProviderExtension
Given a concrete ValidationRuleProviderExtension implementation
Then it should be recognized as a ValidationRuleProviderExtension instance
Scenario: A concrete tool provider satisfies ToolProviderExtension
Given a concrete ToolProviderExtension implementation
Then it should be recognized as a ToolProviderExtension instance
Scenario: A concrete tool middleware satisfies ToolMiddlewareExtension
Given a concrete ToolMiddlewareExtension implementation
Then it should be recognized as a ToolMiddlewareExtension instance
Scenario: A concrete skill provider satisfies SkillProviderExtension
Given a concrete SkillProviderExtension implementation
Then it should be recognized as a SkillProviderExtension instance
Scenario: A concrete skill template satisfies SkillTemplateExtension
Given a concrete SkillTemplateExtension implementation
Then it should be recognized as a SkillTemplateExtension instance
Scenario: A concrete resource resolver satisfies ResourceResolverExtension
Given a concrete ResourceResolverExtension implementation
Then it should be recognized as a ResourceResolverExtension instance
Scenario: A concrete resource type handler satisfies ResourceTypeHandlerExtension
Given a concrete ResourceTypeHandlerExtension implementation
Then it should be recognized as a ResourceTypeHandlerExtension instance
Scenario: A concrete A2A transport satisfies A2ATransportExtension
Given a concrete A2ATransportExtension implementation
Then it should be recognized as an A2ATransportExtension instance
Scenario: A concrete A2A extension method satisfies A2AExtensionMethodExtension
Given a concrete A2AExtensionMethodExtension implementation
Then it should be recognized as an A2AExtensionMethodExtension instance
Scenario: A concrete event handler satisfies EventHandlerExtension
Given a concrete EventHandlerExtension implementation
Then it should be recognized as an EventHandlerExtension instance
Scenario: A concrete event filter satisfies EventFilterExtension
Given a concrete EventFilterExtension implementation
Then it should be recognized as an EventFilterExtension instance
Scenario: A concrete config source satisfies ConfigSourceExtension
Given a concrete ConfigSourceExtension implementation
Then it should be recognized as a ConfigSourceExtension instance
Scenario: A concrete config validator satisfies ConfigValidatorExtension
Given a concrete ConfigValidatorExtension implementation
Then it should be recognized as a ConfigValidatorExtension instance
Scenario: A concrete safety guardrail satisfies SafetyGuardrailExtension
Given a concrete SafetyGuardrailExtension implementation
Then it should be recognized as a SafetyGuardrailExtension instance
And calling its guardrail methods should succeed
@@ -0,0 +1,787 @@
"""Step definitions for plugin_extension_points.feature.
Tests all 30 spec-defined extension points: registration, lookup
by name, lookup by category, protocol type correctness, entry point
discovery, context subsystem wiring, and idempotent registration.
Based on issue #939.
"""
from __future__ import annotations
from typing import Any
from behave import given, then, when
from behave.runner import Context
from cleveragents.infrastructure.plugins.extension_catalog import (
TOTAL_EXTENSION_POINTS,
get_extension_point_definitions,
get_extension_points_by_category,
register_all_extension_points,
)
from cleveragents.infrastructure.plugins.extension_protocols import (
ALL_EXTENSION_PROTOCOLS,
A2AExtensionMethodExtension,
A2ATransportExtension,
ConfigSourceExtension,
ConfigValidatorExtension,
ContextPipelineComponentExtension,
ContextStorageBackendExtension,
ContextStrategyExtension,
EventFilterExtension,
EventHandlerExtension,
OutputFormatExtension,
OutputMaterializerExtension,
OutputRendererExtension,
ResourceResolverExtension,
ResourceTypeHandlerExtension,
SafetyGuardrailExtension,
SkillProviderExtension,
SkillTemplateExtension,
ToolMiddlewareExtension,
ToolProviderExtension,
ValidationRuleProviderExtension,
ValidationRunnerExtension,
)
from cleveragents.infrastructure.plugins.manager import PluginManager
from cleveragents.infrastructure.plugins.types import ExtensionPoint
# ---------------------------------------------------------------------------
# Fresh PluginManager
# ---------------------------------------------------------------------------
@given("a fresh PluginManager for extension point registration")
def step_fresh_pm(context: Context) -> None:
context.ep_manager = PluginManager()
@given("a PluginManager with all extension points registered")
def step_pm_with_all_eps(context: Context) -> None:
context.ep_manager = PluginManager()
register_all_extension_points(context.ep_manager)
# ---------------------------------------------------------------------------
# Register all extension points
# ---------------------------------------------------------------------------
@when("I register all spec-defined extension points")
def step_register_all(context: Context) -> None:
context.ep_registered_count = register_all_extension_points(
context.ep_manager,
)
@when("I register all spec-defined extension points again")
def step_register_all_again(context: Context) -> None:
register_all_extension_points(context.ep_manager)
@then("exactly {count:d} extension points should be registered")
def step_exactly_n_registered(context: Context, count: int) -> None:
eps = context.ep_manager.list_extension_points()
assert len(eps) == count, f"Expected {count}, got {len(eps)}"
# ---------------------------------------------------------------------------
# Extension point definitions
# ---------------------------------------------------------------------------
@when("I retrieve the extension point definitions")
def step_retrieve_defs(context: Context) -> None:
context.ep_definitions = get_extension_point_definitions()
@then("I should get exactly {count:d} extension point definitions")
def step_defs_count(context: Context, count: int) -> None:
assert len(context.ep_definitions) == count, (
f"Expected {count}, got {len(context.ep_definitions)}"
)
@then("each definition should have a non-empty name")
def step_each_def_has_name(context: Context) -> None:
for ep in context.ep_definitions:
assert ep.name, f"Extension point has empty name: {ep}"
@then("each definition should have a protocol_type")
def step_each_def_has_protocol(context: Context) -> None:
for ep in context.ep_definitions:
assert ep.protocol_type is not None, (
f"Extension point {ep.name} has no protocol_type"
)
@then("each definition should have a non-empty description")
def step_each_def_has_description(context: Context) -> None:
for ep in context.ep_definitions:
assert ep.description, (
f"Extension point {ep.name} has empty description"
)
# ---------------------------------------------------------------------------
# Lookup by name
# ---------------------------------------------------------------------------
@when('I look up extension point "{name}"')
def step_lookup_by_name(context: Context, name: str) -> None:
eps = context.ep_manager.list_extension_points()
matches = [ep for ep in eps if ep.name == name]
context.ep_lookup_result: ExtensionPoint | None = (
matches[0] if matches else None
)
@then('the looked-up extension point name should be "{name}"')
def step_lookup_name_is(context: Context, name: str) -> None:
assert context.ep_lookup_result is not None, (
f"Extension point '{name}' not found"
)
assert context.ep_lookup_result.name == name
@then("the looked-up extension point should be None")
def step_lookup_is_none(context: Context) -> None:
assert context.ep_lookup_result is None
# ---------------------------------------------------------------------------
# Lookup by category
# ---------------------------------------------------------------------------
@when("I retrieve extension points grouped by category")
def step_grouped_by_category(context: Context) -> None:
context.ep_categories = get_extension_points_by_category()
@then('the "{category}" category should have {count:d} extension points')
def step_category_count(context: Context, category: str, count: int) -> None:
actual = len(context.ep_categories.get(category, []))
assert actual == count, (
f"Category '{category}': expected {count}, got {actual}"
)
# ---------------------------------------------------------------------------
# Protocol type correctness
# ---------------------------------------------------------------------------
@then('the protocol type name should be "{proto_name}"')
def step_protocol_type_name(context: Context, proto_name: str) -> None:
assert context.ep_lookup_result is not None
actual = context.ep_lookup_result.protocol_type.__name__
assert actual == proto_name, f"Expected '{proto_name}', got '{actual}'"
# ---------------------------------------------------------------------------
# Entry point discovery
# ---------------------------------------------------------------------------
@when('I discover extension point plugins from group "{group}"')
def step_discover_ep_group(context: Context, group: str) -> None:
context.ep_discovered = context.ep_manager.discover(group=group)
@then("the entry point discovered list should be empty")
def step_ep_discovered_empty(context: Context) -> None:
assert len(context.ep_discovered) == 0
# ---------------------------------------------------------------------------
# TOTAL_EXTENSION_POINTS constant
# ---------------------------------------------------------------------------
@then("the TOTAL_EXTENSION_POINTS constant should equal {value:d}")
def step_total_constant(context: Context, value: int) -> None:
assert value == TOTAL_EXTENSION_POINTS, (
f"Expected {value}, got {TOTAL_EXTENSION_POINTS}"
)
# ---------------------------------------------------------------------------
# Context extension point wiring in ACMSPipeline
# ---------------------------------------------------------------------------
@given("an ACMSPipeline wired with the plugin manager")
def step_pipeline_with_pm(context: Context) -> None:
from cleveragents.application.services.acms_service import ACMSPipeline
context.ep_pipeline = ACMSPipeline(
plugin_manager=context.ep_manager,
)
@given("an ACMSPipeline without a plugin manager")
def step_pipeline_without_pm(context: Context) -> None:
from cleveragents.application.services.acms_service import ACMSPipeline
context.ep_pipeline = ACMSPipeline()
@then("the pipeline should report {count:d} context extension points")
def step_pipeline_context_eps(context: Context, count: int) -> None:
actual = len(context.ep_pipeline.context_extension_points)
assert actual == count, (
f"Expected {count} context extension points, got {actual}"
)
# ---------------------------------------------------------------------------
# Runtime-checkable protocols
# ---------------------------------------------------------------------------
@when("I retrieve all extension protocol types")
def step_retrieve_all_protocols(context: Context) -> None:
context.ep_protocol_map = ALL_EXTENSION_PROTOCOLS
@then("every protocol should be runtime-checkable")
def step_all_protocols_runtime_checkable(context: Context) -> None:
for name, proto in context.ep_protocol_map.items():
# runtime_checkable protocols have _is_runtime_protocol = True
is_rc: Any = getattr(proto, "_is_runtime_protocol", False)
assert is_rc, (
f"Protocol {name} ({proto.__name__}) is not runtime-checkable"
)
# ---------------------------------------------------------------------------
# registry_key matches category
# ---------------------------------------------------------------------------
@then("each definition registry_key should match its name category prefix")
def step_registry_key_matches(context: Context) -> None:
for ep in context.ep_definitions:
expected_category = ep.name.split(".")[0]
assert ep.registry_key == expected_category, (
f"Extension point '{ep.name}': registry_key '{ep.registry_key}' "
f"does not match expected category '{expected_category}'"
)
# ---------------------------------------------------------------------------
# Concrete protocol implementations for coverage
# ---------------------------------------------------------------------------
class _StubContextStrategy:
@property
def name(self) -> str:
return "stub"
def can_handle(self, request: Any) -> float:
return 1.0
def assemble(self, fragments: Any, budget: Any) -> list[Any]:
return []
class _StubPipelineComponent:
@property
def component_name(self) -> str:
return "stub_component"
def process(self, fragments: Any, context: Any) -> list[Any]:
return list(fragments)
class _StubStorageBackend:
def __init__(self) -> None:
self._store: dict[str, Any] = {}
@property
def backend_type(self) -> str:
return "memory"
def store(self, key: str, data: Any) -> None:
self._store[key] = data
def retrieve(self, key: str) -> Any:
return self._store.get(key)
class _StubOutputRenderer:
@property
def format_name(self) -> str:
return "text"
def render(self, data: Any) -> str:
return str(data)
class _StubOutputMaterializer:
@property
def target_type(self) -> str:
return "stdout"
def materialize(self, content: str, destination: str) -> None:
pass
class _StubOutputFormat:
@property
def format_id(self) -> str:
return "custom"
def serialize(self, data: Any) -> str:
return str(data)
def deserialize(self, raw: str) -> Any:
return raw
class _StubValidationRunner:
@property
def runner_name(self) -> str:
return "stub_runner"
def validate(self, target: Any) -> list[Any]:
return []
class _StubValidationRuleProvider:
@property
def provider_name(self) -> str:
return "stub_provider"
def get_rules(self) -> list[Any]:
return []
class _StubToolProvider:
@property
def provider_name(self) -> str:
return "stub_tools"
def list_tools(self) -> list[str]:
return ["tool1"]
def invoke(self, tool_name: str, arguments: Any) -> Any:
return None
class _StubToolMiddleware:
@property
def middleware_name(self) -> str:
return "stub_mw"
def before_invoke(self, tool_name: str, arguments: Any) -> dict[str, Any]:
return dict(arguments)
def after_invoke(self, tool_name: str, result: Any) -> Any:
return result
class _StubSkillProvider:
@property
def provider_name(self) -> str:
return "stub_skills"
def list_skills(self) -> list[str]:
return ["skill1"]
def get_skill(self, skill_name: str) -> Any:
return None
class _StubSkillTemplate:
@property
def template_name(self) -> str:
return "stub_template"
def render(self, context: Any) -> str:
return "rendered"
class _StubResourceResolver:
@property
def resolver_name(self) -> str:
return "stub_resolver"
def resolve(self, uri: str) -> Any:
return None
class _StubResourceTypeHandler:
@property
def handled_type(self) -> str:
return "file"
def can_handle(self, resource_type: str) -> bool:
return resource_type == "file"
def load(self, uri: str) -> Any:
return None
class _StubA2ATransport:
@property
def transport_name(self) -> str:
return "stub_transport"
def send(self, destination: str, payload: Any) -> None:
pass
def receive(self) -> Any:
return None
class _StubA2AExtensionMethod:
@property
def method_name(self) -> str:
return "stub_method"
def execute(self, params: Any) -> Any:
return None
class _StubEventHandler:
@property
def handler_name(self) -> str:
return "stub_handler"
def handle(self, event: Any) -> None:
pass
class _StubEventFilter:
@property
def filter_name(self) -> str:
return "stub_filter"
def should_propagate(self, event: Any) -> bool:
return True
class _StubConfigSource:
@property
def source_name(self) -> str:
return "stub_source"
def load(self) -> dict[str, Any]:
return {}
class _StubConfigValidator:
@property
def validator_name(self) -> str:
return "stub_validator"
def validate(self, config: Any) -> list[str]:
return []
class _StubSafetyGuardrail:
@property
def guardrail_name(self) -> str:
return "stub_guardrail"
def check(self, action: Any) -> bool:
return True
def explain(self) -> str:
return "Always allows"
# --- Context Strategy ---
@given("a concrete ContextStrategyExtension implementation")
def step_concrete_ctx_strategy(context: Context) -> None:
context.ep_impl = _StubContextStrategy()
@then("it should be recognized as a ContextStrategyExtension instance")
def step_isinstance_ctx_strategy(context: Context) -> None:
assert isinstance(context.ep_impl, ContextStrategyExtension)
@then("calling its protocol methods should succeed")
def step_call_ctx_strategy_methods(context: Context) -> None:
impl = context.ep_impl
assert impl.name == "stub"
assert impl.can_handle({}) == 1.0
assert impl.assemble([], None) == []
# --- Pipeline Component ---
@given("a concrete ContextPipelineComponentExtension implementation")
def step_concrete_pipeline(context: Context) -> None:
context.ep_impl = _StubPipelineComponent()
@then("it should be recognized as a ContextPipelineComponentExtension instance")
def step_isinstance_pipeline(context: Context) -> None:
assert isinstance(context.ep_impl, ContextPipelineComponentExtension)
@then("calling its pipeline component methods should succeed")
def step_call_pipeline_methods(context: Context) -> None:
impl = context.ep_impl
assert impl.component_name == "stub_component"
assert impl.process(["a", "b"], {}) == ["a", "b"]
# --- Storage Backend ---
@given("a concrete ContextStorageBackendExtension implementation")
def step_concrete_storage(context: Context) -> None:
context.ep_impl = _StubStorageBackend()
@then("it should be recognized as a ContextStorageBackendExtension instance")
def step_isinstance_storage(context: Context) -> None:
assert isinstance(context.ep_impl, ContextStorageBackendExtension)
@then("calling its storage methods should succeed")
def step_call_storage_methods(context: Context) -> None:
impl = context.ep_impl
assert impl.backend_type == "memory"
impl.store("k", "v")
assert impl.retrieve("k") == "v"
# --- Output Renderer ---
@given("a concrete OutputRendererExtension implementation")
def step_concrete_renderer(context: Context) -> None:
context.ep_impl = _StubOutputRenderer()
@then("it should be recognized as an OutputRendererExtension instance")
def step_isinstance_renderer(context: Context) -> None:
assert isinstance(context.ep_impl, OutputRendererExtension)
@then("calling its renderer methods should succeed")
def step_call_renderer_methods(context: Context) -> None:
impl = context.ep_impl
assert impl.format_name == "text"
assert impl.render(42) == "42"
# --- Output Materializer ---
@given("a concrete OutputMaterializerExtension implementation")
def step_concrete_materializer(context: Context) -> None:
context.ep_impl = _StubOutputMaterializer()
@then("it should be recognized as an OutputMaterializerExtension instance")
def step_isinstance_materializer(context: Context) -> None:
assert isinstance(context.ep_impl, OutputMaterializerExtension)
# --- Output Format ---
@given("a concrete OutputFormatExtension implementation")
def step_concrete_format(context: Context) -> None:
context.ep_impl = _StubOutputFormat()
@then("it should be recognized as an OutputFormatExtension instance")
def step_isinstance_format(context: Context) -> None:
assert isinstance(context.ep_impl, OutputFormatExtension)
# --- Validation Runner ---
@given("a concrete ValidationRunnerExtension implementation")
def step_concrete_val_runner(context: Context) -> None:
context.ep_impl = _StubValidationRunner()
@then("it should be recognized as a ValidationRunnerExtension instance")
def step_isinstance_val_runner(context: Context) -> None:
assert isinstance(context.ep_impl, ValidationRunnerExtension)
# --- Validation Rule Provider ---
@given("a concrete ValidationRuleProviderExtension implementation")
def step_concrete_val_rule(context: Context) -> None:
context.ep_impl = _StubValidationRuleProvider()
@then("it should be recognized as a ValidationRuleProviderExtension instance")
def step_isinstance_val_rule(context: Context) -> None:
assert isinstance(context.ep_impl, ValidationRuleProviderExtension)
# --- Tool Provider ---
@given("a concrete ToolProviderExtension implementation")
def step_concrete_tool_provider(context: Context) -> None:
context.ep_impl = _StubToolProvider()
@then("it should be recognized as a ToolProviderExtension instance")
def step_isinstance_tool_provider(context: Context) -> None:
assert isinstance(context.ep_impl, ToolProviderExtension)
# --- Tool Middleware ---
@given("a concrete ToolMiddlewareExtension implementation")
def step_concrete_tool_mw(context: Context) -> None:
context.ep_impl = _StubToolMiddleware()
@then("it should be recognized as a ToolMiddlewareExtension instance")
def step_isinstance_tool_mw(context: Context) -> None:
assert isinstance(context.ep_impl, ToolMiddlewareExtension)
# --- Skill Provider ---
@given("a concrete SkillProviderExtension implementation")
def step_concrete_skill_provider(context: Context) -> None:
context.ep_impl = _StubSkillProvider()
@then("it should be recognized as a SkillProviderExtension instance")
def step_isinstance_skill_provider(context: Context) -> None:
assert isinstance(context.ep_impl, SkillProviderExtension)
# --- Skill Template ---
@given("a concrete SkillTemplateExtension implementation")
def step_concrete_skill_template(context: Context) -> None:
context.ep_impl = _StubSkillTemplate()
@then("it should be recognized as a SkillTemplateExtension instance")
def step_isinstance_skill_template(context: Context) -> None:
assert isinstance(context.ep_impl, SkillTemplateExtension)
# --- Resource Resolver ---
@given("a concrete ResourceResolverExtension implementation")
def step_concrete_res_resolver(context: Context) -> None:
context.ep_impl = _StubResourceResolver()
@then("it should be recognized as a ResourceResolverExtension instance")
def step_isinstance_res_resolver(context: Context) -> None:
assert isinstance(context.ep_impl, ResourceResolverExtension)
# --- Resource Type Handler ---
@given("a concrete ResourceTypeHandlerExtension implementation")
def step_concrete_res_handler(context: Context) -> None:
context.ep_impl = _StubResourceTypeHandler()
@then("it should be recognized as a ResourceTypeHandlerExtension instance")
def step_isinstance_res_handler(context: Context) -> None:
assert isinstance(context.ep_impl, ResourceTypeHandlerExtension)
# --- A2A Transport ---
@given("a concrete A2ATransportExtension implementation")
def step_concrete_a2a_transport(context: Context) -> None:
context.ep_impl = _StubA2ATransport()
@then("it should be recognized as an A2ATransportExtension instance")
def step_isinstance_a2a_transport(context: Context) -> None:
assert isinstance(context.ep_impl, A2ATransportExtension)
# --- A2A Extension Method ---
@given("a concrete A2AExtensionMethodExtension implementation")
def step_concrete_a2a_method(context: Context) -> None:
context.ep_impl = _StubA2AExtensionMethod()
@then("it should be recognized as an A2AExtensionMethodExtension instance")
def step_isinstance_a2a_method(context: Context) -> None:
assert isinstance(context.ep_impl, A2AExtensionMethodExtension)
# --- Event Handler ---
@given("a concrete EventHandlerExtension implementation")
def step_concrete_event_handler(context: Context) -> None:
context.ep_impl = _StubEventHandler()
@then("it should be recognized as an EventHandlerExtension instance")
def step_isinstance_event_handler(context: Context) -> None:
assert isinstance(context.ep_impl, EventHandlerExtension)
# --- Event Filter ---
@given("a concrete EventFilterExtension implementation")
def step_concrete_event_filter(context: Context) -> None:
context.ep_impl = _StubEventFilter()
@then("it should be recognized as an EventFilterExtension instance")
def step_isinstance_event_filter(context: Context) -> None:
assert isinstance(context.ep_impl, EventFilterExtension)
# --- Config Source ---
@given("a concrete ConfigSourceExtension implementation")
def step_concrete_config_source(context: Context) -> None:
context.ep_impl = _StubConfigSource()
@then("it should be recognized as a ConfigSourceExtension instance")
def step_isinstance_config_source(context: Context) -> None:
assert isinstance(context.ep_impl, ConfigSourceExtension)
# --- Config Validator ---
@given("a concrete ConfigValidatorExtension implementation")
def step_concrete_config_validator(context: Context) -> None:
context.ep_impl = _StubConfigValidator()
@then("it should be recognized as a ConfigValidatorExtension instance")
def step_isinstance_config_validator(context: Context) -> None:
assert isinstance(context.ep_impl, ConfigValidatorExtension)
# --- Safety Guardrail ---
@given("a concrete SafetyGuardrailExtension implementation")
def step_concrete_safety(context: Context) -> None:
context.ep_impl = _StubSafetyGuardrail()
@then("it should be recognized as a SafetyGuardrailExtension instance")
def step_isinstance_safety(context: Context) -> None:
assert isinstance(context.ep_impl, SafetyGuardrailExtension)
@then("calling its guardrail methods should succeed")
def step_call_safety_methods(context: Context) -> None:
impl = context.ep_impl
assert impl.guardrail_name == "stub_guardrail"
assert impl.check("some_action") is True
assert impl.explain() == "Always allows"
+17
View File
@@ -103,6 +103,9 @@ from cleveragents.infrastructure.database.repositories import (
from cleveragents.infrastructure.database.unit_of_work import UnitOfWork
from cleveragents.infrastructure.events.reactive import ReactiveEventBus
from cleveragents.infrastructure.observability.metrics_emitter import MetricsEmitter
from cleveragents.infrastructure.plugins.extension_catalog import (
register_all_extension_points,
)
from cleveragents.infrastructure.plugins.manager import PluginManager
from cleveragents.providers.registry import ProviderRegistry, get_provider_registry
from cleveragents.shared.redaction import redact_value
@@ -771,6 +774,7 @@ class Container(containers.DeclarativeContainer):
ACMSPipeline,
settings=settings,
unit_of_work=unit_of_work,
plugin_manager=plugin_manager,
)
# ACMS ResourceFileWatcher — watches resource files for changes (#578).
@@ -856,6 +860,19 @@ def get_container() -> Container:
error_type=type(exc).__name__,
error_message=redact_value(str(exc)),
)
# Register all 30 spec-defined extension points on the
# PluginManager singleton so plugins can be discovered and
# validated against typed Protocol interfaces (#939).
try:
pm = _container.plugin_manager()
register_all_extension_points(pm)
except Exception as exc:
_logger.warning(
"extension_points_deferred",
reason="Failed to register extension points at startup.",
error_type=type(exc).__name__,
error_message=redact_value(str(exc)),
)
return _container
@@ -33,6 +33,7 @@ import structlog
if TYPE_CHECKING:
from cleveragents.config.settings import Settings
from cleveragents.infrastructure.database.unit_of_work import UnitOfWork
from cleveragents.infrastructure.plugins.manager import PluginManager
from cleveragents.domain.models.acms.crp import (
ContextRequest,
@@ -626,6 +627,7 @@ class ACMSPipeline:
*,
settings: Settings | None = None,
unit_of_work: UnitOfWork | None = None,
plugin_manager: PluginManager | None = None,
# Phase 1 — Strategy Orchestration
strategy_selector: StrategySelector | None = None,
budget_allocator: BudgetAllocator | None = None,
@@ -672,6 +674,53 @@ class ACMSPipeline:
self._preamble_generator = preamble_generator or DefaultPreambleGenerator()
self._skeleton_compressor = skeleton_compressor or DefaultSkeletonCompressor()
# Plugin extension point integration (#939).
# Store the plugin manager reference so discovered context.*
# extension points can be queried at runtime.
self._plugin_manager = plugin_manager
self._discover_context_extensions()
# ------------------------------------------------------------------
# Plugin extension point discovery
# ------------------------------------------------------------------
def _discover_context_extensions(self) -> None:
"""Query context.* extension points from the PluginManager.
Logs discovered extension points for observability. Plugin
implementations are not auto-activated here they are wired
on demand when a matching strategy or pipeline component is
requested.
"""
if self._plugin_manager is None:
return
context_eps = [
ep
for ep in self._plugin_manager.list_extension_points()
if ep.name.startswith("context.")
]
if context_eps:
self._logger.info(
"context_extensions_discovered",
count=len(context_eps),
names=[ep.name for ep in context_eps],
)
@property
def context_extension_points(self) -> list[str]:
"""Return names of registered context.* extension points.
Returns an empty list if no plugin manager is wired.
"""
if self._plugin_manager is None:
return []
return [
ep.name
for ep in self._plugin_manager.list_extension_points()
if ep.name.startswith("context.")
]
# ------------------------------------------------------------------
# Budget enforcement helper
# ------------------------------------------------------------------
@@ -10,9 +10,11 @@ Key components:
- :class:`PluginManager` lifecycle management (discover/activate/deactivate).
- :class:`PluginDescriptor` immutable metadata for a discovered plugin.
- :class:`PluginState` lifecycle state enum.
- :func:`register_all_extension_points` registers all 30 spec-defined
extension points.
Based on ``docs/specification.md`` Extension Points Summary and
issue #585.
issue #585, #939.
ISSUES CLOSED: #585
"""
@@ -25,6 +27,12 @@ from cleveragents.infrastructure.plugins.exceptions import (
PluginNotFoundError,
ProtocolMismatchError,
)
from cleveragents.infrastructure.plugins.extension_catalog import (
TOTAL_EXTENSION_POINTS,
get_extension_point_definitions,
get_extension_points_by_category,
register_all_extension_points,
)
from cleveragents.infrastructure.plugins.loader import PluginLoader
from cleveragents.infrastructure.plugins.manager import PluginManager
from cleveragents.infrastructure.plugins.types import (
@@ -34,6 +42,7 @@ from cleveragents.infrastructure.plugins.types import (
)
__all__ = [
"TOTAL_EXTENSION_POINTS",
"ExtensionPoint",
"PluginDescriptor",
"PluginError",
@@ -43,4 +52,7 @@ __all__ = [
"PluginNotFoundError",
"PluginState",
"ProtocolMismatchError",
"get_extension_point_definitions",
"get_extension_points_by_category",
"register_all_extension_points",
]
@@ -0,0 +1,306 @@
"""Extension point catalog for all 30 spec-defined extension points.
Provides :func:`register_all_extension_points` which registers every
extension point defined in ``docs/specification.md`` into the
:class:`PluginManager`.
Each extension point has a name, description, Protocol type, and
category derived from its dotted name prefix.
Based on ``docs/specification.md`` Extension Points Summary and
issue #939.
ISSUES CLOSED: #939
"""
from __future__ import annotations
from typing import Any, Protocol
from cleveragents.infrastructure.plugins.extension_protocols import (
A2AExtensionMethodExtension,
A2ATransportExtension,
ConfigSourceExtension,
ConfigValidatorExtension,
ContextPipelineComponentExtension,
ContextStorageBackendExtension,
ContextStrategyExtension,
EventFilterExtension,
EventHandlerExtension,
OutputFormatExtension,
OutputMaterializerExtension,
OutputRendererExtension,
ResourceResolverExtension,
ResourceTypeHandlerExtension,
SafetyGuardrailExtension,
SkillProviderExtension,
SkillTemplateExtension,
ToolMiddlewareExtension,
ToolProviderExtension,
ValidationRuleProviderExtension,
ValidationRunnerExtension,
)
from cleveragents.infrastructure.plugins.types import ExtensionPoint
# ---------------------------------------------------------------------------
# Structural protocol so the catalog is not tightly coupled to
# PluginManager — any object with ``register_extension_point`` works.
# ---------------------------------------------------------------------------
class ExtensionPointRegistrar(Protocol):
"""Structural type for :func:`register_all_extension_points`.
Any object with a ``register_extension_point`` method satisfies
this protocol (e.g. :class:`PluginManager`).
"""
def register_extension_point(self, extension_point: ExtensionPoint) -> None:
"""Register a single extension point."""
...
# ---------------------------------------------------------------------------
# Extension point definitions
# ---------------------------------------------------------------------------
class _ExtensionPointDef:
"""Internal descriptor for building ExtensionPoint instances."""
__slots__ = ("description", "name", "protocol_type")
def __init__(
self, name: str, protocol_type: type[Any], description: str
) -> None:
self.name = name
self.protocol_type = protocol_type
self.description = description
def _category_from_name(name: str) -> str:
"""Extract category prefix from a dotted extension point name.
Examples::
>>> _category_from_name("context.strategy")
'context'
>>> _category_from_name("context.pipeline_component.scorer")
'context'
"""
return name.split(".")[0]
# The authoritative list of all 30 spec-defined extension points.
_EXTENSION_POINT_DEFS: tuple[_ExtensionPointDef, ...] = (
# --- Context (12) ---
_ExtensionPointDef(
"context.strategy",
ContextStrategyExtension,
"Pluggable context assembly strategy",
),
_ExtensionPointDef(
"context.pipeline_component.strategy_selector",
ContextPipelineComponentExtension,
"Selects which context strategies to invoke",
),
_ExtensionPointDef(
"context.pipeline_component.budget_allocator",
ContextPipelineComponentExtension,
"Distributes token budget across strategies",
),
_ExtensionPointDef(
"context.pipeline_component.strategy_executor",
ContextPipelineComponentExtension,
"Executes selected strategies in parallel",
),
_ExtensionPointDef(
"context.pipeline_component.deduplicator",
ContextPipelineComponentExtension,
"Removes duplicate context fragments",
),
_ExtensionPointDef(
"context.pipeline_component.depth_resolver",
ContextPipelineComponentExtension,
"Resolves detail depth levels for fragments",
),
_ExtensionPointDef(
"context.pipeline_component.scorer",
ContextPipelineComponentExtension,
"Scores context fragments by relevance",
),
_ExtensionPointDef(
"context.pipeline_component.packer",
ContextPipelineComponentExtension,
"Packs scored fragments within token budget",
),
_ExtensionPointDef(
"context.pipeline_component.orderer",
ContextPipelineComponentExtension,
"Orders packed fragments for presentation",
),
_ExtensionPointDef(
"context.pipeline_component.preamble_generator",
ContextPipelineComponentExtension,
"Generates preamble text for assembled context",
),
_ExtensionPointDef(
"context.pipeline_component.skeleton_compressor",
ContextPipelineComponentExtension,
"Compresses code skeletons to save tokens",
),
_ExtensionPointDef(
"context.storage_backend",
ContextStorageBackendExtension,
"Pluggable storage backend for context data",
),
# --- Output (3) ---
_ExtensionPointDef(
"output.renderer",
OutputRendererExtension,
"Renders assembled output to a specific format",
),
_ExtensionPointDef(
"output.materializer",
OutputMaterializerExtension,
"Writes rendered output to a destination",
),
_ExtensionPointDef(
"output.format",
OutputFormatExtension,
"Custom output serialization format",
),
# --- Validation (2) ---
_ExtensionPointDef(
"validation.runner",
ValidationRunnerExtension,
"Pluggable validation execution engine",
),
_ExtensionPointDef(
"validation.rule_provider",
ValidationRuleProviderExtension,
"Provides validation rules from external sources",
),
# --- Tools (2) ---
_ExtensionPointDef(
"tool.provider",
ToolProviderExtension,
"Registers external tools for agent use",
),
_ExtensionPointDef(
"tool.middleware",
ToolMiddlewareExtension,
"Intercepts tool invocations for cross-cutting concerns",
),
# --- Skills (2) ---
_ExtensionPointDef(
"skill.provider",
SkillProviderExtension,
"Provides skill definitions from external sources",
),
_ExtensionPointDef(
"skill.template",
SkillTemplateExtension,
"Custom skill template renderer",
),
# --- Resources (2) ---
_ExtensionPointDef(
"resource.resolver",
ResourceResolverExtension,
"Resolves resource URIs to content",
),
_ExtensionPointDef(
"resource.type_handler",
ResourceTypeHandlerExtension,
"Handles loading of specific resource types",
),
# --- A2A (2) ---
_ExtensionPointDef(
"a2a.transport",
A2ATransportExtension,
"Agent-to-agent communication transport layer",
),
_ExtensionPointDef(
"a2a.extension_method",
A2AExtensionMethodExtension,
"Custom A2A protocol extension method",
),
# --- Events (2) ---
_ExtensionPointDef(
"event.handler",
EventHandlerExtension,
"Handles domain events from the event bus",
),
_ExtensionPointDef(
"event.filter",
EventFilterExtension,
"Filters events before propagation",
),
# --- Config (2) ---
_ExtensionPointDef(
"config.source",
ConfigSourceExtension,
"External configuration source provider",
),
_ExtensionPointDef(
"config.validator",
ConfigValidatorExtension,
"Validates configuration values against rules",
),
# --- Safety (1) ---
_ExtensionPointDef(
"safety.guardrail",
SafetyGuardrailExtension,
"Safety guardrail for agent action inspection",
),
)
# Public constant: number of spec-defined extension points.
TOTAL_EXTENSION_POINTS: int = 30
def get_extension_point_definitions() -> tuple[ExtensionPoint, ...]:
"""Return all 30 spec-defined extension points as ExtensionPoint models.
Each entry includes the point's name, Protocol type, description,
and a ``registry_key`` equal to its category prefix.
"""
return tuple(
ExtensionPoint(
name=defn.name,
protocol_type=defn.protocol_type,
description=defn.description,
registry_key=_category_from_name(defn.name),
)
for defn in _EXTENSION_POINT_DEFS
)
def get_extension_points_by_category() -> dict[str, list[ExtensionPoint]]:
"""Return extension points grouped by category.
Categories are derived from the first dotted segment of each
extension point name (e.g. ``"context"``, ``"output"``).
"""
grouped: dict[str, list[ExtensionPoint]] = {}
for ep in get_extension_point_definitions():
category = _category_from_name(ep.name)
grouped.setdefault(category, []).append(ep)
return grouped
def register_all_extension_points(manager: ExtensionPointRegistrar) -> int:
"""Register all 30 spec-defined extension points on *manager*.
Args:
manager: Object with a ``register_extension_point`` method
(typically :class:`PluginManager`).
Returns:
The number of extension points registered.
"""
count = 0
for ep in get_extension_point_definitions():
manager.register_extension_point(ep)
count += 1
return count
@@ -0,0 +1,337 @@
"""Typed Protocol interfaces for all spec-defined extension points.
Defines ``@runtime_checkable`` Protocol classes for each of the 30
extension point categories. Each Protocol specifies the minimum
interface that a plugin implementation must satisfy.
Based on ``docs/specification.md`` Extension Points Summary and
issue #939.
ISSUES CLOSED: #939
"""
from __future__ import annotations
from collections.abc import Mapping, Sequence
from typing import Any, Protocol, runtime_checkable
# ---------------------------------------------------------------------------
# Context extension protocols (12 extension points)
# ---------------------------------------------------------------------------
@runtime_checkable
class ContextStrategyExtension(Protocol):
"""Protocol for context strategy plugins."""
@property
def name(self) -> str: ...
def can_handle(self, request: Mapping[str, Any]) -> float: ...
def assemble(self, fragments: Sequence[Any], budget: Any) -> Sequence[Any]: ...
@runtime_checkable
class ContextPipelineComponentExtension(Protocol):
"""Protocol for pipeline stage components.
Covers all 10 pipeline phases: strategy_selector,
budget_allocator, strategy_executor, deduplicator,
depth_resolver, scorer, packer, orderer,
preamble_generator, skeleton_compressor.
"""
@property
def component_name(self) -> str: ...
def process(self, fragments: Sequence[Any], context: Mapping[str, Any]) -> Sequence[Any]: ... # noqa: E501
@runtime_checkable
class ContextStorageBackendExtension(Protocol):
"""Protocol for context storage backends (text, vector, graph)."""
@property
def backend_type(self) -> str: ...
def store(self, key: str, data: Any) -> None: ...
def retrieve(self, key: str) -> Any: ...
# ---------------------------------------------------------------------------
# Output extension protocols (3 extension points)
# ---------------------------------------------------------------------------
@runtime_checkable
class OutputRendererExtension(Protocol):
"""Protocol for output renderer plugins."""
@property
def format_name(self) -> str: ...
def render(self, data: Any) -> str: ...
@runtime_checkable
class OutputMaterializerExtension(Protocol):
"""Protocol for output materializer plugins."""
@property
def target_type(self) -> str: ...
def materialize(self, content: str, destination: str) -> None: ...
@runtime_checkable
class OutputFormatExtension(Protocol):
"""Protocol for custom output format plugins."""
@property
def format_id(self) -> str: ...
def serialize(self, data: Any) -> str: ...
def deserialize(self, raw: str) -> Any: ...
# ---------------------------------------------------------------------------
# Validation extension protocols (2 extension points)
# ---------------------------------------------------------------------------
@runtime_checkable
class ValidationRunnerExtension(Protocol):
"""Protocol for validation runner plugins."""
@property
def runner_name(self) -> str: ...
def validate(self, target: Any) -> Sequence[Any]: ...
@runtime_checkable
class ValidationRuleProviderExtension(Protocol):
"""Protocol for validation rule provider plugins."""
@property
def provider_name(self) -> str: ...
def get_rules(self) -> Sequence[Any]: ...
# ---------------------------------------------------------------------------
# Tool extension protocols (2 extension points)
# ---------------------------------------------------------------------------
@runtime_checkable
class ToolProviderExtension(Protocol):
"""Protocol for tool provider plugins."""
@property
def provider_name(self) -> str: ...
def list_tools(self) -> Sequence[str]: ...
def invoke(self, tool_name: str, arguments: Mapping[str, Any]) -> Any: ...
@runtime_checkable
class ToolMiddlewareExtension(Protocol):
"""Protocol for tool middleware plugins."""
@property
def middleware_name(self) -> str: ...
def before_invoke(self, tool_name: str, arguments: Mapping[str, Any]) -> Mapping[str, Any]: ... # noqa: E501
def after_invoke(self, tool_name: str, result: Any) -> Any: ...
# ---------------------------------------------------------------------------
# Skill extension protocols (2 extension points)
# ---------------------------------------------------------------------------
@runtime_checkable
class SkillProviderExtension(Protocol):
"""Protocol for skill provider plugins."""
@property
def provider_name(self) -> str: ...
def list_skills(self) -> Sequence[str]: ...
def get_skill(self, skill_name: str) -> Any: ...
@runtime_checkable
class SkillTemplateExtension(Protocol):
"""Protocol for skill template plugins."""
@property
def template_name(self) -> str: ...
def render(self, context: Mapping[str, Any]) -> str: ...
# ---------------------------------------------------------------------------
# Resource extension protocols (2 extension points)
# ---------------------------------------------------------------------------
@runtime_checkable
class ResourceResolverExtension(Protocol):
"""Protocol for resource resolver plugins."""
@property
def resolver_name(self) -> str: ...
def resolve(self, uri: str) -> Any: ...
@runtime_checkable
class ResourceTypeHandlerExtension(Protocol):
"""Protocol for resource type handler plugins."""
@property
def handled_type(self) -> str: ...
def can_handle(self, resource_type: str) -> bool: ...
def load(self, uri: str) -> Any: ...
# ---------------------------------------------------------------------------
# A2A extension protocols (2 extension points)
# ---------------------------------------------------------------------------
@runtime_checkable
class A2ATransportExtension(Protocol):
"""Protocol for A2A transport plugins."""
@property
def transport_name(self) -> str: ...
def send(self, destination: str, payload: Any) -> None: ...
def receive(self) -> Any: ...
@runtime_checkable
class A2AExtensionMethodExtension(Protocol):
"""Protocol for A2A extension method plugins."""
@property
def method_name(self) -> str: ...
def execute(self, params: Mapping[str, Any]) -> Any: ...
# ---------------------------------------------------------------------------
# Event extension protocols (2 extension points)
# ---------------------------------------------------------------------------
@runtime_checkable
class EventHandlerExtension(Protocol):
"""Protocol for event handler plugins."""
@property
def handler_name(self) -> str: ...
def handle(self, event: Any) -> None: ...
@runtime_checkable
class EventFilterExtension(Protocol):
"""Protocol for event filter plugins."""
@property
def filter_name(self) -> str: ...
def should_propagate(self, event: Any) -> bool: ...
# ---------------------------------------------------------------------------
# Config extension protocols (2 extension points)
# ---------------------------------------------------------------------------
@runtime_checkable
class ConfigSourceExtension(Protocol):
"""Protocol for configuration source plugins."""
@property
def source_name(self) -> str: ...
def load(self) -> Mapping[str, Any]: ...
@runtime_checkable
class ConfigValidatorExtension(Protocol):
"""Protocol for configuration validator plugins."""
@property
def validator_name(self) -> str: ...
def validate(self, config: Mapping[str, Any]) -> Sequence[str]: ...
# ---------------------------------------------------------------------------
# Safety extension protocols (1 extension point)
# ---------------------------------------------------------------------------
@runtime_checkable
class SafetyGuardrailExtension(Protocol):
"""Protocol for safety guardrail plugins."""
@property
def guardrail_name(self) -> str: ...
def check(self, action: Any) -> bool: ...
def explain(self) -> str: ...
# ---------------------------------------------------------------------------
# Re-export mapping for programmatic access
# ---------------------------------------------------------------------------
ALL_EXTENSION_PROTOCOLS: dict[str, type[Any]] = {
"context.strategy": ContextStrategyExtension,
"context.pipeline_component.strategy_selector": ContextPipelineComponentExtension,
"context.pipeline_component.budget_allocator": ContextPipelineComponentExtension,
"context.pipeline_component.strategy_executor": ContextPipelineComponentExtension,
"context.pipeline_component.deduplicator": ContextPipelineComponentExtension,
"context.pipeline_component.depth_resolver": ContextPipelineComponentExtension,
"context.pipeline_component.scorer": ContextPipelineComponentExtension,
"context.pipeline_component.packer": ContextPipelineComponentExtension,
"context.pipeline_component.orderer": ContextPipelineComponentExtension,
"context.pipeline_component.preamble_generator": ContextPipelineComponentExtension,
"context.pipeline_component.skeleton_compressor": ContextPipelineComponentExtension,
"context.storage_backend": ContextStorageBackendExtension,
"output.renderer": OutputRendererExtension,
"output.materializer": OutputMaterializerExtension,
"output.format": OutputFormatExtension,
"validation.runner": ValidationRunnerExtension,
"validation.rule_provider": ValidationRuleProviderExtension,
"tool.provider": ToolProviderExtension,
"tool.middleware": ToolMiddlewareExtension,
"skill.provider": SkillProviderExtension,
"skill.template": SkillTemplateExtension,
"resource.resolver": ResourceResolverExtension,
"resource.type_handler": ResourceTypeHandlerExtension,
"a2a.transport": A2ATransportExtension,
"a2a.extension_method": A2AExtensionMethodExtension,
"event.handler": EventHandlerExtension,
"event.filter": EventFilterExtension,
"config.source": ConfigSourceExtension,
"config.validator": ConfigValidatorExtension,
"safety.guardrail": SafetyGuardrailExtension,
}