pub fn parse_config(yaml: &str) -> Result<CpexConfig, Box<PluginError>>Expand description
Parse a CPEX config from a YAML string.
Examples found in repository?
examples/plugin_demo.rs (line 419)
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 334)
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}