Skip to main content

PluginViolation

Struct PluginViolation 

Source
pub struct PluginViolation {
    pub code: String,
    pub reason: String,
    pub description: Option<String>,
    pub details: HashMap<String, Value>,
    pub plugin_name: Option<String>,
    pub proto_error_code: Option<i64>,
}
Expand description

Structured policy violation returned by a plugin that denies execution.

Carries a machine-readable code, human-readable reason, and optional diagnostic details. Corresponds to the Python PluginViolation type.

§Examples

use cpex_core::error::PluginViolation;

let v = PluginViolation::new("missing_permission", "User lacks pii_access");
assert_eq!(v.code, "missing_permission");
assert_eq!(v.reason, "User lacks pii_access");

Fields§

§code: String

Machine-readable violation identifier (e.g., "missing_permission").

§reason: String

Short human-readable reason for the denial.

§description: Option<String>

Optional detailed explanation.

§details: HashMap<String, Value>

Structured diagnostic data for logging or debugging.

§plugin_name: Option<String>

Name of the plugin that produced the violation. Set by the framework after the plugin returns, not by the plugin itself.

§proto_error_code: Option<i64>

Protocol-level error code for the host to map to the wire format. MCP: JSON-RPC codes (e.g., -32603). HTTP: status codes (e.g., 403). Set by the plugin; the host interprets it.

Implementations§

Source§

impl PluginViolation

Source

pub fn new(code: impl Into<String>, reason: impl Into<String>) -> Self

Create a new violation with a code and reason.

