pub struct BackgroundTasks { /* private fields */ }Expand description
Handles to fire-and-forget background tasks spawned by the executor.
Returned separately from PipelineResult so that the policy
result stays immutable. If not awaited, tasks complete on their
own in the background. Call wait_for_background_tasks() when you
need to ensure tasks have finished (tests, graceful shutdown,
audit flush).
Implementations§
Source§impl BackgroundTasks
impl BackgroundTasks
Sourcepub async fn wait_for_background_tasks(self) -> Vec<PluginError>
pub async fn wait_for_background_tasks(self) -> Vec<PluginError>
Wait for all fire-and-forget background tasks to complete.
Returns a list of errors from any tasks that panicked. An empty list means all tasks completed successfully.
Consumes self — each task handle can only be awaited once.
If not called, background tasks still complete on their own. Use this for tests, graceful shutdown, or when you need to ensure audit/logging tasks have flushed before proceeding.
Examples found in repository?
examples/plugin_demo.rs (line 449)
411async fn main() {
412 println!("=== CPEX Plugin Demo ===\n");
413
414 // --- Load config from YAML file ---
415 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 // --- Scenario 1: PII tool without clearance ---
439 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 // Wait for any fire-and-forget tasks
449 bg.wait_for_background_tasks().await;
450
451 // --- Scenario 2: PII tool with clearance ---
452 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 // Simulate clearance by pre-populating global_state
460 // (In production, an earlier hook would set this from a token claim)
461 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 // Now call post-invoke — threads the context table from pre-invoke
472 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 // --- Scenario 3: Non-PII tool ---
486 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 // --- Scenario 4: Unknown tool (wildcard route) ---
498 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 // --- Scenario 5: Awaiting plugin — cache hit ---
510 // RemoteAuthz's `handle` is `async fn` and reads from a tokio
511 // RwLock. Its initialize() pre-loaded an ACL containing "alice"
512 // and "bob"; this call exercises the cache-hit fast path.
513 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 // --- Scenario 6: Awaiting plugin — cache miss path with .await ---
525 // "charlie" is not in the cached ACL, so RemoteAuthz takes the
526 // cache-miss branch and `.await`s a simulated remote call before
527 // denying.
528 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 // --- Scenario 7: No user identity ---
540 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 // --- Shutdown ---
552 println!("--- Shutting down ---\n");
553 mgr.shutdown().await;
554
555 println!("=== Demo complete ===");
556}More examples
examples/cmf_capabilities_demo.rs (line 430)
326async fn main() {
327 println!("=== CMF Capabilities Demo ===\n");
328
329 // Load config from YAML file — capabilities declared per plugin
330 let config_path = "crates/cpex-core/examples/cmf_capabilities_demo.yaml";
331 println!("--- Loading config from {} ---\n", config_path);
332 let yaml = std::fs::read_to_string(config_path)
333 .unwrap_or_else(|e| panic!("Failed to read {}: {}", config_path, e));
334 let cpex_config = cpex_core::config::parse_config(&yaml).unwrap();
335
336 let mgr = PluginManager::default();
337 mgr.register_factory("builtin/identity-checker", Box::new(IdentityCheckerFactory));
338 mgr.register_factory("builtin/header-injector", Box::new(HeaderInjectorFactory));
339 mgr.register_factory("builtin/audit-logger", Box::new(AuditLoggerFactory));
340 mgr.load_config(cpex_config).unwrap();
341 mgr.initialize().await.unwrap();
342
343 // --- Build CMF Message ---
344 let payload = MessagePayload {
345 message: Message {
346 schema_version: cpex_core::cmf::constants::SCHEMA_VERSION.into(),
347 role: Role::Assistant,
348 content: vec![
349 ContentPart::Text {
350 text: "Looking up compensation.".into(),
351 },
352 ContentPart::ToolCall {
353 content: ToolCall {
354 tool_call_id: "tc_001".into(),
355 name: "get_compensation".into(),
356 arguments: [("employee_id".to_string(), serde_json::json!(42))].into(),
357 namespace: None,
358 },
359 },
360 ],
361 channel: None,
362 },
363 };
364
365 // --- Build Extensions with security, HTTP, meta ---
366 let mut security = SecurityExtension::default();
367 security.add_label("PII");
368 security.add_label("HR_DATA");
369 security.classification = Some("confidential".into());
370 security.subject = Some(cpex_core::extensions::security::SubjectExtension {
371 id: Some("alice".into()),
372 subject_type: Some(cpex_core::extensions::security::SubjectType::User),
373 roles: ["hr_admin".to_string()].into(),
374 permissions: ["read_compensation".to_string()].into(),
375 ..Default::default()
376 });
377
378 let mut http = HttpExtension::default();
379 http.set_header("Authorization", "Bearer eyJ...");
380 http.set_header("X-Request-ID", "req-abc-123");
381
382 let ext = Extensions {
383 request: Some(Arc::new(RequestExtension {
384 environment: Some("production".into()),
385 request_id: Some("req-abc-123".into()),
386 ..Default::default()
387 })),
388 security: Some(Arc::new(security)),
389 http: Some(Arc::new(http)),
390 meta: Some(Arc::new(MetaExtension {
391 entity_type: Some("tool".into()),
392 entity_name: Some("get_compensation".into()),
393 tags: ["pii".to_string(), "hr".to_string()].into(),
394 ..Default::default()
395 })),
396 ..Default::default()
397 };
398
399 // --- Pre-invoke: type-safe dispatch via invoke_named ---
400 println!("=== Phase 1: cmf.tool_pre_invoke ===\n");
401
402 // invoke_named<CmfHook> gives compile-time payload type checking
403 // while routing to the specific "cmf.tool_pre_invoke" hook name
404 let (pre_result, bg) = mgr
405 .invoke_named::<CmfHook>(
406 "cmf.tool_pre_invoke",
407 payload,
408 ext,
409 None, // first hook — no context table
410 )
411 .await;
412
413 println!();
414 if pre_result.continue_processing {
415 println!("Pre-invoke result: ALLOWED");
416 if let Some(ref modified_ext) = pre_result.modified_extensions {
417 if let Some(ref sec) = modified_ext.security {
418 let labels: Vec<&String> = sec.labels.iter().collect();
419 println!(" Labels after pre-invoke: {:?}", labels);
420 }
421 if let Some(ref http) = modified_ext.http {
422 println!(" Headers after pre-invoke: {:?}", http.request_headers);
423 }
424 }
425 } else {
426 println!(
427 "Pre-invoke result: DENIED — {}",
428 pre_result.violation.as_ref().unwrap().reason
429 );
430 bg.wait_for_background_tasks().await;
431 println!("\n=== Demo complete ===");
432 return;
433 }
434 bg.wait_for_background_tasks().await;
435
436 // --- Simulate tool execution ---
437 println!("\n--- Tool 'get_compensation' executes... ---");
438 println!(" Result: {{\"salary\": 150000, \"currency\": \"USD\"}}\n");
439
440 // --- Post-invoke: different CMF message with tool result ---
441 println!("=== Phase 2: cmf.tool_post_invoke ===\n");
442
443 let post_payload = MessagePayload {
444 message: Message {
445 schema_version: cpex_core::cmf::constants::SCHEMA_VERSION.into(),
446 role: Role::Tool,
447 content: vec![ContentPart::ToolResult {
448 content: cpex_core::cmf::ToolResult {
449 tool_call_id: "tc_001".into(),
450 tool_name: "get_compensation".into(),
451 content: serde_json::json!({"salary": 150000, "currency": "USD"}),
452 is_error: false,
453 },
454 }],
455 channel: None,
456 },
457 };
458
459 // Build post-invoke extensions — carry forward any modifications
460 // from pre-invoke via the context table
461 let post_ext = pre_result.modified_extensions.unwrap_or_else(|| {
462 // Rebuild if no modifications
463 let mut security = SecurityExtension::default();
464 security.add_label("PII");
465 Extensions {
466 security: Some(Arc::new(security)),
467 meta: Some(Arc::new(MetaExtension {
468 entity_type: Some("tool".into()),
469 entity_name: Some("get_compensation".into()),
470 ..Default::default()
471 })),
472 ..Default::default()
473 }
474 });
475
476 // Thread the context table from pre-invoke to preserve plugin state
477 let (post_result, post_bg) = mgr
478 .invoke_named::<CmfHook>(
479 "cmf.tool_post_invoke",
480 post_payload,
481 post_ext,
482 Some(pre_result.context_table),
483 )
484 .await;
485
486 println!();
487 if post_result.continue_processing {
488 println!("Post-invoke result: ALLOWED");
489 } else {
490 println!(
491 "Post-invoke result: DENIED — {}",
492 post_result.violation.as_ref().unwrap().reason
493 );
494 }
495
496 post_bg.wait_for_background_tasks().await;
497 println!("\n=== Demo complete ===");
498}Trait Implementations§
Auto Trait Implementations§
impl Freeze for BackgroundTasks
impl RefUnwindSafe for BackgroundTasks
impl Send for BackgroundTasks
impl Sync for BackgroundTasks
impl Unpin for BackgroundTasks
impl UnsafeUnpin for BackgroundTasks
impl UnwindSafe for BackgroundTasks
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Mutably borrows from an owned value. Read more