Skip to content

Feat: New plugin routing feature#1480

Closed
terylt wants to merge 14 commits intomainfrom
feat/new_plugin_conditions
Closed

Feat: New plugin routing feature#1480
terylt wants to merge 14 commits intomainfrom
feat/new_plugin_conditions

Conversation

@terylt
Copy link
Copy Markdown
Collaborator

@terylt terylt commented Nov 20, 2025

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

  • Fast exact matching via name and tags (hash lookups, no expression evaluation)
  • Flexible filtering via when expressions for complex logic (regex, metadata, compound conditions)
  • Implicit priority using list position (no explicit priorities needed for simple cases)
  • Configurable merge strategy for handling overlapping rules (most_specific or merge_all)
  • Symmetric wrapping via reverse_order_on_post for cleaner pre/post hook pairs
  • Infrastructure filtering via server_name, server_id, gateway_id
  • Two-level caching - static resolution (cached) + runtime filtering (per-request when clauses)

Configuration Structure

Plugins Section

Define plugins once with their base configuration. These are templates that can be overridden in hook rules.

plugins:
  - name: pii_filter
    kind: filter
    version: 1.0.0
    author: security-team
    hooks: [tool_pre_invoke, tool_post_invoke]
    mode: enforce
    config:
      redaction_char: "*"
      log_redactions: true
    metadata:
      category: security
      compliance: [GDPR, CCPA, HIPAA]
      owner: security-team@company.com
      documentation: https://wiki.company.com/plugins/pii-filter

  - name: audit_logger
    kind: observability
    version: 1.0.0
    author: compliance-team
    hooks: [tool_pre_invoke, tool_post_invoke, prompt_pre_invoke]
    mode: permissive
    metadata:
      category: observability
      retention_default: 90

  - name: path_sanitizer
    kind: security
    version: 1.0.0
    author: security-team
    hooks: [tool_pre_invoke]
    mode: enforce

  - name: rate_limiter
    kind: protection
    version: 1.0.0
    author: ops-team
    hooks: [tool_pre_invoke]
    mode: enforce
    config:
      max_requests: 100
      window_seconds: 60

Routes Section

Define routing rules for when and how plugins attach to entities. Enable routing with plugin_settings.enable_plugin_routing: true.

plugin_settings:
  enable_plugin_routing: true
  rule_merge_strategy: "most_specific"  # or "merge_all"

Basic Examples

Tag-Based Matching

routes:
  # Apply PII filter to any tool with 'customer' or 'pii' tag
  - entities: [tool]
    tags: [customer, pii]
    reverse_order_on_post: true
    plugins:
      - name: pii_filter
        apply_to:
          fields: ["args.email", "args.ssn", "args.phone"]
      - name: audit_logger

Exact Name Match

routes:
  # Critical payment tool gets extra validation
  - entities: [tool]
    name: process_payment
    plugins:
      - name: payment_validator
      - name: fraud_detector
        config:
          threshold: 0.95  # Override default config
      - name: transaction_logger

Multiple Names or Tags

routes:
  - entities: [tool]
    name: [create_user, update_user, delete_user]
    plugins:
      - name: user_validator
      - name: audit_logger

  - entities: [tool, resource]
    tags: [sensitive]
    reverse_order_on_post: true
    plugins:
      - name: encryption_filter
      - name: audit_logger

Catch-All Rules

routes:
  # Apply to ALL tools (no name/tags filter)
  - entities: [tool]
    plugins:
      - name: general_tracker
      - name: performance_monitor

  # Apply to ALL prompts
  - entities: [prompt]
    plugins:
      - name: prompt_sanitizer

Advanced Filtering

Complex when Expressions

routes:
  # Combine tags with runtime conditions
  - entities: [tool]
    tags: [database, write-operation]
    when: "metadata.get('transaction_required') == true and server_name != 'read-replica'"
    plugins:
      - name: transaction_wrapper

  # Regex pattern matching
  - entities: [tool]
    when: "re.match(r'^(create_|update_)', name)"
    plugins:
      - name: input_validator

  # Resource URI filtering
  - entities: [resource]
    when: "payload.uri.endswith('.env') or payload.uri.endswith('.secrets')"
    plugins:
      - name: secret_redactor

HTTP-Level Plugins

routes:
  # Target specific HTTP hooks
  - hooks: [http_pre_request]
    plugins:
      - name: global_auth
      - name: request_id_injector

  # Combine hook filter with when clause
  - hooks: [http_pre_request]
    when: "payload.method == 'POST'"
    plugins:
      - name: rate_limiter
        config:
          max_requests: 100
          window_seconds: 60

  # Admin path protection
  - hooks: [http_pre_request]
    when: "payload.path.startswith('/admin/')"
    plugins:
      - name: admin_auth_checker
      - name: audit_logger

