Skip to main content

BackgroundTasks

Struct BackgroundTasks 

Source
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

Source

pub fn empty() -> Self

Create an empty set of background tasks.

Source

pub fn is_empty(&self) -> bool

Whether there are any background tasks.

Source

pub fn len(&self) -> usize

Number of background tasks.

Source

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
Hide additional 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§

Source§

impl Debug for BackgroundTasks

Source§

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

Formats the value using the given formatter. 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> 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