Skip to main content

praxis_policy_core/
factory.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 Praxis Contributors
3
4// Plugin factory registry.
5//
6// Provides a factory pattern for creating plugin instances from
7// config. The host registers factories by `kind` name before
8// loading config. When the engine processes a config file, it
9// looks up the factory for each plugin's `kind` and calls create().
10//
11// This decouples plugin instantiation from the engine — the
12// engine doesn't know how to create a "builtin" vs "wasm"
13// The factory does.
14
15use std::collections::HashMap;
16use std::sync::Arc;
17
18use crate::error::PluginError;
19use crate::plugin::{Plugin, PluginConfig};
20use crate::registry::AnyHookHandler;
21
22/// Factory for creating plugin instances from config.
23///
24/// The host registers factories by `kind` name before loading
25/// config. When the engine processes a config file, it looks up
26/// the factory for each plugin's `kind` and calls `create()`.
27///
28/// The factory returns both the plugin and its handler because it
29/// knows the concrete types — which handler traits the plugin
30/// implements and which hooks it handles.
31///
32/// # Examples
33///
34/// ```rust,ignore
35/// struct RateLimiterFactory;
36///
37/// impl PluginFactory for RateLimiterFactory {
38///     fn create(&self, config: &PluginConfig)
39///         -> Result<PluginInstance, Box<PluginError>>
40///     {
41///         let plugin = Arc::new(RateLimiter::from_config(config)?);
42///         let handler = Arc::new(TypedHandlerAdapter::<RequestHeadersReceived, _>::new(
43///             Arc::clone(&plugin),
44///         ));
45///         Ok(PluginInstance { plugin, handler })
46///     }
47/// }
48///
49/// let mut factories = PluginFactoryRegistry::new();
50/// factories.register("security/rate_limit", Box::new(RateLimiterFactory));
51/// ```
52pub trait PluginFactory: Send + Sync {
53    /// Create a plugin instance and its handler from config.
54    ///
55    /// The `config` is the plugin's entry from the YAML file.
56    /// # Errors
57    ///
58    /// Returns `PluginError::Config` when the entry's settings are missing,
59    /// malformed, or out of range for this plugin.
60    fn create(&self, config: &PluginConfig) -> Result<PluginInstance, Box<PluginError>>;
61}
62
63/// A created plugin instance — the plugin and its type-erased handlers.
64///
65/// Each handler is paired with the hook name it handles. A plugin
66/// that implements multiple hook types (e.g., `ToolPreInvoke` and
67/// `ToolPostInvoke`) returns one entry per hook.
68pub struct PluginInstance {
69    /// The plugin implementation.
70    pub plugin: Arc<dyn Plugin>,
71
72    /// Type-erased handlers paired with their hook names.
73    /// Each entry maps a hook name to the adapter for that hook type.
74    pub handlers: Vec<(&'static str, Arc<dyn AnyHookHandler>)>,
75}
76
77/// Registry of plugin factories keyed by `kind` name.
78///
79/// The host populates this before calling `PolicyEngine::from_config()`.
80/// Each factory knows how to create plugins of a specific kind.
81///
82/// # Examples
83///
84/// ```rust,ignore
85/// let mut factories = PluginFactoryRegistry::new();
86/// factories.register("builtin/rate_limit", Box::new(RateLimiterFactory));
87/// factories.register("builtin/identity", Box::new(IdentityFactory));
88///
89/// let engine = PolicyEngine::from_config(path, &factories)?;
90/// ```
91pub struct PluginFactoryRegistry {
92    factories: HashMap<String, Arc<dyn PluginFactory>>,
93}
94
95impl PluginFactoryRegistry {
96    /// Create an empty factory registry.
97    pub fn new() -> Self {
98        Self {
99            factories: HashMap::new(),
100        }
101    }
102
103    /// Register a factory for a given `kind` name.
104    ///
105    /// Registration is last-writer-wins: re-registering an existing `kind`
106    /// overrides it (this is intentional — a host can swap a builtin's impl).
107    /// Because silent override is a footgun, a warning is logged when an
108    /// existing registration is replaced.
109    /// Takes a `Box` and stores an `Arc`. Callers keep the `Box::new(...)`
110    /// spelling; the shared handle exists so a lookup can hand back an owned
111    /// factory and let the registry lock go before the factory is invoked.
112    pub fn register(&mut self, kind: impl Into<String>, factory: Box<dyn PluginFactory>) {
113        let kind = kind.into();
114        if self
115            .factories
116            .insert(kind.clone(), Arc::from(factory))
117            .is_some()
118        {
119            tracing::warn!(kind = %kind, "plugin factory overrides an existing registration");
120        }
121    }
122
123    /// Look up a factory by `kind` name, returning an owned handle.
124    ///
125    /// Owned rather than borrowed on purpose: the engine holds this registry
126    /// behind an `RwLock`, and a borrow would keep the read guard alive across
127    /// the `create` call. `create` runs host-supplied factory code that may
128    /// re-enter the engine, and taking the write side while a read guard is
129    /// still held on the same thread deadlocks. Cloning the `Arc` lets the
130    /// caller drop the guard first.
131    pub fn get(&self, kind: &str) -> Option<Arc<dyn PluginFactory>> {
132        self.factories.get(kind).map(Arc::clone)
133    }
134
135    /// Whether a factory exists for the given `kind`.
136    pub fn has(&self, kind: &str) -> bool {
137        self.factories.contains_key(kind)
138    }
139
140    /// All registered kind names.
141    pub fn kinds(&self) -> Vec<&str> {
142        self.factories
143            .keys()
144            .map(std::string::String::as_str)
145            .collect()
146    }
147}
148
149impl Default for PluginFactoryRegistry {
150    fn default() -> Self {
151        Self::new()
152    }
153}
154
155#[cfg(test)]
156#[allow(
157    clippy::expect_used,
158    clippy::panic,
159    clippy::unwrap_used,
160    reason = "tests"
161)]
162mod tests {
163    use super::*;
164    use crate::plugin::PluginConfig;
165
166    #[derive(Debug)]
167    struct StubFactory(&'static str);
168
169    impl PluginFactory for StubFactory {
170        fn create(&self, _config: &PluginConfig) -> Result<PluginInstance, Box<PluginError>> {
171            Err(Box::new(PluginError::Config {
172                message: format!("stub {}", self.0),
173            }))
174        }
175    }
176
177    #[test]
178    fn a_registered_kind_is_found_and_an_unregistered_one_is_not() {
179        let mut reg = PluginFactoryRegistry::default();
180        assert!(!reg.has("a/b"), "an empty registry knows nothing");
181        assert!(reg.get("a/b").is_none());
182
183        reg.register("a/b", Box::new(StubFactory("first")));
184        assert!(reg.has("a/b"));
185        assert!(reg.get("a/b").is_some());
186        assert!(
187            !reg.has("c/d"),
188            "registering one kind must not answer for another"
189        );
190    }
191
192    /// Registration is last-writer-wins on purpose, so a host can swap a
193    /// builtin's implementation. The replacement has to actually take effect, or
194    /// the host's override would be silently ignored.
195    #[test]
196    fn re_registering_a_kind_replaces_the_previous_factory() {
197        let mut reg = PluginFactoryRegistry::default();
198        reg.register("a/b", Box::new(StubFactory("first")));
199        reg.register("a/b", Box::new(StubFactory("second")));
200        let factory = reg.get("a/b").expect("kind is registered");
201        // The stub reports its identity through its error message, which is the
202        // only observable difference between the two.
203        let Err(e) = factory.create(&PluginConfig::default()) else {
204            panic!("the stub always errors")
205        };
206        assert!(
207            e.to_string().contains("second"),
208            "the later registration must win: {e}"
209        );
210        assert_eq!(reg.kinds().len(), 1, "an override is not a second entry");
211    }
212
213    #[test]
214    fn kinds_lists_every_registered_name() {
215        let mut reg = PluginFactoryRegistry::default();
216        reg.register("a/b", Box::new(StubFactory("x")));
217        reg.register("c/d", Box::new(StubFactory("y")));
218        let mut kinds = reg.kinds();
219        kinds.sort_unstable();
220        assert_eq!(kinds, vec!["a/b", "c/d"]);
221    }
222
223    /// `get` hands back an owned handle rather than a borrow, so the engine can
224    /// drop its read guard before calling host-supplied `create` code. Holding
225    /// the guard across that call deadlocks if the factory re-enters the engine,
226    /// so this pins the ownership contract.
227    #[test]
228    fn get_returns_an_owned_handle_that_outlives_the_registry() {
229        let factory = {
230            let mut reg = PluginFactoryRegistry::default();
231            reg.register("a/b", Box::new(StubFactory("kept")));
232            reg.get("a/b").expect("kind is registered")
233        };
234        let Err(e) = factory.create(&PluginConfig::default()) else {
235            panic!("the stub always errors")
236        };
237        assert!(e.to_string().contains("kept"));
238    }
239}