Hook Type Filtering

Target specific hooks for fine-grained control:

routes:
  # Only run on pre-invoke hooks
  - entities: [tool]
    hooks: [tool_pre_invoke]
    tags: [customer]
    plugins:
      - name: input_validator

  # Only run on post-invoke hooks
  - entities: [tool]
    hooks: [tool_post_invoke]
    tags: [customer]
    plugins:
      - name: response_sanitizer

  # Run on both pre and post hooks
  - entities: [tool]
    hooks: [tool_pre_invoke, tool_post_invoke]
    tags: [audit]
    plugins:
      - name: audit_logger

Implicit Priority

Plugins without explicit priority are auto-assigned priorities based on their list position (0, 1, 2, ...). This eliminates the need for explicit priorities in simple cases.

routes:
  - entities: [tool]
    tags: [api]
    plugins:
      - name: auth_check      # priority: 0 (auto-assigned)
      - name: rate_limiter    # priority: 1 (auto-assigned)
      - name: validator       # priority: 2 (auto-assigned)

Explicit priorities override auto-assignment:

routes:
  - entities: [tool]
    tags: [critical]
    plugins:
      - name: circuit_breaker
        priority: 5          # explicit
      - name: validator      # priority: 1 (auto-assigned)
      - name: audit_logger
        priority: 10         # explicit

Execution order: validator (1) → circuit_breaker (5) → audit_logger (10)

Infrastructure Filtering

Target specific infrastructure layers using server_name, server_id, or gateway_id filters. These are cached as part of the static resolution for performance.

routes:
  # Server-specific rules
  - entities: [tool]
    server_name: production-api
    plugins:
      - name: prod_rate_limiter

  - entities: [tool]
    server_name: [api-prod, api-staging]
    tags: [pii]
    plugins:
      - name: pii_filter

  # Gateway-specific rules
  - entities: [tool]
    gateway_id: gateway-us-east
    tags: [customer]
    plugins:
      - name: us_compliance_checker

  # Combined infrastructure + entity filtering
  - entities: [tool]
    tags: [customer]
    server_name: prod-api
    gateway_id: gateway-us-east
    plugins:
      - name: us_customer_compliance

HTTP-level vs Entity-level: Rules without entities apply 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_strategy setting controls how plugins are combined.

Strategy: most_specific (Default)

Only the most specific matching rules contribute plugins. Less specific rules are ignored.

Specificity scoring:

  • Exact name match: 1000
  • Tag match: 100
  • Hook type filter: 50
  • when expression: 10
  • Entity type only: 0
  • Infrastructure filters (server_name, etc.): add to base score
plugin_settings:
  rule_merge_strategy: "most_specific"  # default

routes:
  # Specificity: 0 (entity type only)
  - entities: [tool]
    plugins:
      - name: general_tracker

  # Specificity: 100 (tag match)
  - entities: [tool]
    tags: [customer]
    plugins:
      - name: pii_filter

  # Specificity: 1000 (name match)
  - entities: [tool]
    name: create_customer
    plugins:
      - name: customer_validator

For tool "create_customer" with tag "customer":

  • All three rules match
  • Only rules with highest specificity (1000) contribute
  • Result: customer_validator only
  • General and tag-based rules are excluded

Use case: Specific rules completely override general rules (simpler mental model, prevents plugin duplication).

Strategy: merge_all

All matching rules contribute plugins. Rules are ordered by explicit priority, then by specificity. Plugins are collected from all rules and sorted by plugin priority.

plugin_settings:
  rule_merge_strategy: "merge_all"

routes:
  - entities: [tool]
    priority: 300
    plugins:
      - name: general_tracker
        priority: 30

  - entities: [tool]
    tags: [customer]
    priority: 200
    plugins:
      - name: pii_filter
        priority: 20

  - entities: [tool]
    name: create_customer
    priority: 100
    plugins:
      - name: customer_validator
        priority: 10

For tool "create_customer" with tag "customer":

  • All three rules match
  • Rules ordered by priority: 100 → 200 → 300
  • Plugins collected and sorted by plugin priority
  • Result: 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_all strategy is experimental. The recommended and default strategy is most_specific for simpler mental models and predictable behavior.

Multiple Instances with Different Configs

The same plugin can appear multiple times with different configurations, creating separate instances:

