Skip to main content

PluginResult

Struct PluginResult 

Source
pub struct PluginResult<P: PluginPayload> {
    pub continue_processing: bool,
    pub modified_payload: Option<P>,
    pub modified_extensions: Option<OwnedExtensions>,
    pub violation: Option<PluginViolation>,
    pub metadata: Option<Value>,
}
Expand description

Result returned by a hook handler.

Payload and extension modifications are separate — this is a core design decision. Extension-only changes (add a label, set a header) don’t require copying the payload. The payload is only present in modified_payload when message content actually changed.

The executor interprets the result based on the plugin’s mode:

  • Sequential/Transform: modified_payload and modified_extensions are accepted.
  • Audit/Concurrent/FireAndForget: modifications are discarded.
  • Sequential/Concurrent: continue_processing = false halts the pipeline.
  • Transform/Audit/FireAndForget: blocks are suppressed.

Mirrors Python’s PluginResult[T] with separate modified_payload and modified_extensions fields.

§Examples

use cpex_core::hooks::{PluginPayload, PluginResult};
use cpex_core::error::PluginViolation;

// Define a simple payload
#[derive(Debug, Clone)]
struct TestPayload { value: i32 }
cpex_core::impl_plugin_payload!(TestPayload);

// Allow — no changes
let result: PluginResult<TestPayload> = PluginResult::allow();
assert!(result.continue_processing);
assert!(result.modified_payload.is_none());

// Deny
let result: PluginResult<TestPayload> = PluginResult::deny(
    PluginViolation::new("forbidden", "not allowed")
);
assert!(!result.continue_processing);
assert!(result.violation.is_some());

Fields§

§continue_processing: bool

Whether the pipeline should continue processing. false halts the pipeline (deny). Only respected for Sequential and Concurrent modes.

§modified_payload: Option<P>

Modified payload. None means no content modification. Only accepted from Sequential and Transform mode plugins.

§modified_extensions: Option<OwnedExtensions>

Modified extensions. None means no extension changes. Return an OwnedExtensions from extensions.cow_copy(). The executor validates (immutable unchanged, monotonic superset) and merges back into the pipeline’s Extensions.

§violation: Option<PluginViolation>

Policy violation. Present when continue_processing is false.

§metadata: Option<Value>

Optional metadata from the plugin (telemetry, diagnostics). Not used for scheduling or policy decisions.

Implementations§

Source§

impl<P: PluginPayload> PluginResult<P>

Source

pub fn allow() -> Self

Allow — payload continues unchanged, no extension changes.

