Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,15 @@ jobs:
- uses: taiki-e/install-action@v2.75.27
with:
tool: cargo-nextest
- run: cargo nextest run --locked --workspace
- run: cargo nextest run --locked --workspace --all-features

build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.2
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2.9.1
- run: cargo build --locked --workspace
- run: cargo build --locked --workspace --all-features

bench:
runs-on: ubuntu-latest
Expand Down
28 changes: 9 additions & 19 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ members = [
"crates/contextforge-gateway-rs-cpex",
"crates/contextforge-gateway-rs-lib",
"crates/contextforge-load-test",
"crates/plugins/cpex-secrets-detection",
]
resolver = "3"

Expand Down Expand Up @@ -56,7 +57,7 @@ cpex = "=0.2.2"
uuid = { version = "1.23.1", features = ["v4"] }
axum = "0.8"
openport = { version = "0.4.0", features = ["rand"] }
secrets_detection_rust = { git = "https://github.com/IBM/cpex-plugins", rev = "6ff7af74587574fe6115ce87427519b63f6062da", package = "secrets_detection_rust" }
cpex-secrets-detection = { path = "./crates/plugins/cpex-secrets-detection" }

[profile.release]
codegen-units = 1
Expand Down
35 changes: 34 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ curl --request POST \
Runtime CPEX plugins are disabled by default. Enable hook execution when starting the gateway:

```bash
cargo run --release --bin contextforge-gateway-rs -- \
cargo run --release \
--bin contextforge-gateway-rs -- \
--address 0.0.0.0:8001 \
--redis-port 6379 \
--redis-address 127.0.0.1 \
Expand All @@ -92,6 +93,38 @@ Plugin configuration is stored in Redis at key `ContextForgeGatewayRuntimePlugin

This integration currently passes only tool payloads. CPEX configs that enable route-based plugin selection, plugin directories, global policies/defaults, non-tool hooks, or plugin conditions are rejected in this PR. Redis write access to this key is a control-plane trust boundary because it controls which registered hooks run.

### Experimental Secrets Detection Plugin

The bundled secrets detection CPEX plugin is experimental. It is compiled into
the gateway with `contextforge-gateway-rs/plugins`; Redis config only activates
plugin factories that are already present in the binary.

Activation requires all three pieces:

- Compile-time feature: `contextforge-gateway-rs/plugins`
- Runtime flag: `--runtime-plugins-enabled true`
- Redis config key: `ContextForgeGatewayRuntimePluginConfig`

The plugin kind is `validator/secrets-detection`. The dataplane currently wires
only `cmf.tool_pre_invoke` and `cmf.tool_post_invoke`.

Example run command:

```bash
cargo run --release \
--features contextforge-gateway-rs/plugins \
--bin contextforge-gateway-rs -- \
--address 0.0.0.0:8001 \
--redis-port 6379 \
--redis-address 127.0.0.1 \
--token-verification-public-key assets/jwt.key.pub \
--token-verification-private-key assets/jwt.key \
--number-of-cpus 16 \
--redis-mode=plain-text \
--upstream-connection-mode=plain-text-or-tls \
--runtime-plugins-enabled true
```

### Payload Marker Demo

This demo uses the `test-plugins` feature, which includes the demo plugin crates from `cpex-plugins-rs`. The plugin must be included in the gateway build before the gateway starts. Redis runtime registration activates already-registered factories; it does not load new Rust code into a running process.
Expand Down
2 changes: 1 addition & 1 deletion crates/contextforge-gateway-rs-lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ with_tools = []
opentelemetry_sdk.workspace = true
cpex.workspace = true
openport.workspace = true
secrets_detection_rust.workspace = true
cpex-secrets-detection.workspace = true
test-log = "0.2.20"
axum-server = { version = "0.8.0", features = ["tls-rustls"] }