routes:
  - entities: [tool]
    tags: [api]
    plugins:
      - name: rate_limiter
        config:
          max_requests: 100

  - entities: [tool]
    name: high_volume_api
    plugins:
      - name: rate_limiter
        config:
          max_requests: 1000

For tool "high_volume_api" with tag "api" (using merge_all):

  • Two instances of rate_limiter are created (different config hashes)
  • Both execute with their respective configs

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:

  • Add plugins to the specific entity you're viewing
  • Configure: priority, hooks (specific or default), mode, config JSON, override flag
  • Creates "simple rules" with exact entity name match (single entity only)

Multi-Entity Rule Splitting:
When you add a plugin to an entity that's part of a multi-entity rule, the system automatically:

  1. Removes that entity from the multi-entity rule
  2. Creates a new single-entity rule for it
  3. Copies all existing plugins from the multi-entity rule
  4. Adds your new plugin to the new rule

Example:

# Before: Multi-entity rule
- entities: [tool, prompt, resource]
  name: [convert-time, compare-timezones, Business Hours]
  plugins:
    - name: PIIFilterPlugin
      priority: 50

# After: Adding URLReputationPlugin to "Business Hours" via entity page
- entities: [tool, prompt, resource]
  name: [convert-time, compare-timezones]
  plugins:
    - name: PIIFilterPlugin
      priority: 50

- entities: [resource]
  name: Business Hours
  hooks: [resource_pre_fetch, resource_post_fetch]
  plugins:
    - name: PIIFilterPlugin          # Preserved from multi-entity rule
      priority: 50
    - name: URLReputationPlugin      # Newly added
      priority: 80

2. Routing Rules Panel

Full-featured rule management via Plugins → Routing Rules tab.

Features:

  • All 6 entity types: tool, prompt, resource, agent, virtual_server, mcp_server
  • Multi-entity name selection: Select multiple specific entities for one rule
  • Tag-based rules: Match entities by tags
  • When expressions: Conditional logic with regex, metadata checks, payload access
  • Global rules: Apply to all entities of a type (or HTTP-level with no entity type)
  • Plugin configuration: Priority, hooks, mode, config per plugin
  • Rule organization: Display names and metadata for documentation

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:

# Before: Generic rule matches all resources
- entities: [resource]
  plugins:
    - name: GlobalMonitor
      priority: 10

Result for "My Resource": GlobalMonitor executes

# After: Adding plugin to "My Resource" via entity page
- entities: [resource]
  plugins:
    - name: GlobalMonitor      # Still exists
      priority: 10

- entities: [resource]
  name: My Resource            # New specific rule (higher specificity)
  plugins:
    - name: SpecificPlugin
      priority: 80

Result for "My Resource": Only SpecificPlugin executes (generic rule excluded by specificity)

Workarounds:

  1. Use Routing Rules panel to modify the generic rule instead
  2. Switch to merge_all strategy (experimental) in config
  3. Manually add generic plugins to each specific rule

Feature Comparison

Feature Entity Pages Routing Rules Panel Manual YAML Only
Single entity name ✅ Current entity ✅ Select any
Multiple entity names ✅ Multi-select
All 6 entity types Current type only ✅ All types
Tag-based rules
When expressions
Global rules
Plugin priority ✅ Configurable ✅ Configurable
Plugin hooks ✅ Select specific ✅ Select specific
Plugin mode/config ✅ Advanced options ✅ Per plugin
Infrastructure filters*
Rule priority

*Infrastructure filters (server_name, server_id, gateway_id) filter entities based on which specific server/gateway instance hosts them. Different from entity types like virtual_server and mcp_server.

Six Entity Types:

  1. tool - MCP tools
  2. prompt - MCP prompts
  3. resource - MCP resources
  4. agent - A2A agents
  5. virtual_server - Virtual MCP servers (composites)
  6. mcp_server - Physical MCP server instances

Recommendations

Use Entity Pages when:

  • Adding plugins to a specific entity you're viewing
  • Quick configuration changes
  • Simple use cases

Use Routing Rules Panel when:

  • Creating tag-based or conditional rules
  • Managing multiple entities at once
  • Setting up global or catch-all rules
  • Working with multiple entity types
  • Need full visibility of all routing logic

Use Manual YAML Editing when:

  • Need infrastructure-level filters (server_name, gateway_id)
  • Automating configuration via CI/CD
  • Version controlling plugin configuration
  • Bulk importing/exporting rules

Expression Context for when Clauses

The when clause 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 ID
  • tags (list[str]): Entity tags
  • metadata (dict): Entity metadata
  • entity (dict): Complete entity object with fields:
    • entity.name: Same as name
    • entity.type: Same as entity_type
    • entity.id: Same as entity_id
    • entity.tags: Same as tags
    • entity.metadata: Same as metadata