Examples found in repository?
examples/plugin_demo.rs (lines 87-90)
79    async fn handle(
80        &self,
81        payload: &ToolInvokePayload,
82        _extensions: &Extensions,
83        _ctx: &mut PluginContext,
84    ) -> PluginResult<ToolInvokePayload> {
85        if payload.user.is_empty() {
86            println!("  [identity-resolver] DENIED: no user identity");
87            return PluginResult::deny(PluginViolation::new(
88                "no_identity",
89                "User identity is required",
90            ));
91        }
92        println!(
93            "  [identity-resolver] OK: user '{}' identified",
94            payload.user
95        );
96        PluginResult::allow()
97    }
98}
99
100impl HookHandler<ToolPostInvoke> for IdentityResolver {
101    async fn handle(
102        &self,
103        payload: &ToolInvokePayload,
104        _extensions: &Extensions,
105        _ctx: &mut PluginContext,
106    ) -> PluginResult<ToolInvokePayload> {
107        println!(
108            "  [identity-resolver] post-invoke: user '{}' completed '{}'",
109            payload.user, payload.tool_name
110        );
111        PluginResult::allow()
112    }
113}
114
115/// PII guard — blocks access to sensitive tools without clearance.
116struct PiiGuard {
117    cfg: PluginConfig,
118}
119
120#[async_trait]
121impl Plugin for PiiGuard {
122    fn config(&self) -> &PluginConfig {
123        &self.cfg
124    }
125    // initialize() and shutdown() use defaults — no setup needed
126}
127
128impl HookHandler<ToolPreInvoke> for PiiGuard {
129    async fn handle(
130        &self,
131        payload: &ToolInvokePayload,
132        _extensions: &Extensions,
133        ctx: &mut PluginContext,
134    ) -> PluginResult<ToolInvokePayload> {
135        // Check if the user has PII clearance (simulated via context)
136        let has_clearance = ctx
137            .get_global("pii_clearance")
138            .and_then(|v| v.as_bool())
139            .unwrap_or(false);
140
141        if !has_clearance {
142            println!(
143                "  [pii-guard] DENIED: user '{}' lacks PII clearance for '{}'",
144                payload.user, payload.tool_name
145            );
146            return PluginResult::deny(PluginViolation::new(
147                "pii_access_denied",
148                "PII clearance required",
149            ));
150        }
151
152        println!(
153            "  [pii-guard] OK: user '{}' has PII clearance",
154            payload.user
155        );
156        PluginResult::allow()
157    }
158}
159
160/// Audit logger — logs all tool invocations (fire-and-forget).
161struct AuditLogger {
162    cfg: PluginConfig,
163}
164
165#[async_trait]
166impl Plugin for AuditLogger {
167    fn config(&self) -> &PluginConfig {
168        &self.cfg
169    }
170    // initialize() and shutdown() use defaults — no setup needed
171}
172
173impl HookHandler<ToolPreInvoke> for AuditLogger {
174    async fn handle(
175        &self,
176        payload: &ToolInvokePayload,
177        _extensions: &Extensions,
178        _ctx: &mut PluginContext,
179    ) -> PluginResult<ToolInvokePayload> {
180        println!(
181            "  [audit-logger] LOG: user='{}' tool='{}' args='{}'",
182            payload.user, payload.tool_name, payload.arguments
183        );
184        PluginResult::allow()
185    }
186}
187
188impl HookHandler<ToolPostInvoke> for AuditLogger {
189    async fn handle(
190        &self,
191        payload: &ToolInvokePayload,
192        _extensions: &Extensions,
193        _ctx: &mut PluginContext,
194    ) -> PluginResult<ToolInvokePayload> {
195        println!(
196            "  [audit-logger] LOG: post-invoke user='{}' tool='{}'",
197            payload.user, payload.tool_name
198        );
199        PluginResult::allow()
200    }
201}
202
203// ---------------------------------------------------------------------------
204// Awaiting plugin example — RemoteAuthz
205// ---------------------------------------------------------------------------
206//
207// `HookHandler<H>` is async by design — `handle` is `async fn`.
208// Plugins that don't need to `.await` anything still write
209// `async fn handle` and return synchronously; this plugin shows the
210// other direction, where the body genuinely awaits per-invocation
211// work. The realistic version would call a remote authz service
212// (gRPC, HTTP, OPA, Cedarling, etc.); here we simulate the network
213// round-trip with a small `tokio::time::sleep` so the demo runs
214// offline.
215//
216// Key things this shows:
217//   1. Per-request latency state is *cached at init* — the handler
218//      consults the in-memory ACL and only "calls out" on a miss.
219//      Hot-path I/O is the most common source of latency regressions
220//      in plugins, so prefer initialize-time loading wherever you can.
221//   2. Registration uses the exact same factory pattern as any other
222//      plugin — `TypedHandlerAdapter::<H, _>` and the same
223//      `register_factory` call. There is no separate async path.
224struct RemoteAuthz {
225    cfg: PluginConfig,
226    /// ACL "fetched" at init. Populated in Plugin::initialize.
227    allowed_users: tokio::sync::RwLock<std::collections::HashSet<String>>,
228}
229
230#[async_trait]
231impl Plugin for RemoteAuthz {
232    fn config(&self) -> &PluginConfig {
233        &self.cfg
234    }
235    /// Pretend we're loading the ACL from a remote service. In a real
236    /// plugin this would be `client.fetch_acl().await`; we simulate
237    /// the round-trip with a small sleep so the demo runs offline.
238    async fn initialize(&self) -> Result<(), Box<PluginError>> {
239        tokio::time::sleep(std::time::Duration::from_millis(2)).await;
240        let mut acl = self.allowed_users.write().await;
241        acl.extend(["alice", "bob"].iter().map(|s| s.to_string()));
242        println!(
243            "  [remote-authz] initialized — ACL cached ({} users)",
244            acl.len()
245        );
246        Ok(())
247    }
248    async fn shutdown(&self) -> Result<(), Box<PluginError>> {
249        println!("  [remote-authz] shutdown");
250        Ok(())
251    }
252}
253
254impl HookHandler<ToolPreInvoke> for RemoteAuthz {
255    async fn handle(
256        &self,
257        payload: &ToolInvokePayload,
258        _extensions: &Extensions,
259        _ctx: &mut PluginContext,
260    ) -> PluginResult<ToolInvokePayload> {
261        // Cache hit path — fast.
262        let acl = self.allowed_users.read().await;
263        if acl.contains(&payload.user) {
264            println!(
265                "  [remote-authz] OK (cache hit): user '{}' allowed",
266                payload.user
267            );
268            return PluginResult::allow();
269        }
270        drop(acl); // release read lock before the fake remote call
271                   // Cache miss path — simulate a remote authz check. In a real
272                   // plugin this is where you'd `.await` a gRPC or HTTP call.
273                   // The latency cost is real and shows up on the request path.
274        tokio::time::sleep(std::time::Duration::from_millis(1)).await;
275        println!(
276            "  [remote-authz] DENIED (cache miss + remote check): user '{}'",
277            payload.user
278        );
279        PluginResult::deny(PluginViolation::new(
280            "remote_authz_denied",
281            format!("User '{}' not in remote ACL", payload.user),
282        ))
283    }
More examples
Hide additional examples
examples/cmf_capabilities_demo.rs (lines 101-104)
45    async fn handle(
46        &self,
47        payload: &MessagePayload,
48        extensions: &Extensions,
49        _ctx: &mut PluginContext,
50    ) -> PluginResult<MessagePayload> {
51        // Determine if this is pre or post invoke based on message content
52        let is_result = payload.message.is_tool_result();
53
54        if is_result {
55            // POST-INVOKE: verify the tool result came from an authorized call
56            let tool_name = payload
57                .message
58                .get_tool_results()
59                .first()
60                .map(|tr| tr.tool_name.as_str())
61                .unwrap_or("unknown");
62            println!(
63                "  [identity-checker] POST-INVOKE: verifying result from '{}'",
64                tool_name
65            );
66
67            if let Some(ref security) = extensions.security {
68                if let Some(ref subject) = security.subject {
69                    println!(
70                        "  [identity-checker] Result authorized for subject: {:?}",
71                        subject.id
72                    );
73                }
74            }
75            println!("  [identity-checker] POST-INVOKE ALLOWED");
76        } else {
77            // PRE-INVOKE: check caller identity and roles
78            let tool_name = payload
79                .message
80                .get_tool_calls()
81                .first()
82                .map(|tc| tc.name.as_str())
83                .unwrap_or("unknown");
84            println!(
85                "  [identity-checker] PRE-INVOKE: checking identity for '{}'",
86                tool_name
87            );
88
89            if let Some(ref security) = extensions.security {
90                let labels: Vec<&String> = security.labels.iter().collect();
91                println!("  [identity-checker] Security labels: {:?}", labels);
92
93                if let Some(ref subject) = security.subject {
94                    println!(
95                        "  [identity-checker] Subject: {:?}, Roles: {:?}",
96                        subject.id,
97                        subject.roles.iter().collect::<Vec<_>>()
98                    );
99
100                    if security.has_label("PII") && !subject.roles.contains("hr_admin") {
101                        return PluginResult::deny(PluginViolation::new(
102                            "insufficient_role",
103                            format!("Tool '{}' requires 'hr_admin' role for PII data", tool_name),
104                        ));
105                    }
106                }
107            }
108
109            if extensions.http.is_some() {
110                println!("  [identity-checker] WARNING: HTTP visible (unexpected!)");
111            } else {
112                println!("  [identity-checker] HTTP: not visible (correct — no read_headers)");
113            }
114            println!("  [identity-checker] PRE-INVOKE ALLOWED");
115        }
116
117        PluginResult::allow()
118    }
Source

pub fn with_description(self, description: impl Into<String>) -> Self

Attach a detailed description.

Source

pub fn with_details(self, details: HashMap<String, Value>) -> Self

Attach structured diagnostic details.

Source

pub fn with_proto_error_code(self, code: i64) -> Self

Attach a protocol-level error code.

Trait Implementations§

Source§

impl Clone for PluginViolation

Source§

fn clone(&self) -> PluginViolation

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for PluginViolation

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl<'de> Deserialize<'de> for PluginViolation

Source§

fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>
where __D: Deserializer<'de>,

Deserialize this value from the given Serde deserializer. Read more
Source§

impl Display for PluginViolation

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Serialize for PluginViolation

Source§

fn serialize<__S>(&self, __serializer: __S) -> Result<__S::Ok, __S::Error>
where __S: Serializer,

Serialize this value into the given Serde serializer. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> DeserializeOwned for T
where T: for<'de> Deserialize<'de>,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T> ToString for T
where T: Display + ?Sized,

Source§

fn to_string(&self) -> String

Converts the given value to a String. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more