pub struct PluginContextTable {
pub global_state: HashMap<String, Value>,
pub local_states: HashMap<Uuid, HashMap<String, Value>>,
}Expand description
Threaded execution state carried from one hook invocation to the next
within a single request lifecycle (e.g., pre_invoke → post_invoke).
The table holds the canonical pipeline state in two parts:
global_state— a single shared map across all plugins. The executor clones this into each plugin’sPluginContext.global_stateat the start of a run, then commits the plugin’s possibly-modified copy back when the run completes (last-writer-wins for serial phases).local_states— per-plugin private state, indexed by plugin ID. Persists across hook invocations so a plugin’spre_invokecan stash data itspost_invokewill read.
Storing global_state once (rather than copying it inside every per-plugin
PluginContext) makes the canonical state explicit and removes the
non-deterministic “pick an arbitrary plugin’s snapshot” pattern that was
previously needed to recover it.
Returned by the executor in PipelineResult and passed back into the
next hook call. On the first hook call pass None — the executor
creates a fresh table.
Fields§
§global_state: HashMap<String, Value>Authoritative shared state across all plugins in the pipeline.
local_states: HashMap<Uuid, HashMap<String, Value>>Per-plugin local state, indexed by plugin ID (Uuid).
Implementations§
Source§impl PluginContextTable
impl PluginContextTable
Sourcepub fn new() -> Self
pub fn new() -> Self
Create an empty context table.
Examples found in repository?
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}Sourcepub fn take_context(&mut self, plugin_id: Uuid) -> PluginContext
pub fn take_context(&mut self, plugin_id: Uuid) -> PluginContext
Build a PluginContext for the given plugin, removing its stored
local_state from the table and seeding it with a fresh clone of the
canonical global_state. Use in serial phases where the plugin will
commit its local_state changes back via [store_context].
If the plugin has no stored local_state yet, its context starts empty (first invocation in the request lifecycle).
Sourcepub fn snapshot_context(&self, plugin_id: Uuid) -> PluginContext
pub fn snapshot_context(&self, plugin_id: Uuid) -> PluginContext
Build a PluginContext for the given plugin without mutating the
table — the local_state is cloned and the global_state is cloned.
Use in read-only phases (audit, concurrent, fire-and-forget) where
per-plugin mutations should not influence subsequent plugins.
Sourcepub fn store_context(&mut self, plugin_id: Uuid, ctx: PluginContext)
pub fn store_context(&mut self, plugin_id: Uuid, ctx: PluginContext)
Commit a plugin’s context back into the table after it ran. Replaces the canonical global_state with the plugin’s possibly-modified copy (move, no clone) and stores the plugin’s local_state for next time.
Trait Implementations§
Source§impl Clone for PluginContextTable
impl Clone for PluginContextTable
Source§fn clone(&self) -> PluginContextTable
fn clone(&self) -> PluginContextTable
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read more