Infrastructure Context

  • server_name (str | None): Server name
  • server_id (str | None): Server ID
  • gateway_id (str | None): Gateway ID

Request Context

  • args (dict): Request arguments (convenience accessor for payload.args)
  • payload (dict): Complete request payload (varies by hook type - see below)
  • user (str | None): User making the request
  • tenant_id (str | None): Tenant ID
  • agent (str | None): Agent identifier

Payload Fields by Hook Type

Tool hooks (tool_pre_invoke, tool_post_invoke):

  • payload.name: Tool name
  • payload.args: Tool arguments
  • payload.headers: Request headers
  • payload.result: Tool result (post-invoke only)

Prompt hooks (prompt_pre_invoke, prompt_post_invoke):

  • payload.prompt_id: Prompt ID
  • payload.args: Prompt arguments
  • payload.result: Prompt result (post-invoke only)

Resource hooks (resource_pre_fetch, resource_post_fetch):

  • payload.uri: Resource URI
  • payload.metadata: Resource metadata
  • payload.content: Resource content (post-fetch only)

Agent hooks (agent_pre_invoke, agent_post_invoke):

  • payload.agent_id: Agent ID
  • payload.args: Agent arguments
  • payload.result: Agent result (post-invoke only)

HTTP hooks (http_pre_request, http_post_request):

  • payload.method: HTTP method (GET, POST, etc.)
  • payload.path: Request path
  • payload.headers: Request headers
  • payload.client_host: Client IP/host
  • payload.query_params: Query parameters

Available Python Modules

  • re: Regular expression module for pattern matching

Example Expressions

routes:
  # Entity context
  - entities: [tool]
    when: "name.startswith('create_') or name.startswith('update_')"
    plugins: [validator]

  # Tags and metadata
  - entities: [tool]
    when: "'pii' in tags and metadata.get('risk_level') == 'high'"
    plugins: [pii_filter]

  # Infrastructure context
  - entities: [tool]
    when: "server_name in ['prod-api', 'staging-api'] and gateway_id == 'us-east'"
    plugins: [regional_compliance]

  # Request arguments
  - entities: [tool]
    when: "args.get('size', 0) > 1000"
    plugins: [size_validator]

  # Payload fields
  - entities: [resource]
    when: "payload.uri.endswith('.env') or payload.uri.endswith('.secrets')"
    plugins: [secret_redactor]

  # HTTP payload
  - when: "payload.method == 'POST' and payload.path.startswith('/admin/')"
    plugins: [admin_auth]

  # Regex matching
  - entities: [tool]
    when: "re.match(r'^(create_|update_|delete_)', name)"
    plugins: [mutation_logger]

  # Complex conditions
  - entities: [tool]
    when: "'customer' in tags and args.get('email') and server_name == 'prod-api'"
    plugins: [customer_compliance]

Two-Level Caching

The system uses a two-level caching strategy for performance:

  1. 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).

  2. Runtime filtering (per-request): Evaluate when clauses with request context. Not cached - evaluated for each request.

This means static rules are resolved once and cached, but when clauses 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:

routes:
  - entities: [tool]  # Matches ALL tools
    plugins:
      - name: general_monitor

HTTP-Level Rules

Rules without entities apply at HTTP level and must have a when clause or infrastructure filter:

routes:
  # Valid: has 'when' clause
  - when: "payload.method == 'POST'"
    plugins:
      - name: post_handler

  # Valid: has gateway filter
  - gateway_id: gateway-prod
    plugins:
      - name: prod_auth

  # Invalid: no criteria at all
  - plugins:
      - name: some_plugin  # ERROR: HTTP-level rules need 'when' or infrastructure filter

Symmetric Wrapping with reverse_order_on_post

For pre/post hook pairs, use reverse_order_on_post: true to automatically reverse plugin order on post-hooks:

routes:
  - entities: [tool]
    reverse_order_on_post: true
    plugins:
      - name: logger      # Pre: first, Post: last
      - name: validator   # Pre: second, Post: second
      - name: transformer # Pre: third, Post: first

Pre-hook order: logger → validator → transformer
Post-hook order: transformer → validator → logger (reversed)

This creates symmetric wrapping where the first pre-hook is the last post-hook.

