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}