Skip to main content

PluginContextTable

Struct PluginContextTable 

Source
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_invokepost_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’s PluginContext.global_state at 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’s pre_invoke can stash data its post_invoke will 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

Source

pub fn new() -> Self

Create an empty context table.

Examples found in repository?
examples/plugin_demo.rs (line 461)
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}
Source

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).

Source

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.

Source

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.

Source

pub fn len(&self) -> usize

Number of plugins with stored local_state in the table.

Source

pub fn is_empty(&self) -> bool

Whether the table holds no per-plugin local_state.

Trait Implementations§

Source§

impl Clone for PluginContextTable

Source§

fn clone(&self) -> PluginContextTable

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for PluginContextTable

Source§

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

Formats the value using the given formatter. Read more
Source§

impl Default for PluginContextTable

Source§

fn default() -> PluginContextTable

Returns the “default value” for a type. Read more
Source§

impl<'de> Deserialize<'de> for PluginContextTable

Source§

fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>
where __D: Deserializer<'de>,

Deserialize this value from the given Serde deserializer. Read more
Source§

impl Serialize for PluginContextTable

Source§

fn serialize<__S>(&self, __serializer: __S) -> Result<__S::Ok, __S::Error>
where __S: Serializer,

Serialize this value into the given Serde serializer. 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> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> DeserializeOwned for T
where T: for<'de> Deserialize<'de>,

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> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
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