Key Implementation Details

  • Implicit priority: Plugins without explicit priority are auto-assigned based on list position (0, 1, 2, ...)
  • Hook type filtering: Use hooks field to target specific hook types (e.g., [tool_pre_invoke], [http_pre_request])
  • Fast path optimization: name, tags, and hooks use hash lookups; when expressions evaluated only at runtime
  • Infrastructure filtering cached: server_name, server_id, gateway_id are part of static cache key
  • Multiple instances: Same plugin with different configs creates separate instances (keyed by config hash)
  • Expression context: when clauses have access to entity context (name, tags, metadata), infrastructure context (server_name, gateway_id), request context (args, payload, user), and Python modules (re for regex)
  • Plugin configs are optional: Only specify when overriding defaults from plugin definition

Signed-off-by: Teryl Taylor <terylt@ibm.com>
Teryl Taylor added 5 commits November 21, 2025 20:01
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>
@crivetimihai crivetimihai added this to the Release 1.0.0-BETA-2 milestone Dec 15, 2025
@araujof araujof self-requested a review December 16, 2025 20:27
@araujof araujof added enhancement New feature or request plugins labels Dec 16, 2025
Teryl Taylor added 4 commits December 19, 2025 14:03
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>
Copy link
Copy Markdown
Member

@araujof araujof left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 merge resolution 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:
Image
  • Include the entities in the Entities column in the rules table. The current implementation only shows the entity types. Maintain color consistency between entity types and entities. Example:

From:
Image

To this:
Image

  • Unselecting an entity type from the entity filter doesn't remove the previously selected entities from the specific entities text field:
Image
  • Consider removing the specific entities field. That would address the previous issue.
Image
  • 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:
Image
  • 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:
Image

Teryl Taylor added 2 commits January 9, 2026 15:57
Signed-off-by: Teryl Taylor <terylt@ibm.com>
Signed-off-by: Teryl Taylor <terylt@ibm.com>
@crivetimihai crivetimihai added the blocked Blocked by some other predecessor issue or PR. See notes. label Jan 13, 2026
Signed-off-by: Teryl Taylor <terylt@ibm.com>
@github-actions
Copy link
Copy Markdown

github-actions bot commented Jan 13, 2026

Dependency Review

The following issues were found:

  • ✅ 0 vulnerable package(s)
  • ❌ 1 package(s) with incompatible licenses
  • ✅ 0 package(s) with invalid SPDX license definitions
  • ⚠️ 14 package(s) with unknown licenses.

View full job summary

Signed-off-by: Teryl Taylor <terylt@ibm.com>
@crivetimihai
Copy link
Copy Markdown
Member

@terylt — is this superseded by your newer PR #2685 (gRPC plugin transport)? If so, this one can be closed.

@terylt
Copy link
Copy Markdown
Collaborator Author

terylt commented Feb 4, 2026

@crivetimihai No, this has nothing to do with the gRPC PRs... This is about plugin routing. I've decided on this one to:

  1. Incorporate the plugin manager side of this PR into the new stand alone plugin framework.
  2. Abandon the UI component of this PR. I'll let your team build out the UI however you want. You can use this as an example as you incorporate if you want or just start new.

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.

@crivetimihai
Copy link
Copy Markdown
Member

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 simpleeval, and backward compatibility with the existing condition-based system are all solid design decisions. The PR description is exceptionally thorough.

A few items to address as you move this out of draft:

  1. AI attributionrouting/rule_resolver.py has Authors: Claude Code in the header. Per project guidelines, AI assistant references must be removed from PRs/diffs.
  2. Expression evaluator security — the DEFAULT_FUNCTIONS from simpleeval are included wholesale. Please audit and explicitly document which functions are available. Also, the PR docs mention re module access in when clauses, but I don't see it registered in the evaluator.
  3. Cache invalidation_routing_cache has no TTL, no size limit, and no automatic invalidation when YAML config changes via the Admin UI. reload_config() is called through the write lock, but concurrent readers will use stale cache.
  4. Unbounded plugin instance creation_resolve_static creates plugin instances on cache miss via load_and_instantiate_plugin() but never cleans them up. With many config permutations this could grow unbounded.
  5. Shared class-level mutable state_routing_cache: dict = {} and _resolver are class-level defaults, meaning all instances share the same objects. Fragile if a second instance is ever created.
  6. fcntl file locking — Linux-only, will fail on Windows. Worth documenting.
  7. when clause TODOsserver_name and gateway_id resolve to None at runtime, which could silently break expressions.

The specificity scoring system and the test coverage with 11 YAML fixtures are impressive. Looking forward to seeing this progress!

Copy link
Copy Markdown
Member

@crivetimihai crivetimihai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

@crivetimihai
Copy link
Copy Markdown
Member

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

blocked Blocked by some other predecessor issue or PR. See notes. enhancement New feature or request plugins

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants