1use std::sync::Arc;
13
14use async_trait::async_trait;
15use cpex_core::context::PluginContext;
16use cpex_core::error::{PluginError, PluginViolation};
17use cpex_core::executor::PipelineResult;
18use cpex_core::factory::{PluginFactory, PluginInstance};
19use cpex_core::hooks::adapter::TypedHandlerAdapter;
20use cpex_core::hooks::payload::{Extensions, MetaExtension};
21use cpex_core::hooks::trait_def::{HookHandler, HookTypeDef, PluginResult};
22use cpex_core::manager::PluginManager;
23use cpex_core::plugin::{Plugin, PluginConfig};
24
25#[derive(Debug, Clone)]
31struct ToolInvokePayload {
32 tool_name: String,
33 user: String,
34 arguments: String,
35}
36cpex_core::impl_plugin_payload!(ToolInvokePayload);
37
38struct ToolPreInvoke;
40impl HookTypeDef for ToolPreInvoke {
41 type Payload = ToolInvokePayload;
42 type Result = PluginResult<ToolInvokePayload>;
43 const NAME: &'static str = "tool_pre_invoke";
44}
45
46struct ToolPostInvoke;
48impl HookTypeDef for ToolPostInvoke {
49 type Payload = ToolInvokePayload;
50 type Result = PluginResult<ToolInvokePayload>;
51 const NAME: &'static str = "tool_post_invoke";
52}
53
54struct IdentityResolver {
60 cfg: PluginConfig,
61}
62
63#[async_trait]
64impl Plugin for IdentityResolver {
65 fn config(&self) -> &PluginConfig {
66 &self.cfg
67 }
68 async fn initialize(&self) -> Result<(), Box<PluginError>> {
69 println!(" [identity-resolver] initialized");
70 Ok(())
71 }
72 async fn shutdown(&self) -> Result<(), Box<PluginError>> {
73 println!(" [identity-resolver] shutdown");
74 Ok(())
75 }
76}
77
78impl HookHandler<ToolPreInvoke> for IdentityResolver {
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
115struct PiiGuard {
117 cfg: PluginConfig,
118}
119
120#[async_trait]
121impl Plugin for PiiGuard {
122 fn config(&self) -> &PluginConfig {
123 &self.cfg
124 }
125 }
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 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
160struct AuditLogger {
162 cfg: PluginConfig,
163}
164
165#[async_trait]
166impl Plugin for AuditLogger {
167 fn config(&self) -> &PluginConfig {
168 &self.cfg
169 }
170 }
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
203struct RemoteAuthz {
225 cfg: PluginConfig,
226 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 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 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); 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 }
284}
285
286struct IdentityFactory;
291impl PluginFactory for IdentityFactory {
292 fn create(&self, config: &PluginConfig) -> Result<PluginInstance, Box<PluginError>> {
293 let plugin = Arc::new(IdentityResolver {
294 cfg: config.clone(),
295 });
296 Ok(PluginInstance {
297 plugin: plugin.clone(),
298 handlers: vec![
299 (
300 "tool_pre_invoke",
301 Arc::new(TypedHandlerAdapter::<ToolPreInvoke, _>::new(plugin.clone())),
302 ),
303 (
304 "tool_post_invoke",
305 Arc::new(TypedHandlerAdapter::<ToolPostInvoke, _>::new(plugin)),
306 ),
307 ],
308 })
309 }
310}
311
312struct PiiGuardFactory;
313impl PluginFactory for PiiGuardFactory {
314 fn create(&self, config: &PluginConfig) -> Result<PluginInstance, Box<PluginError>> {
315 let plugin = Arc::new(PiiGuard {
316 cfg: config.clone(),
317 });
318 Ok(PluginInstance {
319 plugin: plugin.clone(),
320 handlers: vec![(
321 "tool_pre_invoke",
322 Arc::new(TypedHandlerAdapter::<ToolPreInvoke, _>::new(plugin)),
323 )],
324 })
325 }
326}
327
328struct AuditLoggerFactory;
329impl PluginFactory for AuditLoggerFactory {
330 fn create(&self, config: &PluginConfig) -> Result<PluginInstance, Box<PluginError>> {
331 let plugin = Arc::new(AuditLogger {
332 cfg: config.clone(),
333 });
334 Ok(PluginInstance {
335 plugin: plugin.clone(),
336 handlers: vec![
337 (
338 "tool_pre_invoke",
339 Arc::new(TypedHandlerAdapter::<ToolPreInvoke, _>::new(plugin.clone())),
340 ),
341 (
342 "tool_post_invoke",
343 Arc::new(TypedHandlerAdapter::<ToolPostInvoke, _>::new(plugin)),
344 ),
345 ],
346 })
347 }
348}
349
350struct RemoteAuthzFactory;
355impl PluginFactory for RemoteAuthzFactory {
356 fn create(&self, config: &PluginConfig) -> Result<PluginInstance, Box<PluginError>> {
357 let plugin = Arc::new(RemoteAuthz {
358 cfg: config.clone(),
359 allowed_users: tokio::sync::RwLock::new(std::collections::HashSet::new()),
360 });
361 Ok(PluginInstance {
362 plugin: plugin.clone(),
363 handlers: vec![(
364 "tool_pre_invoke",
365 Arc::new(TypedHandlerAdapter::<ToolPreInvoke, _>::new(plugin)),
366 )],
367 })
368 }
369}
370
371fn make_tool_extensions(tool_name: &str, tags: &[&str]) -> Extensions {
376 Extensions {
377 meta: Some(Arc::new(MetaExtension {
378 entity_type: Some("tool".into()),
379 entity_name: Some(tool_name.into()),
380 tags: tags.iter().map(|s| s.to_string()).collect(),
381 ..Default::default()
382 })),
383 ..Default::default()
384 }
385}
386
387fn print_result(_label: &str, result: &PipelineResult) {
392 if result.continue_processing {
393 println!(" Result: ALLOWED");
394 } else {
395 let violation = result.violation.as_ref().unwrap();
396 println!(
397 " Result: DENIED by '{}' — {} [{}]",
398 violation.plugin_name.as_deref().unwrap_or("unknown"),
399 violation.reason,
400 violation.code,
401 );
402 }
403 println!();
404}
405
406#[tokio::main]
411async fn main() {
412 println!("=== CPEX Plugin Demo ===\n");
413
414 let config_path = "crates/cpex-core/examples/plugin_demo.yaml";
416 println!("--- Loading config from {} ---\n", config_path);
417 let yaml = std::fs::read_to_string(config_path)
418 .unwrap_or_else(|e| panic!("Failed to read {}: {}", config_path, e));
419 let cpex_config = cpex_core::config::parse_config(&yaml).unwrap();
420
421 let mgr = PluginManager::default();
422 mgr.register_factory("builtin/identity", Box::new(IdentityFactory));
423 mgr.register_factory("builtin/pii", Box::new(PiiGuardFactory));
424 mgr.register_factory("builtin/audit", Box::new(AuditLoggerFactory));
425 mgr.register_factory("builtin/remote_authz", Box::new(RemoteAuthzFactory));
426 mgr.load_config(cpex_config).unwrap();
427
428 println!("\n--- Initializing plugins ---\n");
429 mgr.initialize().await.unwrap();
430
431 println!("\nPlugins loaded: {}", mgr.plugin_count());
432 println!(
433 "Hooks registered: tool_pre_invoke={}, tool_post_invoke={}\n",
434 mgr.has_hooks_for("tool_pre_invoke"),
435 mgr.has_hooks_for("tool_post_invoke"),
436 );
437
438 println!("=== Scenario 1: get_compensation (PII tool, no clearance) ===\n");
440 let payload = ToolInvokePayload {
441 tool_name: "get_compensation".into(),
442 user: "alice".into(),
443 arguments: "employee_id=42".into(),
444 };
445 let ext = make_tool_extensions("get_compensation", &[]);
446 let (result, bg) = mgr.invoke::<ToolPreInvoke>(payload, ext, None).await;
447 print_result("get_compensation (no clearance)", &result);
448 bg.wait_for_background_tasks().await;
450
451 println!("=== Scenario 2: get_compensation (PII tool, with clearance) ===\n");
453 let payload = ToolInvokePayload {
454 tool_name: "get_compensation".into(),
455 user: "alice".into(),
456 arguments: "employee_id=42".into(),
457 };
458 let ext = make_tool_extensions("get_compensation", &[]);
459 let mut ctx_table = cpex_core::context::PluginContextTable::new();
462 ctx_table
463 .global_state
464 .insert("pii_clearance".into(), serde_json::Value::Bool(true));
465 let (result, bg) = mgr
466 .invoke::<ToolPreInvoke>(payload, ext, Some(ctx_table))
467 .await;
468 print_result("get_compensation (with clearance)", &result);
469 bg.wait_for_background_tasks().await;
470
471 println!(" --- post-invoke for get_compensation ---\n");
473 let payload = ToolInvokePayload {
474 tool_name: "get_compensation".into(),
475 user: "alice".into(),
476 arguments: "employee_id=42".into(),
477 };
478 let ext = make_tool_extensions("get_compensation", &[]);
479 let (post_result, bg) = mgr
480 .invoke::<ToolPostInvoke>(payload, ext, Some(result.context_table))
481 .await;
482 print_result("get_compensation post-invoke", &post_result);
483 bg.wait_for_background_tasks().await;
484
485 println!("=== Scenario 3: list_departments (non-PII tool) ===\n");
487 let payload = ToolInvokePayload {
488 tool_name: "list_departments".into(),
489 user: "bob".into(),
490 arguments: "".into(),
491 };
492 let ext = make_tool_extensions("list_departments", &[]);
493 let (result, bg) = mgr.invoke::<ToolPreInvoke>(payload, ext, None).await;
494 print_result("list_departments", &result);
495 bg.wait_for_background_tasks().await;
496
497 println!("=== Scenario 4: some_other_tool (wildcard route) ===\n");
499 let payload = ToolInvokePayload {
500 tool_name: "some_other_tool".into(),
501 user: "charlie".into(),
502 arguments: "foo=bar".into(),
503 };
504 let ext = make_tool_extensions("some_other_tool", &[]);
505 let (result, bg) = mgr.invoke::<ToolPreInvoke>(payload, ext, None).await;
506 print_result("some_other_tool (wildcard)", &result);
507 bg.wait_for_background_tasks().await;
508
509 println!("=== Scenario 5: query_external_data (async plugin, cache hit) ===\n");
514 let payload = ToolInvokePayload {
515 tool_name: "query_external_data".into(),
516 user: "alice".into(),
517 arguments: "dataset=sales".into(),
518 };
519 let ext = make_tool_extensions("query_external_data", &[]);
520 let (result, bg) = mgr.invoke::<ToolPreInvoke>(payload, ext, None).await;
521 print_result("query_external_data (alice — in ACL)", &result);
522 bg.wait_for_background_tasks().await;
523
524 println!("=== Scenario 6: query_external_data (async plugin, cache miss) ===\n");
529 let payload = ToolInvokePayload {
530 tool_name: "query_external_data".into(),
531 user: "charlie".into(),
532 arguments: "dataset=sales".into(),
533 };
534 let ext = make_tool_extensions("query_external_data", &[]);
535 let (result, bg) = mgr.invoke::<ToolPreInvoke>(payload, ext, None).await;
536 print_result("query_external_data (charlie — not in ACL)", &result);
537 bg.wait_for_background_tasks().await;
538
539 println!("=== Scenario 7: list_departments (no user identity) ===\n");
541 let payload = ToolInvokePayload {
542 tool_name: "list_departments".into(),
543 user: "".into(),
544 arguments: "".into(),
545 };
546 let ext = make_tool_extensions("list_departments", &[]);
547 let (result, bg) = mgr.invoke::<ToolPreInvoke>(payload, ext, None).await;
548 print_result("list_departments (no user)", &result);
549 bg.wait_for_background_tasks().await;
550
551 println!("--- Shutting down ---\n");
553 mgr.shutdown().await;
554
555 println!("=== Demo complete ===");
556}