Conversation
Signed-off-by: Teryl Taylor <terylt@ibm.com>
Signed-off-by: Teryl Taylor <terylt@ibm.com>
Signed-off-by: Teryl Taylor <terylt@ibm.com>
Signed-off-by: Teryl Taylor <terylt@ibm.com>
Signed-off-by: Teryl Taylor <terylt@ibm.com>
Signed-off-by: Teryl Taylor <terylt@ibm.com>
Signed-off-by: Teryl Taylor <terylt@ibm.com>
Signed-off-by: Teryl Taylor <terylt@ibm.com>
Signed-off-by: Teryl Taylor <terylt@ibm.com>
Signed-off-by: Teryl Taylor <terylt@ibm.com>
araujof
left a comment
There was a problem hiding this comment.
Nice work on this PR!
The routing component for the plugin framework is a great addition to support flexible conditional triggering of plugins based on filters and rules. I think that, overall, the core implementation and changes to the plugin framework look good. It's great that you can still fallback to the legacy behavior, and that the legacy behavior can also be expressed with rules, so there is a path for migration in existing deployments.
Regarding the routing feature, it supports two modes of rule interaction: overwrite and merge, configured via environment. My recommendation is to simplify this aspect of the implementation and keep just the overwrite mode, where more specific/narrow rules take precedence. This mode seems more familiar and easy to understand to me.
Suggested changes to the core routing feature:
- Drop
mergeresolution mode in favor of a single resolution algorithm - Document how rule resolution treats entity types, priorities, etc. (with examples where it helps)
My remaining comments touch the UI (warning: I'm not an UX expert :)).
Suggested changes to the plugin configuration UI:
- Use different color identifiers for the different entity types when representing entities as widgets:
- Include the entities in the
Entitiescolumn in the rules table. The current implementation only shows the entity types. Maintain color consistency between entity types and entities. Example:
- Unselecting an entity type from the entity filter doesn't remove the previously selected entities from the specific entities text field:
- Consider removing the
specific entitiesfield. That would address the previous issue.
- Consider adding an HTTP type checkbox and remove the "Leave it empty ..." text under the selections. Selecting the http hooks would not expand to any selectors underneath, but at least this gives you a way to configure both http and non http hooks in the same rule for a plugin. I think it's also more explicit ("leaving things empty" messages can be confusing or missed). This is what it currently looks like:
- Similarly, consider the hook filter panel. Can we pre-select the defaults and remove the leave it empty message out? It currently looks like this:
Signed-off-by: Teryl Taylor <terylt@ibm.com>
Signed-off-by: Teryl Taylor <terylt@ibm.com>
Signed-off-by: Teryl Taylor <terylt@ibm.com>
Dependency ReviewThe following issues were found:
|
Signed-off-by: Teryl Taylor <terylt@ibm.com>
|
@crivetimihai No, this has nothing to do with the gRPC PRs... This is about plugin routing. I've decided on this one to:
I'll port the backend of this PR over to the new repo after we get it setup and then I'll close this one. |
|
Thanks for the plugin routing feature, @terylt! This is a well-architected addition — the declarative YAML-based routing, two-level caching (static + runtime), safe expression evaluation via A few items to address as you move this out of draft:
The specificity scoring system and the test coverage with 11 YAML fixtures are impressive. Looking forward to seeing this progress! |
crivetimihai
left a comment
There was a problem hiding this comment.
Thanks for the work on this, @terylt. The routing architecture is well-designed with the two-level resolution (static + runtime) approach. A few items:
1. plugin_admin.py is 3,657 lines (new file)
This single router file is massive. Consider splitting into sub-modules (e.g., routers/plugin_admin/rules.py, routers/plugin_admin/bulk_ops.py). For comparison, the existing admin.py is already very large; duplicating that pattern compounds the problem.
2. Security: simpleeval when clause evaluation
evaluator.py:~line 200: The PolicyEvaluator uses simpleeval with DEFAULT_OPERATORS and DEFAULT_FUNCTIONS plus custom functions. While safer than eval(), note that method calls on user-controlled strings are allowed (payload.uri.endswith(".env")). Consider explicitly restricting ALLOWED_FUNCTIONS rather than copying all defaults, and documenting that when expressions come from admin config (not user input).
3. Class-level _routing_cache on PluginManager (manager.py:~470)
_routing_cache: dict = {} is a class-level mutable dict that is not stored in __shared_state (Borg pattern). Consider initializing it in __init__ via __shared_state or documenting this intentional divergence.
4. get_routing_rules reloads config on every GET request
plugin_admin.py:~88: plugin_manager.reload_config() is called on every GET /admin/plugin-routing/rules. This reads from disk per request. Consider reloading only on explicit user action (e.g., a POST endpoint).
|
Closing this draft PR due to age (3 months), merge conflicts, and failing CI. You mentioned planning to rework the approach — please feel free to open a fresh PR against current main when ready. The plugin routing concept is valuable and worth pursuing. Thanks, @terylt! |


Plugin Routing with Declarative Rules
This document describes the declarative rule-based plugin routing system. Instead of nesting plugins under gateway→servers→tools hierarchy, we define flat routing rules that specify what entities plugins apply to using exact matches (fast path) and complex expressions (flexible path).
Key Features
nameandtags(hash lookups, no expression evaluation)whenexpressions for complex logic (regex, metadata, compound conditions)most_specificormerge_all)reverse_order_on_postfor cleaner pre/post hook pairsserver_name,server_id,gateway_idwhenclauses)Configuration Structure
Plugins Section
Define plugins once with their base configuration. These are templates that can be overridden in hook rules.
Routes Section
Define routing rules for when and how plugins attach to entities. Enable routing with
plugin_settings.enable_plugin_routing: true.Basic Examples
Tag-Based Matching
Exact Name Match
Multiple Names or Tags
Catch-All Rules
Advanced Filtering
Complex
whenExpressionsHTTP-Level Plugins
Hook Type Filtering
Target specific hooks for fine-grained control:
Implicit Priority
Plugins without explicit
priorityare auto-assigned priorities based on their list position (0, 1, 2, ...). This eliminates the need for explicit priorities in simple cases.Explicit priorities override auto-assignment:
Execution order:
validator (1) → circuit_breaker (5) → audit_logger (10)Infrastructure Filtering
Target specific infrastructure layers using
server_name,server_id, orgateway_idfilters. These are cached as part of the static resolution for performance.HTTP-level vs Entity-level: Rules without
entitiesapply at HTTP-level (before entity resolution). The system automatically filters plugins based on which hooks they support.Rule Merging Strategies
When multiple rules match the same entity, the
rule_merge_strategysetting controls how plugins are combined.Strategy:
most_specific(Default)Only the most specific matching rules contribute plugins. Less specific rules are ignored.
Specificity scoring:
whenexpression: 10server_name, etc.): add to base scoreFor tool "create_customer" with tag "customer":
customer_validatoronlyUse case: Specific rules completely override general rules (simpler mental model, prevents plugin duplication).
Strategy:
merge_allAll matching rules contribute plugins. Rules are ordered by explicit
priority, then by specificity. Plugins are collected from all rules and sorted by plugin priority.For tool "create_customer" with tag "customer":
customer_validator (10) → pii_filter (20) → general_tracker (30)Use case: Layering plugins from multiple rules (e.g., base security + tag-specific + name-specific).
Note: The
merge_allstrategy is experimental. The recommended and default strategy ismost_specificfor simpler mental models and predictable behavior.Multiple Instances with Different Configs
The same plugin can appear multiple times with different configurations, creating separate instances:
For tool "high_volume_api" with tag "api" (using
merge_all):rate_limiterare created (different config hashes)Admin UI Behavior
The Admin UI provides two interfaces for plugin management with different capabilities:
1. Entity-Level Plugin Pages
Quick plugin additions via entity detail pages (Tools, Prompts, Resources, Agents, etc.).
Features:
Multi-Entity Rule Splitting:
When you add a plugin to an entity that's part of a multi-entity rule, the system automatically:
Example:
2. Routing Rules Panel
Full-featured rule management via Plugins → Routing Rules tab.
Features:
Most Specific Strategy and the UI
With
most_specific(default), adding a plugin via entity pages can hide generic rules:Scenario: You have a generic rule applying to all resources, then add a specific plugin to one resource:
Result for "My Resource":
GlobalMonitorexecutesResult for "My Resource": Only
SpecificPluginexecutes (generic rule excluded by specificity)Workarounds:
merge_allstrategy (experimental) in configFeature Comparison
*Infrastructure filters (
server_name,server_id,gateway_id) filter entities based on which specific server/gateway instance hosts them. Different from entity types likevirtual_serverandmcp_server.Six Entity Types:
tool- MCP toolsprompt- MCP promptsresource- MCP resourcesagent- A2A agentsvirtual_server- Virtual MCP servers (composites)mcp_server- Physical MCP server instancesRecommendations
Use Entity Pages when:
Use Routing Rules Panel when:
Use Manual YAML Editing when:
Expression Context for
whenClausesThe
whenclause has access to the following attributes and metadata:Entity Context
name(str): Entity name (e.g., tool name, prompt name, resource URI)entity_type(str): Entity type (tool,prompt,resource,agent,server,gateway)entity_id(str | None): Optional entity IDtags(list[str]): Entity tagsmetadata(dict): Entity metadataentity(dict): Complete entity object with fields:entity.name: Same asnameentity.type: Same asentity_typeentity.id: Same asentity_identity.tags: Same astagsentity.metadata: Same asmetadataInfrastructure Context
server_name(str | None): Server nameserver_id(str | None): Server IDgateway_id(str | None): Gateway IDRequest Context
args(dict): Request arguments (convenience accessor forpayload.args)payload(dict): Complete request payload (varies by hook type - see below)user(str | None): User making the requesttenant_id(str | None): Tenant IDagent(str | None): Agent identifierPayload Fields by Hook Type
Tool hooks (
tool_pre_invoke,tool_post_invoke):payload.name: Tool namepayload.args: Tool argumentspayload.headers: Request headerspayload.result: Tool result (post-invoke only)Prompt hooks (
prompt_pre_invoke,prompt_post_invoke):payload.prompt_id: Prompt IDpayload.args: Prompt argumentspayload.result: Prompt result (post-invoke only)Resource hooks (
resource_pre_fetch,resource_post_fetch):payload.uri: Resource URIpayload.metadata: Resource metadatapayload.content: Resource content (post-fetch only)Agent hooks (
agent_pre_invoke,agent_post_invoke):payload.agent_id: Agent IDpayload.args: Agent argumentspayload.result: Agent result (post-invoke only)HTTP hooks (
http_pre_request,http_post_request):payload.method: HTTP method (GET, POST, etc.)payload.path: Request pathpayload.headers: Request headerspayload.client_host: Client IP/hostpayload.query_params: Query parametersAvailable Python Modules
re: Regular expression module for pattern matchingExample Expressions
Two-Level Caching
The system uses a two-level caching strategy for performance:
Static resolution (cached): Match rules by
name,tags, and infrastructure filters (server_name,server_id,gateway_id). Cache key:(entity_type, entity_name, hook_type, server_name, server_id, gateway_id).Runtime filtering (per-request): Evaluate
whenclauses with request context. Not cached - evaluated for each request.This means static rules are resolved once and cached, but
whenclauses are evaluated on every request for maximum flexibility.Corner Cases
Catch-All Rules
Rules with only
entities(no name/tags/when) match all entities of that type:HTTP-Level Rules
Rules without
entitiesapply at HTTP level and must have awhenclause or infrastructure filter:Symmetric Wrapping with
reverse_order_on_postFor pre/post hook pairs, use
reverse_order_on_post: trueto automatically reverse plugin order on post-hooks:Pre-hook order:
logger → validator → transformerPost-hook order:
transformer → validator → logger(reversed)This creates symmetric wrapping where the first pre-hook is the last post-hook.
Key Implementation Details
priorityare auto-assigned based on list position (0, 1, 2, ...)hooksfield to target specific hook types (e.g.,[tool_pre_invoke],[http_pre_request])name,tags, andhooksuse hash lookups;whenexpressions evaluated only at runtimeserver_name,server_id,gateway_idare part of static cache keywhenclauses have access to entity context (name,tags,metadata), infrastructure context (server_name,gateway_id), request context (args,payload,user), and Python modules (refor regex)