Skip to content

refactor(plugins): migrate in-tree plugins to PyPI packages (cpex-*)#3965

Merged
brian-hussey merged 14 commits intomainfrom
jps-plugin-pypi
Apr 10, 2026
Merged

refactor(plugins): migrate in-tree plugins to PyPI packages (cpex-*)#3965
brian-hussey merged 14 commits intomainfrom
jps-plugin-pypi

Conversation

@jonpspri
Copy link
Copy Markdown
Collaborator

@jonpspri jonpspri commented Apr 1, 2026

Summary

  • Migrate 6 in-tree plugins (pii_filter, secrets_detection, url_reputation, retry_with_backoff, encoded_exfil_detection, rate_limiter) to standalone PyPI packages (cpex-*)
  • Delete all plugins_rust/ Rust plugin implementations — plugins are now distributed as pre-built wheels from PyPI
  • Remove Rust plugin builder stages from all Containerfiles (keep tools_rust/mcp_runtime)
  • Remove ~100 lines of rust-* plugin Makefile targets
  • Add [plugins] extra to install-dev and CI pytest workflow so cpex packages are installed
  • Update tool_service.py import from plugins.retry_with_backoff to cpex_retry_with_backoff
  • Update plugin kind paths across 7+ doc files from plugins.pii_filter.pii_filter.PIIFilterPlugin to cpex_pii_filter.PIIFilterPlugin
  • Clean up pre-commit configs, CODEOWNERS, MANIFEST.in, whitesource, .gitignore

Test plan

  • make install-dev && make test passes (cpex packages installed via [plugins] extra)
  • CI pytest workflow installs plugins via --extra plugins
  • Container builds succeed without plugins_rust/ directory
  • Plugin loading test (test_all_plugins_load_together) passes with correct count

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings April 1, 2026 06:20
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR migrates the gateway’s rate limiter plugin from an in-tree Python implementation to the external cpex-rate-limiter PyPI package, updating configs/build artifacts accordingly and removing the old plugin code and its tests.

Changes:

  • Add cpex-rate-limiter>=0.0.2 as a new optional dependency group ([plugins]) and lock it in uv.lock.
  • Update plugin configuration files to reference cpex_rate_limiter.RateLimiterPlugin (and disable it in default/unit-test configs).
  • Install the new optional dependency group in Containerfile.lite and remove the in-tree plugins/rate_limiter implementation and related tests/docs.

Reviewed changes