Expand Down
4 changes: 2 additions & 2 deletions crates/contextforge-gateway-rs-lib/tests/gateway_plugins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,12 @@ fn reflect_text_request(text: String) -> CallToolRequestParams {
async fn runtime_with_secrets_detection(hooks: Vec<&'static str>, plugin_config: Value) -> Arc<CpexRuntimeRegistry> {
let mut runtime = CpexRuntimeRegistry::default();
runtime
.register_factory(secrets_detection_rust::KIND, Box::new(secrets_detection_rust::SecretsDetectionFactory))
.register_factory(cpex_secrets_detection::KIND, Box::new(cpex_secrets_detection::SecretsDetectionFactory))
.expect("secrets detection factory registers");
let config: CpexConfig = serde_json::from_value(json!({
"plugins": [{
"name": "secrets-detection",
"kind": secrets_detection_rust::KIND,
"kind": cpex_secrets_detection::KIND,
"hooks": hooks,
"config": plugin_config,
}]
Expand Down
4 changes: 2 additions & 2 deletions crates/contextforge-gateway-rs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ contextforge-gateway-rs-lib = { path = "../contextforge-gateway-rs-lib" }
cpex-payload-marker = { git = "https://github.com/contextforge-gateway-rs/cpex-plugins-rs", rev = "ab47801daccfbba44ea07b033034a347e7b5afdd", optional = true }
cpex-text-prefixer = { git = "https://github.com/contextforge-gateway-rs/cpex-plugins-rs", rev = "ab47801daccfbba44ea07b033034a347e7b5afdd", optional = true }
cpex-tool-namespace = { git = "https://github.com/contextforge-gateway-rs/cpex-plugins-rs", rev = "ab47801daccfbba44ea07b033034a347e7b5afdd", optional = true }
secrets_detection_rust = { workspace = true, optional = true }
cpex-secrets-detection = { workspace = true, optional = true }
clap.workspace = true
tracing.workspace = true
tracing-appender = "0.2.3"
Expand All @@ -30,7 +30,7 @@ tikv-jemallocator = "0.7.0"
rustls.workspace = true

[features]
secrets-detection-plugin = ["dep:secrets_detection_rust"]
plugins = ["dep:cpex-secrets-detection"]
Comment thread
lucarlig marked this conversation as resolved.
test-plugins = ["dep:cpex-payload-marker", "dep:cpex-text-prefixer", "dep:cpex-tool-namespace"]

[dev-dependencies]
Expand Down
10 changes: 5 additions & 5 deletions crates/contextforge-gateway-rs/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,24 +58,24 @@ fn plugin_runtime_from_config(
) -> Result<CpexRuntimeRegistry, Box<dyn std::error::Error + Send + Sync>> {
let redis_client = RedisClient::try_from(RedisConfig::try_from(config)?)?;
let plugin_runtime = CpexRuntimeRegistry::with_redis_config(redis_client);
#[cfg(any(feature = "test-plugins", feature = "secrets-detection-plugin"))]
#[cfg(any(feature = "test-plugins", feature = "plugins"))]
let plugin_runtime = register_builtin_factories(plugin_runtime)?;
Ok(plugin_runtime)
}

#[cfg(any(feature = "test-plugins", feature = "secrets-detection-plugin"))]
#[cfg(any(feature = "test-plugins", feature = "plugins"))]
fn register_builtin_factories(
mut plugin_runtime: CpexRuntimeRegistry,
) -> Result<CpexRuntimeRegistry, Box<dyn std::error::Error + Send + Sync>> {
#[cfg(feature = "test-plugins")]
{
test_plugins::register(&mut plugin_runtime)?;
}
#[cfg(feature = "secrets-detection-plugin")]
#[cfg(feature = "plugins")]
{
plugin_runtime.register_factory(
secrets_detection_rust::KIND,
Box::new(secrets_detection_rust::SecretsDetectionFactory),
cpex_secrets_detection::KIND,
Box::new(cpex_secrets_detection::SecretsDetectionFactory),
)?;
}
Ok(plugin_runtime)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright 2026
// SPDX-License-Identifier: Apache-2.0

#![cfg(feature = "secrets-detection-plugin")]
#![cfg(feature = "plugins")]
Comment thread
lucarlig marked this conversation as resolved.

use std::{
collections::HashMap,
Expand Down
22 changes: 22 additions & 0 deletions crates/plugins/cpex-secrets-detection/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
name = "cpex-secrets-detection"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
publish = false

[lib]
doctest = false

[dependencies]
cpex.workspace = true
regex = "1.12.3"
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true

[dev-dependencies]
tokio.workspace = true

[lints]
workspace = true
79 changes: 79 additions & 0 deletions crates/plugins/cpex-secrets-detection/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# ContextForge Gateway Secrets Detection

Rust CPEX secrets detection plugin for the ContextForge dataplane.

This crate is a first-class member of the `contextforge-data-plane` workspace.
It provides the `SecretsDetectionFactory` registered by the gateway binary when
the `plugins` Cargo feature is enabled.
Comment thread
dawid-nowak marked this conversation as resolved.

## Runtime Activation

The crate is compiled into the gateway binary. Runtime configuration still comes
from the dataplane plugin config document stored in Redis.

Example config:

```json
{
"version": 1,
"cpex": {
"plugins": [
{
"name": "secrets-detection",
"kind": "validator/secrets-detection",
"hooks": ["cmf.tool_pre_invoke", "cmf.tool_post_invoke"],
"config": {
"redact": true,
"redaction_text": "[redacted]",
"block_on_detection": false
}
}
]
}
}
```

The dataplane integration currently wires the tool-call path:

- `cmf.tool_pre_invoke`: scans tool arguments before the backend receives them.
- `cmf.tool_post_invoke`: scans tool results before the client receives them.

The crate also keeps prompt/resource stage handling for CPEX parity and future
hosts, but the current dataplane runtime config only uses the tool pre/post
hooks.

## Behavior

The scanner detects common secret-shaped values in JSON payloads and direct text
content. Depending on config, it can:

- redact detected values
- deny payloads when `block_on_detection` is enabled and the threshold is met
- apply dotted field allowlists/denylists to JSON arguments and results
- emit non-sensitive metadata from direct handlers when trace context exists

The CPEX plugin kind is:

```text
validator/secrets-detection
```

## Known CPEX 0.2.2 Gaps

- `PluginResult.metadata` is not propagated through `PluginManager`.
- A denied result cannot surface a redacted payload through `PluginManager`.

The direct handler can return both metadata and a redacted payload on block, but
those fields are lost at the manager/executor boundary in CPEX 0.2.2.

## Verification

From the workspace root:

```bash
cargo +1.96 test -p cpex-secrets-detection
cargo +1.96 check -p contextforge-gateway-rs --features plugins
cargo +1.96 test -p contextforge-gateway-rs-cpex
cargo +1.96 test -p contextforge-gateway-rs-lib --test gateway_plugins -- --nocapture
cargo +1.96 test -p contextforge-gateway-rs --features plugins --test secrets_detection_e2e -- --ignored --nocapture
```
Loading
Loading