pub struct PolicyEngine { /* private fields */ }Expand description
Owns registered plugins and dispatches hook invocations to them.
Implementations§
Source§impl PolicyEngine
impl PolicyEngine
Sourcepub fn new(config: PolicyEngineConfig) -> PolicyEngine
pub fn new(config: PolicyEngineConfig) -> PolicyEngine
Create a new PolicyEngine with the given configuration.
Sourcepub fn config_generation(&self) -> u64
pub fn config_generation(&self) -> u64
Monotonic counter that increments on every runtime snapshot swap
(registry mutation, config (re)load). External orchestrators
(e.g. praxis-policy-apl-runtime’s dispatch-plan cache) pair their cached values
with the generation seen at build time; a mismatch on lookup
signals “evict + rebuild.” Acquire pairs with the Release
fetch_add in mutate_runtime / try_mutate_runtime so observing
a higher generation guarantees visibility of the new snapshot.
Sourcepub fn register_factory(
&self,
kind: impl Into<String>,
factory: Box<dyn PluginFactory>,
)
pub fn register_factory( &self, kind: impl Into<String>, factory: Box<dyn PluginFactory>, )
Register a plugin factory for a given kind name.
The host calls this to tell the engine how to create plugins
of a specific kind. Must be called before load_config().
§Examples
let mut engine = PolicyEngine::default();
engine.register_factory("builtin", Box::new(BuiltinFactory));
engine.register_factory("security/rate_limit", Box::new(RateLimiterFactory));
engine.load_config(Path::new("plugins.yaml"))?;Sourcepub fn load_config_file(&self, path: &Path) -> Result<(), Box<PluginError>>
pub fn load_config_file(&self, path: &Path) -> Result<(), Box<PluginError>>
Load plugins from a YAML config file.
Parses the config, looks up each plugin’s kind in the
factory registry, instantiates the plugins, and registers
them. Factories must be registered via register_factory()
before calling this method.
§Examples
let mut engine = PolicyEngine::default();
engine.register_factory("builtin", Box::new(BuiltinFactory));
engine.load_config_file(Path::new("plugins/config.yaml"))?;
engine.initialize().await?;§Errors
Returns PluginError::Config when the file cannot be read or parsed, and
whatever Self::load_config reports for the parsed contents.
Sourcepub fn load_config(
&self,
policy_config: PolicyConfig,
) -> Result<(), Box<PluginError>>
pub fn load_config( &self, policy_config: PolicyConfig, ) -> Result<(), Box<PluginError>>
Load plugins from a parsed config.
Looks up each plugin’s kind in the factory registry,
instantiates the plugins, and registers them with their
hook names from the config.
§Errors
Returns PluginError::Config when a plugin’s kind has no registered
factory, when a factory rejects the plugin’s config, or when a
registration conflicts with one already present. The existing snapshot is
left in place, so a failed load does not disturb in-flight requests.
Sourcepub fn register_visitor(&self, visitor: Arc<dyn ConfigVisitor>)
pub fn register_visitor(&self, visitor: Arc<dyn ConfigVisitor>)
Register an external config visitor. Visitors run during
load_config_yaml (after plugin instantiation) and can install
per-route handler overrides via annotate_route. Visitor order
matches registration order. Multiple visitors are allowed —
they typically don’t share state, so order rarely matters.
Sourcepub fn load_config_yaml(
self: &Arc<PolicyEngine>,
yaml: &str,
) -> Result<(), Box<PluginError>>
pub fn load_config_yaml( self: &Arc<PolicyEngine>, yaml: &str, ) -> Result<(), Box<PluginError>>
Load a unified-config YAML string. Parses the YAML twice — once
into a typed PolicyConfig for plugin instantiation, once into a
raw serde_yaml::Value so visitors can inspect orchestrator-
specific blocks (e.g. apl:) that praxis-policy-core itself doesn’t
model. Calls existing load_config(policy_config) first, then
walks each registered visitor over the raw YAML’s sections in
the documented hierarchy order:
visit_global(global_yaml)visit_default(entity_type, default_yaml)perglobal.defaultsentryvisit_policy_bundle(tag, bundle_yaml)perglobal.policiesentryvisit_route(route_yaml, parsed_route)perroutes[]entry
All sections for one visitor run before the next visitor starts,
giving each visitor a consistent view of its own accumulated
state. A visitor returning Err aborts the load — the plugin
snapshot stays at the post-load_config state (partial load is
not rolled back; operators should treat any error from this
method as a hard stop).
§Errors
Returns PluginError::Config when the YAML does not parse, when it does
not deserialize into a policy document, when plugin loading fails as in
Self::load_config, or when a config visitor rejects a section. A
visitor error aborts the load and is not rolled back: treat it as a hard
stop rather than retrying on top of it.
Sourcepub fn from_config(
policy_config: PolicyConfig,
factories: &PluginFactoryRegistry,
) -> Result<PolicyEngine, Box<PluginError>>
pub fn from_config( policy_config: PolicyConfig, factories: &PluginFactoryRegistry, ) -> Result<PolicyEngine, Box<PluginError>>
Create a PolicyEngine from a parsed config (convenience).
Uses the passed factory registry for initial instantiation.
Note: for route-level config overrides to create new instances
at runtime, use register_factory() + load_config() instead
so the engine owns the factories.
§Errors
Returns PluginError::Config for the same reasons as
Self::load_config: an unknown plugin kind, a factory that rejects
its config, or a conflicting registration.
Sourcepub fn register_handler<H, P>(
&self,
plugin: Arc<P>,
config: PluginConfig,
) -> Result<(), Box<PluginError>>where
H: HookTypeDef,
<H as HookTypeDef>::Result: Into<PluginResult<<H as HookTypeDef>::Payload>>,
P: Plugin + HookHandler<H> + 'static,
pub fn register_handler<H, P>(
&self,
plugin: Arc<P>,
config: PluginConfig,
) -> Result<(), Box<PluginError>>where
H: HookTypeDef,
<H as HookTypeDef>::Result: Into<PluginResult<<H as HookTypeDef>::Payload>>,
P: Plugin + HookHandler<H> + 'static,
Register a plugin handler for its primary hook name.
This is the preferred registration method. The framework creates
the type-erased adapter internally — no AnyHookHandler needed.
§Type Parameters
H— the hook type (implementsHookTypeDef).P— the plugin type (implementsPlugin + HookHandler<H>).
§Arguments
plugin— the plugin implementation.config— authoritative config from the config loader.
§Examples
engine.register_handler::<CmfHook, _>(plugin, config)?;§Errors
Returns PluginError::Config when a plugin of the same name is already
registered for this hook.
Sourcepub fn register_handler_for_names<H, P>(
&self,
plugin: Arc<P>,
config: PluginConfig,
names: &[&str],
) -> Result<(), Box<PluginError>>where
H: HookTypeDef,
<H as HookTypeDef>::Result: Into<PluginResult<<H as HookTypeDef>::Payload>>,
P: Plugin + HookHandler<H> + 'static,
pub fn register_handler_for_names<H, P>(
&self,
plugin: Arc<P>,
config: PluginConfig,
names: &[&str],
) -> Result<(), Box<PluginError>>where
H: HookTypeDef,
<H as HookTypeDef>::Result: Into<PluginResult<<H as HookTypeDef>::Payload>>,
P: Plugin + HookHandler<H> + 'static,
Register a plugin handler for multiple hook names.
This is the CMF pattern — one handler covers multiple hook
names (cmf.tool_pre_invoke, cmf.llm_input, etc.).
§Examples
engine.register_handler_for_names::<CmfHook, _>(
plugin, config,
&["cmf.tool_pre_invoke", "cmf.llm_input", "cmf.llm_output"],
)?;§Errors
Returns PluginError::Config when a plugin of the same name is already
registered under any of the given hook names.
Sourcepub fn register_raw<H>(
&self,
plugin: Arc<dyn Plugin>,
config: PluginConfig,
handler: Arc<dyn AnyHookHandler>,
) -> Result<(), Box<PluginError>>where
H: HookTypeDef,
pub fn register_raw<H>(
&self,
plugin: Arc<dyn Plugin>,
config: PluginConfig,
handler: Arc<dyn AnyHookHandler>,
) -> Result<(), Box<PluginError>>where
H: HookTypeDef,
Register with an explicit AnyHookHandler (advanced use).
For cases where the automatic adapter doesn’t fit — e.g.,
Python/WASM bridge hosts that implement AnyHookHandler directly.
Most callers should use register_handler instead.
§Errors
Returns PluginError::Config when a plugin of the same name is already
registered for this hook.
Sourcepub async fn initialize(&self) -> Result<(), Box<PluginError>>
pub async fn initialize(&self) -> Result<(), Box<PluginError>>
Initialize all registered plugins.
Calls plugin.initialize() on each registered plugin. Must be
called before invoking any hooks. Idempotent — calling twice
has no effect.
§Errors
Returns PluginError::Execution when a plugin’s initialize fails.
Plugins already initialized in this call are shut down first, so the
engine does not come up half-started.
Sourcepub async fn shutdown(&self)
pub async fn shutdown(&self)
Shutdown all registered plugins.
Calls plugin.shutdown() on each registered plugin in reverse
registration order. Errors are logged but do not halt the
shutdown process — all plugins get a chance to clean up.
Shut the engine down. Terminal: after shutdown() returns,
no further register_* / invoke_* should be called. New
fire-and-forget tasks spawned after close() will not be tracked
(the TaskTracker is single-shot by design).
Sourcepub async fn invoke_by_name(
&self,
hook_name: &str,
payload: Box<dyn PluginPayload>,
extensions: Extensions,
context_table: Option<PluginContextTable>,
) -> (PipelineResult, BackgroundTasks)
pub async fn invoke_by_name( &self, hook_name: &str, payload: Box<dyn PluginPayload>, extensions: Extensions, context_table: Option<PluginContextTable>, ) -> (PipelineResult, BackgroundTasks)
Invoke a hook by name with a type-erased payload.
This is the dynamic dispatch path used by Python/Go/WASM
callers via FFI or PyO3 bindings. The hook name is resolved
from the registry and dispatched through the 5-phase executor.
§Arguments
hook_name— the hook name string (e.g.,"cmf.tool_pre_invoke").payload— the payload asBox<dyn PluginPayload>.extensions— the full extensions (filtered per plugin by the executor).context_table— optional context table from a previous hook invocation. PassNoneon the first hook call; thread the returned table into subsequent calls to preserve per-plugin state.
§Returns
A tuple of (PipelineResult, BackgroundTasks). The result
contains the final payload, extensions, violation, and context
table. Background tasks can be awaited or dropped.
Sourcepub async fn invoke<H>(
&self,
payload: <H as HookTypeDef>::Payload,
extensions: Extensions,
context_table: Option<PluginContextTable>,
) -> (PipelineResult, BackgroundTasks)where
H: HookTypeDef,
pub async fn invoke<H>(
&self,
payload: <H as HookTypeDef>::Payload,
extensions: Extensions,
context_table: Option<PluginContextTable>,
) -> (PipelineResult, BackgroundTasks)where
H: HookTypeDef,
Invoke a typed hook.
This is the compile-time dispatch path used by Rust callers.
The hook type H determines the payload and result types.
Dispatch goes through the same registry and 5-phase executor
as invoke_by_name().
When routing is enabled, the entity is identified from
extensions.meta (entity_type + entity_name). Only plugins
matching the resolved route fire. When routing is disabled
or meta is absent, all registered plugins fire.
§Type Parameters
H— the hook type (implementsHookTypeDef).
§Arguments
payload— the typed payload.extensions— the full extensions (includes meta for routing).context_table— optional context table from a previous hook.
§Returns
A tuple of (PipelineResult, BackgroundTasks).
Sourcepub async fn invoke_named<H>(
&self,
hook_name: &str,
payload: <H as HookTypeDef>::Payload,
extensions: Extensions,
context_table: Option<PluginContextTable>,
) -> (PipelineResult, BackgroundTasks)where
H: HookTypeDef,
pub async fn invoke_named<H>(
&self,
hook_name: &str,
payload: <H as HookTypeDef>::Payload,
extensions: Extensions,
context_table: Option<PluginContextTable>,
) -> (PipelineResult, BackgroundTasks)where
H: HookTypeDef,
Invoke a typed hook by explicit name.
Combines compile-time payload type checking (from H) with
runtime hook name routing (from hook_name). Use this when
a single hook type (e.g., CmfHook) covers multiple hook
names (e.g., cmf.tool_pre_invoke, cmf.tool_post_invoke).
§Type Parameters
H— the hook type (provides payload type checking).
§Arguments
hook_name— the hook name for dispatch routing.payload— the typed payload (compile-time checked againstH::Payload).extensions— the full extensions.context_table— optional context table from a previous hook.
§Examples
// Compile-time: payload must be MessagePayload (from CmfHook)
// Runtime: dispatches to plugins registered under "cmf.tool_pre_invoke"
let (result, bg) = mgr.invoke_named::<CmfHook>(
"cmf.tool_pre_invoke", payload, ext, None,
).await;Sourcepub fn find_plugin_entries(&self, plugin_name: &str) -> Vec<(String, HookEntry)>
pub fn find_plugin_entries(&self, plugin_name: &str) -> Vec<(String, HookEntry)>
Find every (hook_name, HookEntry) pair belonging to the named
plugin. Returns an empty Vec if the plugin isn’t registered.
Used by external orchestrators (notably praxis-policy-apl-runtime) that decide
the per-route plugin lineup themselves and need handler refs +
trusted_config to build pre-resolved dispatch plans. Cheaper than
going through invoke_named per request because the caller can
cache the resulting entries — pair the result with
config_generation to invalidate the
cache on snapshot swaps.
Bypasses route/entity filtering — caller has already decided this
plugin should run. APL’s routes: is itself the authoritative
lineup; praxis-policy-core’s condition-based routing is a parallel model
for non-APL hosts.
Sourcepub async fn invoke_entries<H>(
&self,
entries: &[HookEntry],
payload: <H as HookTypeDef>::Payload,
extensions: Extensions,
context_table: Option<PluginContextTable>,
) -> (PipelineResult, BackgroundTasks)where
H: HookTypeDef,
pub async fn invoke_entries<H>(
&self,
entries: &[HookEntry],
payload: <H as HookTypeDef>::Payload,
extensions: Extensions,
context_table: Option<PluginContextTable>,
) -> (PipelineResult, BackgroundTasks)where
H: HookTypeDef,
Dispatch a caller-supplied slice of HookEntries through the
executor’s full 5-phase pipeline (sequential, transform, audit,
concurrent, fire-and-forget). All on_error / timeout / mode /
write-token machinery applies.
Bypasses hook-name lookup and route/entity filtering — caller has
already resolved the lineup (typically via
find_plugin_entries + a per-route
dispatch plan). The H: HookTypeDef parameter enforces payload
type at compile time; mismatched payloads fail to compile, same
as invoke_named.
Returns (PipelineResult, BackgroundTasks) identical in shape to
invoke_named so callers can swap between the two paths without
rewriting downstream result handling.
Sourcepub fn annotate_route<H>(
&self,
entity_type: impl Into<String>,
entity_name: impl Into<String>,
scope: Option<String>,
hook_name: impl Into<String>,
handler: Arc<H>,
config: PluginConfig,
)where
H: Plugin + AnyHookHandler + 'static,
pub fn annotate_route<H>(
&self,
entity_type: impl Into<String>,
entity_name: impl Into<String>,
scope: Option<String>,
hook_name: impl Into<String>,
handler: Arc<H>,
config: PluginConfig,
)where
H: Plugin + AnyHookHandler + 'static,
Override the resolved plugin list for one (entity_type, entity_name)
pair on the listed hooks with a single synthetic handler. The handler
takes responsibility for any further plugin dispatch within itself
(typically by calling invoke_entries against
the same registry’s other entries — i.e. APL’s plugin(name) →
CmfPluginInvoker → invoke_entries flow).
This is the integration point external orchestrators (APL, future
Rego/Cedar-direct/Custom) use to drive plugins via their own
semantics instead of praxis-policy-core’s imperative routes.*.plugins:
chain. Bumps the config generation so cached dispatch plans in
downstream caches invalidate.
config provides the trusted_config for the synthetic plugin —
the executor reads mode, on_error, capabilities, etc. from
it the same way it does for any other registered plugin. Capabilities
should be a superset of what the orchestrator needs to read from
Extensions (praxis-policy-core’s per-plugin filter still applies to the
synthetic handler).
The underlying plugins: chain for this route is not removed —
those plugins stay discoverable via find_plugin_entries
so the orchestrator can dispatch into them by name.
Sourcepub fn remove_route_annotation(
&self,
entity_type: &str,
entity_name: &str,
scope: Option<&str>,
hook_name: &str,
)
pub fn remove_route_annotation( &self, entity_type: &str, entity_name: &str, scope: Option<&str>, hook_name: &str, )
Remove a route annotation for a specific hook. No-op when no annotation exists for the key. Bumps the generation so downstream caches invalidate.
Sourcepub async fn build_override_entries(
&self,
plugin_name: &str,
config_override: Option<&Value>,
capabilities_override: Option<&HashSet<String>>,
on_error_override: Option<OnError>,
) -> Vec<(String, HookEntry)>
pub async fn build_override_entries( &self, plugin_name: &str, config_override: Option<&Value>, capabilities_override: Option<&HashSet<String>>, on_error_override: Option<OnError>, ) -> Vec<(String, HookEntry)>
Build per-hook HookEntrys for a plugin with optional route-
level overrides. Used by external orchestrators (notably
praxis-policy-apl-runtime’s dispatch plan) that need to splice per-route plugin
variants — different config, narrower capabilities, different
on_error — into the dispatch lineup while keeping praxis-policy-core
the source of truth for instantiation and isolation.
Behavior:
- All three overrides
None: returns the base entries unchanged. Caller can use them as-is. - Only
capabilities_override/on_error_overrideset (config_overrideisNone): builds newPluginRefs sharing the base pluginArcwith a mergedTrustedConfig(override caps /on_errorreplace base values) and an independent circuit breaker. Cheap — no factory call. config_overrideset: invokes the registered factory for the plugin’skindwith a mergedPluginConfig(overrideconfigreplaces baseconfigwholesale per unified-config spec — not deep merge), callsinitialize()on the new instance, and wraps every returned handler in a newPluginRefwith a fresh circuit breaker.
Returns an empty Vec when:
- the plugin name isn’t registered in the engine,
- the factory for the plugin’s
kindis missing, - the factory’s
createerrors, - or
initialize()fails on the new instance.
Each of those is a configuration / wiring fault the caller
should treat as NotFound at dispatch time. The method logs
the underlying error before returning empty so debugging
surfaces in operator logs rather than as a silent miss.
Sourcepub fn clear_routing_cache(&self)
pub fn clear_routing_cache(&self)
Clear the routing cache. Call when config is reloaded or plugins are registered/unregistered. Also resets the “cache full” warn-once latch so the next fill cycle can warn again.
Sourcepub fn routing_cache_size(&self) -> usize
pub fn routing_cache_size(&self) -> usize
Number of entries in the routing cache.
Sourcepub fn has_hooks_for(&self, hook_name: &str) -> bool
pub fn has_hooks_for(&self, hook_name: &str) -> bool
Whether anything would run for the given hook name — either a registered plugin handler OR a route annotation targeting that hook.
Route annotations (installed by APL from a route’s policy: /
args: / result: blocks) must be counted here: a route whose only
handler for a phase is an annotation (e.g. a response-side
result: { ssn: redact(...) } on cmf.tool_post_invoke, with no
globally-registered post-invoke plugin) would otherwise report
“no hooks” and be skipped by out-of-process hosts that use this as a
fast-skip gate — silently dropping the route’s policy for that phase.
Sourcepub fn get_plugin(&self, name: &str) -> Option<Arc<PluginRef>>
pub fn get_plugin(&self, name: &str) -> Option<Arc<PluginRef>>
Look up a plugin by name. Returns an Arc<PluginRef> clone — works
with the snapshot-based dispatch model where the registry sits
behind a transient Arc<RuntimeSnapshot> guard. Arc<PluginRef>
derefs to PluginRef, so callers can chain methods directly:
mgr.get_plugin("name").unwrap().is_disabled() still compiles.
Sourcepub fn plugin_count(&self) -> usize
pub fn plugin_count(&self) -> usize
Total number of registered plugins.
Sourcepub fn plugin_names(&self) -> Vec<String>
pub fn plugin_names(&self) -> Vec<String>
All registered plugin names (owned, not borrowed from the registry).
Sourcepub fn is_initialized(&self) -> bool
pub fn is_initialized(&self) -> bool
Whether the engine has been initialized.
Trait Implementations§
Source§impl Default for PolicyEngine
impl Default for PolicyEngine
Source§fn default() -> PolicyEngine
fn default() -> PolicyEngine
Source§impl PluginModeLookup for PolicyEngine
impl PluginModeLookup for PolicyEngine
Auto Trait Implementations§
impl !Freeze for PolicyEngine
impl !RefUnwindSafe for PolicyEngine
impl !UnwindSafe for PolicyEngine
impl Send for PolicyEngine
impl Sync for PolicyEngine
impl Unpin for PolicyEngine
impl UnsafeUnpin for PolicyEngine
Blanket Implementations§
Source§impl<T> AnyExt for T
impl<T> AnyExt for T
Source§fn downcast_ref<T>(this: &Self) -> Option<&T>where
T: Any,
fn downcast_ref<T>(this: &Self) -> Option<&T>where
T: Any,
T behind referenceSource§fn downcast_mut<T>(this: &mut Self) -> Option<&mut T>where
T: Any,
fn downcast_mut<T>(this: &mut Self) -> Option<&mut T>where
T: Any,
T behind mutable referenceSource§fn downcast_rc<T>(this: Rc<Self>) -> Result<Rc<T>, Rc<Self>>where
T: Any,
fn downcast_rc<T>(this: Rc<Self>) -> Result<Rc<T>, Rc<Self>>where
T: Any,
T behind Rc pointerSource§fn downcast_arc<T>(this: Arc<Self>) -> Result<Arc<T>, Arc<Self>>where
T: Any,
fn downcast_arc<T>(this: Arc<Self>) -> Result<Arc<T>, Arc<Self>>where
T: Any,
T behind Arc pointerSource§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
Source§impl<T, X> CoerceTo<T> for Xwhere
T: CoerceFrom<X> + ?Sized,
impl<T, X> CoerceTo<T> for Xwhere
T: CoerceFrom<X> + ?Sized,
fn coerce_rc_to(self: Rc<X>) -> Rc<T>
fn coerce_box_to(self: Box<X>) -> Box<T>
fn coerce_ref_to(&self) -> &T
fn coerce_mut_to(&mut self) -> &mut T
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more