Copilot reviewed 16 out of 17 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
uv.lock Locks cpex-rate-limiter and adds the plugins extra.
pyproject.toml Introduces optional dependency group plugins with cpex-rate-limiter.
Containerfile.lite Installs .[...,plugins] so container builds include the package.
plugins/config.yaml Switches rate limiter kind to cpex_rate_limiter... and disables by default.
plugins/config-pii-guardian-policy.yaml Switches kind to external rate limiter.
tests/unit/.../init_hooks_plugins_test.yaml Switches kind to external rate limiter and disables it for unit tests.
tests/performance/plugins/config.yaml Switches kind to external rate limiter (still enabled for perf runs).
plugins/webhook_notification/test_config.yaml Switches kind to external rate limiter for webhook testing config.
docs/docs/using/plugins/plugins.md Updates rate limiter entry to point to external package.
llms/plugins-llms.md Updates plugin list entry to cpex-rate-limiter.
docs/docs/testing/unittest.md Removes rate limiter unit test from the unit test list.
plugins/rate_limiter/* Removes in-tree plugin implementation, manifest, and README.
tests/unit/.../rate_limiter/test_rate_limiter.py Removes in-tree rate limiter unit tests.
tests/integration/test_rate_limiter.py Removes in-tree rate limiter integration tests.
Comments suppressed due to low confidence (1)

tests/performance/plugins/config.yaml:248

  • This performance config enables cpex_rate_limiter.RateLimiterPlugin (mode: permissive). Since cpex-rate-limiter is an optional extra, running tests/performance/test_plugins_performance.py will fail unless users have installed .[plugins]. Consider either disabling the rate limiter here by default or adding an explicit note in this config about the required extra/install command.
  # Rate limiter (fixed window, in-memory)
  - name: RateLimiterPlugin
    kind: cpex_rate_limiter.RateLimiterPlugin
    description: Per-user/tenant/tool rate limits
    version: 0.1.0
    author: Mihai Criveti
    hooks: [prompt_pre_fetch, tool_pre_invoke]
    tags: [limits, throttle]
    mode: permissive
    priority: 20

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/performance/plugins/config.yaml Outdated
kind: plugins.rate_limiter.rate_limiter.RateLimiterPlugin
kind: cpex_rate_limiter.RateLimiterPlugin
description: Per-user/tenant/tool rate limits
version: 0.1.0
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

The plugin version: 0.1.0 no longer matches the implementation referenced by kind: cpex_rate_limiter.RateLimiterPlugin (package is pinned to >=0.0.2). Since plugin version is surfaced via admin plugin APIs/UI, consider updating this value to the external package version (or omitting it like plugins/config.yaml does) to avoid misleading metadata.

Suggested change
version: 0.1.0

Copilot uses AI. Check for mistakes.
Comment thread plugins/config-pii-guardian-policy.yaml Outdated
kind: "plugins.rate_limiter.rate_limiter.RateLimiterPlugin"
kind: "cpex_rate_limiter.RateLimiterPlugin"
description: "Per-user/tenant/tool rate limits"
version: "0.1.0"
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

The plugin version: "0.1.0" is now inconsistent with kind: "cpex_rate_limiter.RateLimiterPlugin" (external package is >=0.0.2). Because plugin version is exposed in admin plugin APIs/UI, update this value to the external package version or remove it to avoid misleading operators.

Suggested change
version: "0.1.0"

Copilot uses AI. Check for mistakes.
kind: "plugins.rate_limiter.rate_limiter.RateLimiterPlugin"
kind: "cpex_rate_limiter.RateLimiterPlugin"
description: "Per-user/tenant/tool rate limits"
version: "0.1.0"
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

This fixture still advertises version: "0.1.0" while kind now points to the external cpex_rate_limiter.RateLimiterPlugin (>=0.0.2). Even though the plugin is disabled in this test config, keeping the version accurate (or omitting it) helps avoid confusion when debugging plugin metadata or admin plugin listings.

Suggested change
version: "0.1.0"

Copilot uses AI. Check for mistakes.
Comment on lines 19 to 24
# Rate Limiter Plugin (will generate rate limit violations)
- name: "RateLimiter"
kind: "plugins.rate_limiter.rate_limiter.RateLimiterPlugin"
kind: "cpex_rate_limiter.RateLimiterPlugin"
hooks: ["tool_pre_invoke"]
mode: "permissive" # Don't block for testing
priority: 200
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

This dev test config now depends on the external cpex-rate-limiter package (kind: cpex_rate_limiter.RateLimiterPlugin). Add an explicit note here about installing the optional extra (e.g. uv pip install '.[plugins]') or set this plugin to mode: disabled by default, otherwise following the config file may fail with an import error in environments without that extra installed.

Copilot uses AI. Check for mistakes.
@lucarlig
Copy link
Copy Markdown
Collaborator

lucarlig commented Apr 1, 2026

The import works fine on my side and I can see the plugin.

the author field, I think @gandhipratik203 is the author for the Rust version we’re uploading, although we might want to keep it as “Contextforge developers” for consistency.

One last thing: should we consider keeping the Python plugin as an example somewhere instead of removing it entirely?

lucarlig
lucarlig previously approved these changes Apr 1, 2026
@gandhipratik203
Copy link
Copy Markdown
Collaborator

gandhipratik203 commented Apr 1, 2026

Hi @jonpspri @crivetimihai — thanks for putting this together. This is a good move — distributing plugins as pre-compiled PyPI packages is a well-established pattern (ruff, orjson, pydantic-core all do the same thing) and makes Rust acceleration accessible without requiring a toolchain on the gateway.

I'd like to help this move forward:

  1. ADR — I can put together an ADR for the plugin extraction pattern since I have context on both the in-tree and package approaches. Would
    cover the reasoning, pros/cons, effects on the build pipeline, testing strategy, and fallback behavior.
  2. Test coverage — Happy to add integration tests, load tests, and algorithm-level validation to the cpex-plugins repo to make sure the
    package has production-grade coverage.

@jonpspri jonpspri marked this pull request as draft April 9, 2026 22:13
@jonpspri jonpspri changed the title refactor(plugins): replace in-tree rate_limiter with cpex-rate-limiter package refactor(plugins): migrate in-tree plugins to PyPI packages (cpex-*) Apr 10, 2026
@araujof
Copy link
Copy Markdown
Member

araujof commented Apr 10, 2026

@jonpspri I don't anticipate issues, but I think we should coordinate this PR with #3754.

@lucarlig
Copy link
Copy Markdown
Collaborator

Agreed with @gandhipratik203 on the plan:

Step 1
Move all plugins out of cpex-plugins (keeping plugin tests here) and freeze plugin code in that repo.

Step 2
Figure out the testing strategy and implement it (probably just clone contextforge repo in CI), then unfreeze plugin development.

For the old Python plugins, @jonpspri will spin up an example repo.

I’ll also add an ADR to document this.

Reasoning:
Right now, changes in cpex-plugins can unintentionally break CI in mcp-context-forge. Freezing the plugin code and decoupling it ensures that random or unrelated changes don’t cause CI failures in MCP, making builds more stable and predictable.

@brian.hussey @jonpspri — does this approach work?

lucarlig added a commit that referenced this pull request Apr 10, 2026
Signed-off-by: lucarlig <luca.carlig@ibm.com>
@lucarlig lucarlig force-pushed the jps-plugin-pypi branch 2 times, most recently from e7163a0 to 751be5b Compare April 10, 2026 10:33
@lucarlig
Copy link
Copy Markdown
Collaborator

lucarlig commented Apr 10, 2026

@gandhipratik203 Short follow-up: I kept and updated the integration coverage for the packaged cpex_* plugins, fixed the shipped config/import drift, added gateway-side smoke coverage for the packaged plugin wiring, aligned the external package version metadata, and added ADR-048 to document the extraction order and repo split rationale. I also removed the gateway runtime dependency on cpex_retry_with_backoff internals and added targeted tests for the new retry parsing paths. Local full pytest, diff-cover, and doctests all passed after the follow-up fixes.

Copy link
Copy Markdown
Collaborator

@gandhipratik203 gandhipratik203 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 turning these around so quickly — really appreciate the responsiveness, and the scope decisions feel right.

  • ✅ Integration tests preserved (test_rate_limiter.py and test_encoded_exfil.py kept in-tree) — good call keeping them where they're already wired into CI.
  • url_reputation pulled from the migration scope — appreciate the conservative approach here, much better to ship it when it's ready.
  • ✅ ADR added (048-extract-rust-backed-plugins-first-and-preserve-python-examples.md) — nice to have the rationale documented for future contributors.
  • ✅ Luca's plan to freeze plugin code in cpex-plugins and decouple CI is a clean way to stabilize things and addresses the testing concern well.

Approving — thanks again @lucarlig @jonpspri for the quick turnaround on this.

jonpspri and others added 14 commits April 10, 2026 15:05
…r package

Remove the in-tree rate_limiter plugin and replace it with the
cpex-rate-limiter PyPI package, a compiled Rust extension providing
the same RateLimiterPlugin class with additional algorithms
(sliding-window, token-bucket) alongside the original fixed-window.

- Add cpex-rate-limiter>=0.0.2 as a [plugins] optional dependency
- Update Containerfile.lite to install the plugins extra
- Remove plugins/rate_limiter/ source directory
- Remove unit and integration tests that imported plugin internals
- Update all config files to use cpex_rate_limiter.RateLimiterPlugin
- Disable RateLimiterPlugin in test fixture config (package not
  available in unit test environment)
- Update documentation to reflect the external package

Signed-off-by: Jonathan Springer <jps@s390x.com>

Signed-off-by: lucarlig <luca.carlig@ibm.com>
…ngine, benchmarks, and validation (#3809)

* feat(rate-limiter): pluggable algorithms, tenant isolation fix, and scale load test

- Add pluggable algorithm strategy: fixed_window, sliding_window, token_bucket
- Add Redis backend for shared cross-instance rate limiting
- Fix tenant isolation: skip by_tenant when tenant_id is None
- Fix sliding window: sweep expired timestamps before counting
- Fix backend validation: restore _validate_config check
- Fix token bucket memory path: apply max(1,...) guard to reset timestamp
- Add Redis integration tests for all three algorithms
- Add direct regression tests for get_current_user tenant_id fallback
- Add scale load test with Redis memory timeline and live algorithm detection
- Add RL_PACE_MULTIPLIER for near-limit pace testing and boundary burst detection
- Remove redundant algorithm locustfile; scale file is canonical
- Correct stale comments and README limitations

Signed-off-by: Pratik Gandhi <gandhipratik203@gmail.com>

* feat(rate-limiter): add Rust-backed engine, check() API, benchmarks, and validation

- Rust-backed sliding window engine with pyo3-log integration
- check() API with tenant propagation, sweep/retry-after support
- Eliminate redundant ZRANGE in sliding window Lua script
- Fix detect-secrets baseline for rate limiter load tests
- Clarify memory backend is single-instance only in docs

Signed-off-by: Pratik Gandhi <gandhipratik203@gmail.com>

* chore: regenerate detect-secrets baseline after rebase

Signed-off-by: Pratik Gandhi <gandhipratik203@gmail.com>

* refactor(rate-limiter): review fixes, Redis hardening, key-format parity tests

- Extract _dispatch_hook() shared by prompt_pre_fetch and tool_pre_invoke,
  reducing each hook to a single-line wrapper
- Elevate Redis val_i64/val_f64 parse-error logging from warn to error so
  silent fail-open degradation surfaces in operator dashboards
- Clamp sliding-window reset_timestamp with .max(1) so it is always strictly
  in the future even when the oldest entry expires in < 1 s
- Add 5 s tokio::time::timeout around Redis connection establishment to
  prevent indefinite blocking on network partition
- Replace silent except-pass in EVALSHA SHA tracking with logger.debug
- Document dual Lua-script invariant (rolling-upgrade key-format parity)
  in both Python RedisBackend docstring and Rust redis_backend.rs header
- Add 7 parametrized test_redis_key_format_parity_* tests validating that
  Python and Rust produce identical Redis keys for the same inputs
- Revert unrelated .pyi stub changes for encoded_exfil_detection, pii_filter,
  retry_with_backoff, and secrets_detection

Signed-off-by: Jonathan Springer <jps@s390x.com>

* fix: strip trailing whitespace in pyi stubs, remove accidental .claude/ralph-loop.local.md

- Remove plugins_rust/rate_limiter/.claude/ralph-loop.local.md which
  was accidentally committed — this is a local Claude Code loop state
  file and should never have been checked in.
- Fix trailing whitespace in plugins_rust/rate_limiter/python/
  rate_limiter_rust/__init__.pyi docstrings to pass pre-commit hooks.

Signed-off-by: Pratik Gandhi <gandhipratik203@gmail.com>

* chore: regenerate detect-secrets baseline for new exfil test strings

Update .secrets.baseline after adding test_extra_sensitive_keywords
in plugins_rust/encoded_exfil_detection/src/lib.rs:969 which contains
a fake credential string that triggers the Secret Keyword detector.
All new entries are false positives (test data).

Signed-off-by: Pratik Gandhi <gandhipratik203@gmail.com>

* chore: audit new detect-secrets baseline entries as false positives

The baseline regeneration reset is_secret to null for entries whose
line numbers shifted. Mark all 17 unaudited entries as is_secret=false
(test data, example configs, fake credentials) to pass the
--fail-on-unaudited pre-commit check.

Signed-off-by: Pratik Gandhi <gandhipratik203@gmail.com>

---------

Signed-off-by: Pratik Gandhi <gandhipratik203@gmail.com>
Signed-off-by: Jonathan Springer <jps@s390x.com>
Co-authored-by: Jonathan Springer <jps@s390x.com>

Signed-off-by: lucarlig <luca.carlig@ibm.com>
…ation (#3839)

Implement automatic tool discovery for upstream MCP servers via
usage-aware adaptive polling. The gateway can now continuously
synchronise tool lists from registered servers without manual
intervention.

Server classification (hot/cold):
- Classify servers based on MCP session pool usage patterns
- Hot servers (top 20% by recent usage): polled at 1x base interval
- Cold servers (remaining 80%): polled at 3x base interval
- Classification is deterministic: sorted by recency, active sessions,
  use count, and URL for tie-breaking
- Leader election via Redis with TTL renewal for multi-worker
  coordination
- Falls back to local-only operation without Redis

Integration with GatewayService:
- Health checks respect hot/cold classification intervals
- Auto-refresh of tools/resources/prompts respects classification
- Fail-open on classification errors (poll anyway)
- Poll timestamps tracked via Redis with TTL expiry
- Uses base gateway URL (pre-auth) for classification lookups to
  avoid leaking query-param auth secrets to Redis

Configuration:
- AUTO_REFRESH_SERVERS=true enables automatic tool sync (default: false)
- GATEWAY_AUTO_REFRESH_INTERVAL=300 sets base polling interval
- HOT_COLD_CLASSIFICATION_ENABLED=false (opt-in, requires Redis)

Includes comprehensive tests with 100% coverage on the new
ServerClassificationService and integration tests for the
GatewayService hot/cold polling paths.

Closes #3734

Signed-off-by: Lang-Akshay <akshay.shinde26@ibm.com>
Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

Signed-off-by: lucarlig <luca.carlig@ibm.com>
…r package

Remove the in-tree rate_limiter plugin and replace it with the
cpex-rate-limiter PyPI package, a compiled Rust extension providing
the same RateLimiterPlugin class with additional algorithms
(sliding-window, token-bucket) alongside the original fixed-window.

- Add cpex-rate-limiter>=0.0.2 as a [plugins] optional dependency
- Update Containerfile.lite to install the plugins extra
- Remove plugins/rate_limiter/ source directory
- Remove unit and integration tests that imported plugin internals
- Update all config files to use cpex_rate_limiter.RateLimiterPlugin
- Disable RateLimiterPlugin in test fixture config (package not
  available in unit test environment)
- Update documentation to reflect the external package

Signed-off-by: Jonathan Springer <jps@s390x.com>

Signed-off-by: lucarlig <luca.carlig@ibm.com>
Remove all plugins_rust/ build infrastructure and update references
across Containerfiles, Makefile, CI workflows, pre-commit configs,
CODEOWNERS, and documentation to reflect that plugins are now
distributed as PyPI packages (cpex-*) via the [plugins] optional extra.

- Remove Rust plugin builder stages from all Containerfiles
- Remove ~100 lines of rust-* plugin Makefile targets (keep mcp-runtime)
- Add --extra plugins to CI pytest workflow
- Add [plugins] extra to install-dev Makefile target
- Update tool_service.py import to use cpex_retry_with_backoff
- Update plugin kind paths in 7 doc files to cpex_pii_filter.*
- Clean up pre-commit, CODEOWNERS, MANIFEST.in, whitesource, .gitignore

Signed-off-by: Jonathan Springer <jps@s390x.com>
Signed-off-by: lucarlig <luca.carlig@ibm.com>
Round 1 (blockers + high):
- Restore exclude-newer = "10 days" in pyproject.toml; replace stale
  langchain/requests pins with cpex-* per-package overrides anchored
  to 2026-04-09 so the plugins resolve newer than the global window
- Guard cpex_retry_with_backoff import in tool_service.py with
  try/except ImportError; falls back to (None, True) for the Python
  pipeline when the optional [plugins] extra is not installed
- Delete orphaned .github/workflows/rust-plugins.yml and the
  associated test cases in tests/unit/test_rust_plugins_workflow.py;
  drop the workflow card from docs/docs/architecture/explorer.html
- Delete orphaned docs/docs/using/plugins/rust-plugins.md and remove
  it from docs/docs/using/plugins/.pages mkdocs nav
- Harden docker-entrypoint.sh install_plugin_requirements:
  canonicalize /app and the resolved requirements path with
  readlink -f and require the path to live under /app/, log
  non-comment lines from the requirements file before pip runs,
  and skip cleanly on validation failure
- Delete PLUGIN-MIGRATION-PLAN.md (one-time planning doc)
- Add COPY plugins/requirements.txt to Containerfile.scratch (the
  layered Containerfile.lite already had it; the broad COPY . in
  Containerfile already includes it)

Round 2 (medium + low):
- Bump cpex-* version pin floors in pyproject.toml [plugins] to
  match resolved versions in uv.lock (cpex-rate-limiter>=0.0.3,
  cpex-encoded-exfil-detection>=0.2.0, cpex-pii-filter>=0.2.0,
  cpex-url-reputation>=0.1.1)
- Add Prerequisites section to tests/performance/PLUGIN_PROFILING.md
  documenting the [plugins] extra requirement
- Add Status: Partially superseded note to ADR-041 explaining that
  plugins_rust/ was removed when in-tree Rust plugins migrated to
  PyPI packages
- Document upgrade semantics in plugins/requirements.txt header
  (pip without --upgrade skips already-satisfied constraints)
- Add importlib.util.find_spec() precheck to
  tests/performance/test_plugins_performance.py main(); the script
  now skips cleanly with an actionable message if any of the five
  cpex packages referenced by the perf config are missing
- Rename tests/unit/test_rust_plugins_workflow.py to
  test_go_toolchain_pinning.py to match its remaining contents
  (Go workflow pin and Makefile toolchain assertion)

Follow-ups tracked in #4116 and
IBM/cpex-plugins#21 for the longer-term tool_service.py refactor
that will eliminate the cross-package import entirely.

Signed-off-by: Jonathan Springer <jps@s390x.com>

Signed-off-by: lucarlig <luca.carlig@ibm.com>
Signed-off-by: lucarlig <luca.carlig@ibm.com>
Signed-off-by: lucarlig <luca.carlig@ibm.com>
Signed-off-by: lucarlig <luca.carlig@ibm.com>
Signed-off-by: lucarlig <luca.carlig@ibm.com>
Signed-off-by: lucarlig <luca.carlig@ibm.com>
Signed-off-by: lucarlig <luca.carlig@ibm.com>
Signed-off-by: lucarlig <luca.carlig@ibm.com>
Signed-off-by: lucarlig <luca.carlig@ibm.com>
Copy link
Copy Markdown
Member

@brian-hussey brian-hussey left a comment

Choose a reason for hiding this comment

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

Approval as @lucarlig and @gandhipratik203 have previously approved.

@brian-hussey brian-hussey merged commit ca4957a into main Apr 10, 2026
27 checks passed
@brian-hussey brian-hussey deleted the jps-plugin-pypi branch April 10, 2026 14:58
claudia-gray pushed a commit that referenced this pull request Apr 13, 2026
…3965)

* refactor(plugins): replace in-tree rate_limiter with cpex-rate-limiter package

Remove the in-tree rate_limiter plugin and replace it with the
cpex-rate-limiter PyPI package, a compiled Rust extension providing
the same RateLimiterPlugin class with additional algorithms
(sliding-window, token-bucket) alongside the original fixed-window.

- Add cpex-rate-limiter>=0.0.2 as a [plugins] optional dependency
- Update Containerfile.lite to install the plugins extra
- Remove plugins/rate_limiter/ source directory
- Remove unit and integration tests that imported plugin internals
- Update all config files to use cpex_rate_limiter.RateLimiterPlugin
- Disable RateLimiterPlugin in test fixture config (package not
  available in unit test environment)
- Update documentation to reflect the external package

Signed-off-by: Jonathan Springer <jps@s390x.com>

Signed-off-by: lucarlig <luca.carlig@ibm.com>

* feat(rate-limiter): pluggable algorithms with Rust-backed execution engine, benchmarks, and validation (#3809)

* feat(rate-limiter): pluggable algorithms, tenant isolation fix, and scale load test

- Add pluggable algorithm strategy: fixed_window, sliding_window, token_bucket
- Add Redis backend for shared cross-instance rate limiting
- Fix tenant isolation: skip by_tenant when tenant_id is None
- Fix sliding window: sweep expired timestamps before counting
- Fix backend validation: restore _validate_config check
- Fix token bucket memory path: apply max(1,...) guard to reset timestamp
- Add Redis integration tests for all three algorithms
- Add direct regression tests for get_current_user tenant_id fallback
- Add scale load test with Redis memory timeline and live algorithm detection
- Add RL_PACE_MULTIPLIER for near-limit pace testing and boundary burst detection
- Remove redundant algorithm locustfile; scale file is canonical
- Correct stale comments and README limitations

Signed-off-by: Pratik Gandhi <gandhipratik203@gmail.com>

* feat(rate-limiter): add Rust-backed engine, check() API, benchmarks, and validation

- Rust-backed sliding window engine with pyo3-log integration
- check() API with tenant propagation, sweep/retry-after support
- Eliminate redundant ZRANGE in sliding window Lua script
- Fix detect-secrets baseline for rate limiter load tests
- Clarify memory backend is single-instance only in docs

Signed-off-by: Pratik Gandhi <gandhipratik203@gmail.com>

* chore: regenerate detect-secrets baseline after rebase

Signed-off-by: Pratik Gandhi <gandhipratik203@gmail.com>

* refactor(rate-limiter): review fixes, Redis hardening, key-format parity tests

- Extract _dispatch_hook() shared by prompt_pre_fetch and tool_pre_invoke,
  reducing each hook to a single-line wrapper
- Elevate Redis val_i64/val_f64 parse-error logging from warn to error so
  silent fail-open degradation surfaces in operator dashboards
- Clamp sliding-window reset_timestamp with .max(1) so it is always strictly
  in the future even when the oldest entry expires in < 1 s
- Add 5 s tokio::time::timeout around Redis connection establishment to
  prevent indefinite blocking on network partition
- Replace silent except-pass in EVALSHA SHA tracking with logger.debug
- Document dual Lua-script invariant (rolling-upgrade key-format parity)
  in both Python RedisBackend docstring and Rust redis_backend.rs header
- Add 7 parametrized test_redis_key_format_parity_* tests validating that
  Python and Rust produce identical Redis keys for the same inputs
- Revert unrelated .pyi stub changes for encoded_exfil_detection, pii_filter,
  retry_with_backoff, and secrets_detection

Signed-off-by: Jonathan Springer <jps@s390x.com>

* fix: strip trailing whitespace in pyi stubs, remove accidental .claude/ralph-loop.local.md

- Remove plugins_rust/rate_limiter/.claude/ralph-loop.local.md which
  was accidentally committed — this is a local Claude Code loop state
  file and should never have been checked in.
- Fix trailing whitespace in plugins_rust/rate_limiter/python/
  rate_limiter_rust/__init__.pyi docstrings to pass pre-commit hooks.

Signed-off-by: Pratik Gandhi <gandhipratik203@gmail.com>

* chore: regenerate detect-secrets baseline for new exfil test strings

Update .secrets.baseline after adding test_extra_sensitive_keywords
in plugins_rust/encoded_exfil_detection/src/lib.rs:969 which contains
a fake credential string that triggers the Secret Keyword detector.
All new entries are false positives (test data).

Signed-off-by: Pratik Gandhi <gandhipratik203@gmail.com>

* chore: audit new detect-secrets baseline entries as false positives

The baseline regeneration reset is_secret to null for entries whose
line numbers shifted. Mark all 17 unaudited entries as is_secret=false
(test data, example configs, fake credentials) to pass the
--fail-on-unaudited pre-commit check.

Signed-off-by: Pratik Gandhi <gandhipratik203@gmail.com>

---------

Signed-off-by: Pratik Gandhi <gandhipratik203@gmail.com>
Signed-off-by: Jonathan Springer <jps@s390x.com>
Co-authored-by: Jonathan Springer <jps@s390x.com>

Signed-off-by: lucarlig <luca.carlig@ibm.com>

* feat(discovery): add automatic tool discovery with hot/cold classification (#3839)

Implement automatic tool discovery for upstream MCP servers via
usage-aware adaptive polling. The gateway can now continuously
synchronise tool lists from registered servers without manual
intervention.

Server classification (hot/cold):
- Classify servers based on MCP session pool usage patterns
- Hot servers (top 20% by recent usage): polled at 1x base interval
- Cold servers (remaining 80%): polled at 3x base interval
- Classification is deterministic: sorted by recency, active sessions,
  use count, and URL for tie-breaking
- Leader election via Redis with TTL renewal for multi-worker
  coordination
- Falls back to local-only operation without Redis

Integration with GatewayService:
- Health checks respect hot/cold classification intervals
- Auto-refresh of tools/resources/prompts respects classification
- Fail-open on classification errors (poll anyway)
- Poll timestamps tracked via Redis with TTL expiry
- Uses base gateway URL (pre-auth) for classification lookups to
  avoid leaking query-param auth secrets to Redis

Configuration:
- AUTO_REFRESH_SERVERS=true enables automatic tool sync (default: false)
- GATEWAY_AUTO_REFRESH_INTERVAL=300 sets base polling interval
- HOT_COLD_CLASSIFICATION_ENABLED=false (opt-in, requires Redis)

Includes comprehensive tests with 100% coverage on the new
ServerClassificationService and integration tests for the
GatewayService hot/cold polling paths.

Closes #3734

Signed-off-by: Lang-Akshay <akshay.shinde26@ibm.com>
Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

Signed-off-by: lucarlig <luca.carlig@ibm.com>

* refactor(plugins): replace in-tree rate_limiter with cpex-rate-limiter package

Remove the in-tree rate_limiter plugin and replace it with the
cpex-rate-limiter PyPI package, a compiled Rust extension providing
the same RateLimiterPlugin class with additional algorithms
(sliding-window, token-bucket) alongside the original fixed-window.

- Add cpex-rate-limiter>=0.0.2 as a [plugins] optional dependency
- Update Containerfile.lite to install the plugins extra
- Remove plugins/rate_limiter/ source directory
- Remove unit and integration tests that imported plugin internals
- Update all config files to use cpex_rate_limiter.RateLimiterPlugin
- Disable RateLimiterPlugin in test fixture config (package not
  available in unit test environment)
- Update documentation to reflect the external package

Signed-off-by: Jonathan Springer <jps@s390x.com>

Signed-off-by: lucarlig <luca.carlig@ibm.com>

* refactor(plugins): update build, CI, and docs for PyPI plugin migration

Remove all plugins_rust/ build infrastructure and update references
across Containerfiles, Makefile, CI workflows, pre-commit configs,
CODEOWNERS, and documentation to reflect that plugins are now
distributed as PyPI packages (cpex-*) via the [plugins] optional extra.

- Remove Rust plugin builder stages from all Containerfiles
- Remove ~100 lines of rust-* plugin Makefile targets (keep mcp-runtime)
- Add --extra plugins to CI pytest workflow
- Add [plugins] extra to install-dev Makefile target
- Update tool_service.py import to use cpex_retry_with_backoff
- Update plugin kind paths in 7 doc files to cpex_pii_filter.*
- Clean up pre-commit, CODEOWNERS, MANIFEST.in, whitesource, .gitignore

Signed-off-by: Jonathan Springer <jps@s390x.com>
Signed-off-by: lucarlig <luca.carlig@ibm.com>

* fix(plugins): address PR review findings on PyPI plugin migration

Round 1 (blockers + high):
- Restore exclude-newer = "10 days" in pyproject.toml; replace stale
  langchain/requests pins with cpex-* per-package overrides anchored
  to 2026-04-09 so the plugins resolve newer than the global window
- Guard cpex_retry_with_backoff import in tool_service.py with
  try/except ImportError; falls back to (None, True) for the Python
  pipeline when the optional [plugins] extra is not installed
- Delete orphaned .github/workflows/rust-plugins.yml and the
  associated test cases in tests/unit/test_rust_plugins_workflow.py;
  drop the workflow card from docs/docs/architecture/explorer.html
- Delete orphaned docs/docs/using/plugins/rust-plugins.md and remove
  it from docs/docs/using/plugins/.pages mkdocs nav
- Harden docker-entrypoint.sh install_plugin_requirements:
  canonicalize /app and the resolved requirements path with
  readlink -f and require the path to live under /app/, log
  non-comment lines from the requirements file before pip runs,
  and skip cleanly on validation failure
- Delete PLUGIN-MIGRATION-PLAN.md (one-time planning doc)
- Add COPY plugins/requirements.txt to Containerfile.scratch (the
  layered Containerfile.lite already had it; the broad COPY . in
  Containerfile already includes it)

Round 2 (medium + low):
- Bump cpex-* version pin floors in pyproject.toml [plugins] to
  match resolved versions in uv.lock (cpex-rate-limiter>=0.0.3,
  cpex-encoded-exfil-detection>=0.2.0, cpex-pii-filter>=0.2.0,
  cpex-url-reputation>=0.1.1)
- Add Prerequisites section to tests/performance/PLUGIN_PROFILING.md
  documenting the [plugins] extra requirement
- Add Status: Partially superseded note to ADR-041 explaining that
  plugins_rust/ was removed when in-tree Rust plugins migrated to
  PyPI packages
- Document upgrade semantics in plugins/requirements.txt header
  (pip without --upgrade skips already-satisfied constraints)
- Add importlib.util.find_spec() precheck to
  tests/performance/test_plugins_performance.py main(); the script
  now skips cleanly with an actionable message if any of the five
  cpex packages referenced by the perf config are missing
- Rename tests/unit/test_rust_plugins_workflow.py to
  test_go_toolchain_pinning.py to match its remaining contents
  (Go workflow pin and Makefile toolchain assertion)

Follow-ups tracked in #4116 and
IBM/cpex-plugins#21 for the longer-term tool_service.py refactor
that will eliminate the cross-package import entirely.

Signed-off-by: Jonathan Springer <jps@s390x.com>

Signed-off-by: lucarlig <luca.carlig@ibm.com>

* revert: restore tests changes from PR #3965

Signed-off-by: lucarlig <luca.carlig@ibm.com>

* fix(ci): align plugin tests with PyPI migration

Signed-off-by: lucarlig <luca.carlig@ibm.com>

* test: remove legacy plugin test skip infrastructure

Signed-off-by: lucarlig <luca.carlig@ibm.com>

* test: align packaged plugin tests with rust shims

Signed-off-by: lucarlig <luca.carlig@ibm.com>

* test: cover retry policy import path in tool service

Signed-off-by: lucarlig <luca.carlig@ibm.com>

* fix: harden cpex plugin migration paths

Signed-off-by: lucarlig <luca.carlig@ibm.com>

* test: cover retry policy parser branches

Signed-off-by: lucarlig <luca.carlig@ibm.com>

* test: cover plugin requirements entrypoint path

Signed-off-by: lucarlig <luca.carlig@ibm.com>

---------

Signed-off-by: lucarlig <luca.carlig@ibm.com>
Signed-off-by: Jonathan Springer <jps@s390x.com>
Co-authored-by: Pratik Gandhi <gandhipratik203@gmail.com>
Co-authored-by: Lang-Akshay <akshay.shinde26@ibm.com>
Co-authored-by: lucarlig <luca.carlig@ibm.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants