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: StringMachine-readable violation identifier (e.g., "missing_permission").
reason: StringShort 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
impl PluginViolation
Sourcepub fn new(code: impl Into<String>, reason: impl Into<String>) -> Self
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?
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
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 }Sourcepub fn with_description(self, description: impl Into<String>) -> Self
pub fn with_description(self, description: impl Into<String>) -> Self
Attach a detailed description.
Sourcepub fn with_details(self, details: HashMap<String, Value>) -> Self
pub fn with_details(self, details: HashMap<String, Value>) -> Self
Attach structured diagnostic details.
Sourcepub fn with_proto_error_code(self, code: i64) -> Self
pub fn with_proto_error_code(self, code: i64) -> Self
Attach a protocol-level error code.
Trait Implementations§
Source§impl Clone for PluginViolation
impl Clone for PluginViolation
Source§fn clone(&self) -> PluginViolation
fn clone(&self) -> PluginViolation
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read more