Examples found in repository?
examples/plugin_demo.rs (line 96)
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 (line 117)
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    }
119}
120
121// ---------------------------------------------------------------------------
122// Plugin: HeaderInjector
123// Has read_headers, write_headers, append_labels capabilities.
124// Uses COW to add a security label and inject a header.
125// ---------------------------------------------------------------------------
126
127struct HeaderInjector {
128    cfg: PluginConfig,
129}
130
131#[async_trait]
132impl Plugin for HeaderInjector {
133    fn config(&self) -> &PluginConfig {
134        &self.cfg
135    }
136}
137
138impl HookHandler<CmfHook> for HeaderInjector {
139    async fn handle(
140        &self,
141        _payload: &MessagePayload,
142        extensions: &Extensions,
143        _ctx: &mut PluginContext,
144    ) -> PluginResult<MessagePayload> {
145        // Can see HTTP (has read_headers)
146        if let Some(ref http) = extensions.http {
147            println!(
148                "  [header-injector] HTTP headers visible: {:?}",
149                http.request_headers
150            );
151        }
152
153        // Can NOT see security subject (no read_subject)
154        if let Some(ref security) = extensions.security {
155            if security.subject.is_some() {
156                println!("  [header-injector] WARNING: Subject visible (unexpected!)");
157            } else {
158                println!("  [header-injector] Security subject: not visible (no read_subject)");
159            }
160        }
161
162        // COW copy to modify — tokens propagate from the executor
163        let mut modified = extensions.cow_copy();
164
165        // Add a label via MonotonicSet (has append_labels)
166        if modified.labels_write_token.is_some() {
167            modified.security.as_mut().unwrap().add_label("PROCESSED");
168            println!("  [header-injector] Added label 'PROCESSED'");
169        }
170
171        // Inject a header via Guarded (has write_headers)
172        if let Some(ref token) = modified.http_write_token {
173            modified
174                .http
175                .as_mut()
176                .unwrap()
177                .write(token)
178                .set_header("X-Processed-By", "header-injector");
179            println!("  [header-injector] Injected header 'X-Processed-By'");
180        }
181
182        PluginResult::modify_extensions(modified)
183    }
184}
185
186// ---------------------------------------------------------------------------
187// Plugin: AuditLogger
188// Has read_headers, read_security, read_labels capabilities.
189// Read-only — just logs what it can see.
190// ---------------------------------------------------------------------------
191
192struct AuditLogger {
193    cfg: PluginConfig,
194}
195
196#[async_trait]
197impl Plugin for AuditLogger {
198    fn config(&self) -> &PluginConfig {
199        &self.cfg
200    }
201}
202
203impl HookHandler<CmfHook> for AuditLogger {
204    async fn handle(
205        &self,
206        payload: &MessagePayload,
207        extensions: &Extensions,
208        _ctx: &mut PluginContext,
209    ) -> PluginResult<MessagePayload> {
210        let is_result = payload.message.is_tool_result();
211        let phase = if is_result { "POST" } else { "PRE" };
212
213        let tool_name = if is_result {
214            payload
215                .message
216                .get_tool_results()
217                .first()
218                .map(|tr| tr.tool_name.as_str())
219                .unwrap_or("unknown")
220        } else {
221            payload
222                .message
223                .get_tool_calls()
224                .first()
225                .map(|tc| tc.name.as_str())
226                .unwrap_or("unknown")
227        };
228
229        print!("  [audit-logger] AUDIT[{}]: tool='{}' ", phase, tool_name);
230
231        if let Some(ref security) = extensions.security {
232            let labels: Vec<&String> = security.labels.iter().collect();
233            print!("labels={:?} ", labels);
234        }
235
236        if let Some(ref http) = extensions.http {
237            if let Some(req_id) = http.get_header("X-Request-ID") {
238                print!("request_id='{}' ", req_id);
239            }
240        }
241
242        if is_result {
243            let is_error = payload
244                .message
245                .get_tool_results()
246                .first()
247                .map(|tr| tr.is_error)
248                .unwrap_or(false);
249            print!("error={} ", is_error);
250        }
251
252        println!();
253        PluginResult::allow()
254    }
Source

pub fn deny(violation: PluginViolation) -> Self

Deny — pipeline halts with a violation.

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 modify_payload(payload: P) -> Self

Modify payload only — extensions unchanged.

Source

pub fn modify_extensions(extensions: OwnedExtensions) -> Self

Modify extensions only — payload unchanged. Takes an OwnedExtensions from extensions.cow_copy().

Examples found in repository?
examples/cmf_capabilities_demo.rs (line 182)
139    async fn handle(
140        &self,
141        _payload: &MessagePayload,
142        extensions: &Extensions,
143        _ctx: &mut PluginContext,
144    ) -> PluginResult<MessagePayload> {
145        // Can see HTTP (has read_headers)
146        if let Some(ref http) = extensions.http {
147            println!(
148                "  [header-injector] HTTP headers visible: {:?}",
149                http.request_headers
150            );
151        }
152
153        // Can NOT see security subject (no read_subject)
154        if let Some(ref security) = extensions.security {
155            if security.subject.is_some() {
156                println!("  [header-injector] WARNING: Subject visible (unexpected!)");
157            } else {
158                println!("  [header-injector] Security subject: not visible (no read_subject)");
159            }
160        }
161
162        // COW copy to modify — tokens propagate from the executor
163        let mut modified = extensions.cow_copy();
164
165        // Add a label via MonotonicSet (has append_labels)
166        if modified.labels_write_token.is_some() {
167            modified.security.as_mut().unwrap().add_label("PROCESSED");
168            println!("  [header-injector] Added label 'PROCESSED'");
169        }
170
171        // Inject a header via Guarded (has write_headers)
172        if let Some(ref token) = modified.http_write_token {
173            modified
174                .http
175                .as_mut()
176                .unwrap()
177                .write(token)
178                .set_header("X-Processed-By", "header-injector");
179            println!("  [header-injector] Injected header 'X-Processed-By'");
180        }
181
182        PluginResult::modify_extensions(modified)
183    }
Source

pub fn modify(payload: P, extensions: OwnedExtensions) -> Self

Modify both payload and extensions. Takes an OwnedExtensions from extensions.cow_copy().

Source

pub fn is_denied(&self) -> bool

Whether this result represents a denial.

Source

pub fn is_payload_modified(&self) -> bool

Whether this result carries a modified payload.

Source

pub fn is_extensions_modified(&self) -> bool

Whether this result carries modified extensions.

Trait Implementations§

Source§

impl<P: Debug + PluginPayload> Debug for PluginResult<P>

Source§

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

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

impl<P: PluginPayload> Default for PluginResult<P>

Source§

fn default() -> Self

Returns the “default value” for a type. Read more

Auto Trait Implementations§

§

impl<P> Freeze for PluginResult<P>
where P: Freeze,

§

impl<P> RefUnwindSafe for PluginResult<P>
where P: RefUnwindSafe,

§

impl<P> Send for PluginResult<P>

§

impl<P> Sync for PluginResult<P>

§

impl<P> Unpin for PluginResult<P>
where P: Unpin,

§

impl<P> UnsafeUnpin for PluginResult<P>
where P: UnsafeUnpin,

§

impl<P> UnwindSafe for PluginResult<P>
where P: UnwindSafe,

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> 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, 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