1use std::collections::{HashMap, HashSet};
13use std::fmt;
14use std::future::Future;
15use std::pin::Pin;
16use std::sync::{Arc, LazyLock, Mutex, OnceLock, RwLock};
17
18use serde::{Deserialize, Serialize};
19use serde_json::{Map, Value as Json};
20use thiserror::Error;
21
22use crate::api::registry::{
23 deregister_llm_conditional_execution_guardrail, deregister_llm_execution_intercept,
24 deregister_llm_request_intercept, deregister_llm_sanitize_request_guardrail,
25 deregister_llm_sanitize_response_guardrail, deregister_llm_stream_execution_intercept,
26 deregister_tool_conditional_execution_guardrail, deregister_tool_execution_intercept,
27 deregister_tool_request_intercept, deregister_tool_sanitize_request_guardrail,
28 deregister_tool_sanitize_response_guardrail, register_llm_conditional_execution_guardrail,
29 register_llm_execution_intercept, register_llm_request_intercept,
30 register_llm_sanitize_request_guardrail, register_llm_sanitize_response_guardrail,
31 register_llm_stream_execution_intercept, register_tool_conditional_execution_guardrail,
32 register_tool_execution_intercept, register_tool_request_intercept,
33 register_tool_sanitize_request_guardrail, register_tool_sanitize_response_guardrail,
34};
35use crate::api::runtime::{
36 EventSubscriberFn, LlmConditionalFn, LlmExecutionFn, LlmRequestInterceptFn,
37 LlmSanitizeRequestFn, LlmSanitizeResponseFn, LlmStreamExecutionFn, ToolConditionalFn,
38 ToolExecutionFn, ToolInterceptFn, ToolSanitizeFn,
39};
40use crate::api::subscriber::{deregister_subscriber, register_subscriber};
41
42type PluginMap = HashMap<String, Arc<dyn Plugin>>;
43
44static PLUGIN_HANDLERS: LazyLock<RwLock<PluginMap>> = LazyLock::new(|| RwLock::new(HashMap::new()));
45static ACTIVE_PLUGIN_CONFIGURATION: LazyLock<Mutex<Option<ActivePluginConfiguration>>> =
46 LazyLock::new(|| Mutex::new(None));
47static BUILTIN_PLUGIN_REGISTRATION: OnceLock<Result<()>> = OnceLock::new();
48
49#[derive(Debug, Error)]
51pub enum PluginError {
52 #[error("invalid config: {0}")]
54 InvalidConfig(String),
55
56 #[error("not found: {0}")]
58 NotFound(String),
59
60 #[error("serialization error: {0}")]
62 Serialization(#[from] serde_json::Error),
63
64 #[error("internal error: {0}")]
66 Internal(String),
67
68 #[error("registration failed: {0}")]
70 RegistrationFailed(String),
71}
72
73pub type Result<T> = std::result::Result<T, PluginError>;
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
78#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
79pub struct PluginConfig {
80 #[serde(default = "default_plugin_config_version")]
82 pub version: u32,
83 #[serde(default)]
85 pub components: Vec<PluginComponentSpec>,
86 #[serde(default)]
88 pub policy: ConfigPolicy,
89}
90
91impl Default for PluginConfig {
92 fn default() -> Self {
93 Self {
94 version: default_plugin_config_version(),
95 components: vec![],
96 policy: ConfigPolicy::default(),
97 }
98 }
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
103#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
104pub struct PluginComponentSpec {
105 pub kind: String,
107 #[serde(default = "default_enabled")]
112 pub enabled: bool,
113 #[serde(default)]
115 pub config: Map<String, Json>,
116}
117
118impl PluginComponentSpec {
119 pub fn new(kind: impl Into<String>) -> Self {
121 Self {
122 kind: kind.into(),
123 enabled: true,
124 config: Map::new(),
125 }
126 }
127}
128
129#[derive(Debug, Clone, Default, Serialize, Deserialize)]
131#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
132pub struct ConfigReport {
133 #[serde(default)]
135 pub diagnostics: Vec<ConfigDiagnostic>,
136}
137
138impl ConfigReport {
139 pub fn has_errors(&self) -> bool {
141 self.diagnostics
142 .iter()
143 .any(|diag| diag.level == DiagnosticLevel::Error)
144 }
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
149#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
150pub struct ConfigDiagnostic {
151 pub level: DiagnosticLevel,
153 pub code: String,
155 #[serde(default, skip_serializing_if = "Option::is_none")]
157 pub component: Option<String>,
158 #[serde(default, skip_serializing_if = "Option::is_none")]
160 pub field: Option<String>,
161 pub message: String,
163}
164
165#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
167#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
168#[serde(rename_all = "lowercase")]
169pub enum DiagnosticLevel {
170 Warning,
172 Error,
174}
175
176#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
178#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
179pub struct ConfigPolicy {
180 #[serde(default = "default_warn")]
182 pub unknown_component: UnsupportedBehavior,
183 #[serde(default = "default_warn")]
185 pub unknown_field: UnsupportedBehavior,
186 #[serde(default = "default_error")]
188 pub unsupported_value: UnsupportedBehavior,
189}
190
191impl Default for ConfigPolicy {
192 fn default() -> Self {
193 Self {
194 unknown_component: default_warn(),
195 unknown_field: default_warn(),
196 unsupported_value: default_error(),
197 }
198 }
199}
200
201crate::editor_config! {
202 impl ConfigPolicy {
203 unknown_component => {
204 label: "unknown_component",
205 kind: Enum,
206 values: ["warn", "ignore", "error"],
207 },
208 unknown_field => {
209 label: "unknown_field",
210 kind: Enum,
211 values: ["warn", "ignore", "error"],
212 },
213 unsupported_value => {
214 label: "unsupported_value",
215 kind: Enum,
216 values: ["warn", "ignore", "error"],
217 },
218 }
219}
220
221#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
223#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
224#[serde(rename_all = "lowercase")]
225pub enum UnsupportedBehavior {
226 Ignore,
228 #[default]
230 Warn,
231 Error,
233}
234
235fn default_warn() -> UnsupportedBehavior {
236 UnsupportedBehavior::Warn
237}
238
239fn default_error() -> UnsupportedBehavior {
240 UnsupportedBehavior::Error
241}
242
243fn default_plugin_config_version() -> u32 {
244 1
245}
246
247fn default_enabled() -> bool {
248 true
249}
250
251pub struct PluginRegistration {
253 pub kind: String,
255 pub name: String,
257 deregister: Box<dyn FnMut() -> Result<()> + Send>,
258}
259
260impl fmt::Debug for PluginRegistration {
261 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
262 f.debug_struct("PluginRegistration")
263 .field("kind", &self.kind)
264 .field("name", &self.name)
265 .finish_non_exhaustive()
266 }
267}
268
269impl PluginRegistration {
270 pub fn new(
272 kind: impl Into<String>,
273 name: impl Into<String>,
274 deregister: Box<dyn FnMut() -> Result<()> + Send>,
275 ) -> Self {
276 Self {
277 kind: kind.into(),
278 name: name.into(),
279 deregister,
280 }
281 }
282}
283
284#[derive(Default)]
290pub struct PluginRegistrationContext {
291 registrations: Vec<PluginRegistration>,
292 namespace: Option<String>,
293}
294
295impl PluginRegistrationContext {
296 pub fn new() -> Self {
298 Self::default()
299 }
300
301 pub fn with_namespace(namespace: impl Into<String>) -> Self {
303 Self {
304 registrations: vec![],
305 namespace: Some(namespace.into()),
306 }
307 }
308
309 pub fn qualify_name(&self, name: &str) -> String {
315 match &self.namespace {
316 Some(namespace) => format!("{namespace}{name}"),
317 None => name.to_string(),
318 }
319 }
320
321 pub fn register_subscriber(&mut self, name: &str, callback: EventSubscriberFn) -> Result<()> {
323 let qualified_name = self.qualify_name(name);
324 register_subscriber(&qualified_name, callback)
325 .map_err(|err| PluginError::RegistrationFailed(format!("subscriber: {err}")))?;
326
327 let name_owned = qualified_name;
328 self.registrations.push(PluginRegistration::new(
329 "plugin",
330 name_owned.clone(),
331 Box::new(move || {
332 deregister_subscriber(&name_owned)
333 .map(|_| ())
334 .map_err(|err| {
335 PluginError::RegistrationFailed(format!(
336 "subscriber deregistration failed: {err}"
337 ))
338 })
339 }),
340 ));
341 Ok(())
342 }
343
344 pub fn register_llm_request_intercept(
346 &mut self,
347 name: &str,
348 priority: i32,
349 break_chain: bool,
350 callback: LlmRequestInterceptFn,
351 ) -> Result<()> {
352 let qualified_name = self.qualify_name(name);
353 register_llm_request_intercept(&qualified_name, priority, break_chain, callback).map_err(
354 |err| PluginError::RegistrationFailed(format!("llm request intercept: {err}")),
355 )?;
356
357 let name_owned = qualified_name;
358 self.registrations.push(PluginRegistration::new(
359 "plugin",
360 name_owned.clone(),
361 Box::new(move || {
362 deregister_llm_request_intercept(&name_owned)
363 .map(|_| ())
364 .map_err(|err| {
365 PluginError::RegistrationFailed(format!(
366 "llm request intercept deregistration failed: {err}"
367 ))
368 })
369 }),
370 ));
371 Ok(())
372 }
373
374 pub fn register_tool_sanitize_request_guardrail(
376 &mut self,
377 name: &str,
378 priority: i32,
379 callback: ToolSanitizeFn,
380 ) -> Result<()> {
381 let qualified_name = self.qualify_name(name);
382 register_tool_sanitize_request_guardrail(&qualified_name, priority, callback).map_err(
383 |err| {
384 PluginError::RegistrationFailed(format!("tool sanitize request guardrail: {err}"))
385 },
386 )?;
387
388 let name_owned = qualified_name;
389 self.registrations.push(PluginRegistration::new(
390 "plugin",
391 name_owned.clone(),
392 Box::new(move || {
393 deregister_tool_sanitize_request_guardrail(&name_owned)
394 .map(|_| ())
395 .map_err(|err| {
396 PluginError::RegistrationFailed(format!(
397 "tool sanitize request guardrail deregistration failed: {err}"
398 ))
399 })
400 }),
401 ));
402 Ok(())
403 }
404
405 pub fn register_tool_sanitize_response_guardrail(
407 &mut self,
408 name: &str,
409 priority: i32,
410 callback: ToolSanitizeFn,
411 ) -> Result<()> {
412 let qualified_name = self.qualify_name(name);
413 register_tool_sanitize_response_guardrail(&qualified_name, priority, callback).map_err(
414 |err| {
415 PluginError::RegistrationFailed(format!("tool sanitize response guardrail: {err}"))
416 },
417 )?;
418
419 let name_owned = qualified_name;
420 self.registrations.push(PluginRegistration::new(
421 "plugin",
422 name_owned.clone(),
423 Box::new(move || {
424 deregister_tool_sanitize_response_guardrail(&name_owned)
425 .map(|_| ())
426 .map_err(|err| {
427 PluginError::RegistrationFailed(format!(
428 "tool sanitize response guardrail deregistration failed: {err}"
429 ))
430 })
431 }),
432 ));
433 Ok(())
434 }
435
436 pub fn register_tool_conditional_execution_guardrail(
438 &mut self,
439 name: &str,
440 priority: i32,
441 callback: ToolConditionalFn,
442 ) -> Result<()> {
443 let qualified_name = self.qualify_name(name);
444 register_tool_conditional_execution_guardrail(&qualified_name, priority, callback)
445 .map_err(|err| {
446 PluginError::RegistrationFailed(format!(
447 "tool conditional execution guardrail: {err}"
448 ))
449 })?;
450
451 let name_owned = qualified_name;
452 self.registrations.push(PluginRegistration::new(
453 "plugin",
454 name_owned.clone(),
455 Box::new(move || {
456 deregister_tool_conditional_execution_guardrail(&name_owned)
457 .map(|_| ())
458 .map_err(|err| {
459 PluginError::RegistrationFailed(format!(
460 "tool conditional execution guardrail deregistration failed: {err}"
461 ))
462 })
463 }),
464 ));
465 Ok(())
466 }
467
468 pub fn register_llm_sanitize_request_guardrail(
470 &mut self,
471 name: &str,
472 priority: i32,
473 callback: LlmSanitizeRequestFn,
474 ) -> Result<()> {
475 let qualified_name = self.qualify_name(name);
476 register_llm_sanitize_request_guardrail(&qualified_name, priority, callback).map_err(
477 |err| PluginError::RegistrationFailed(format!("llm sanitize request guardrail: {err}")),
478 )?;
479
480 let name_owned = qualified_name;
481 self.registrations.push(PluginRegistration::new(
482 "plugin",
483 name_owned.clone(),
484 Box::new(move || {
485 deregister_llm_sanitize_request_guardrail(&name_owned)
486 .map(|_| ())
487 .map_err(|err| {
488 PluginError::RegistrationFailed(format!(
489 "llm sanitize request guardrail deregistration failed: {err}"
490 ))
491 })
492 }),
493 ));
494 Ok(())
495 }
496
497 pub fn register_llm_sanitize_response_guardrail(
499 &mut self,
500 name: &str,
501 priority: i32,
502 callback: LlmSanitizeResponseFn,
503 ) -> Result<()> {
504 let qualified_name = self.qualify_name(name);
505 register_llm_sanitize_response_guardrail(&qualified_name, priority, callback).map_err(
506 |err| {
507 PluginError::RegistrationFailed(format!("llm sanitize response guardrail: {err}"))
508 },
509 )?;
510
511 let name_owned = qualified_name;
512 self.registrations.push(PluginRegistration::new(
513 "plugin",
514 name_owned.clone(),
515 Box::new(move || {
516 deregister_llm_sanitize_response_guardrail(&name_owned)
517 .map(|_| ())
518 .map_err(|err| {
519 PluginError::RegistrationFailed(format!(
520 "llm sanitize response guardrail deregistration failed: {err}"
521 ))
522 })
523 }),
524 ));
525 Ok(())
526 }
527
528 pub fn register_llm_conditional_execution_guardrail(
530 &mut self,
531 name: &str,
532 priority: i32,
533 callback: LlmConditionalFn,
534 ) -> Result<()> {
535 let qualified_name = self.qualify_name(name);
536 register_llm_conditional_execution_guardrail(&qualified_name, priority, callback).map_err(
537 |err| {
538 PluginError::RegistrationFailed(format!(
539 "llm conditional execution guardrail: {err}"
540 ))
541 },
542 )?;
543
544 let name_owned = qualified_name;
545 self.registrations.push(PluginRegistration::new(
546 "plugin",
547 name_owned.clone(),
548 Box::new(move || {
549 deregister_llm_conditional_execution_guardrail(&name_owned)
550 .map(|_| ())
551 .map_err(|err| {
552 PluginError::RegistrationFailed(format!(
553 "llm conditional execution guardrail deregistration failed: {err}"
554 ))
555 })
556 }),
557 ));
558 Ok(())
559 }
560
561 pub fn register_llm_execution_intercept(
563 &mut self,
564 name: &str,
565 priority: i32,
566 callback: LlmExecutionFn,
567 ) -> Result<()> {
568 let qualified_name = self.qualify_name(name);
569 register_llm_execution_intercept(&qualified_name, priority, callback).map_err(|err| {
570 PluginError::RegistrationFailed(format!("llm execution intercept: {err}"))
571 })?;
572
573 let name_owned = qualified_name;
574 self.registrations.push(PluginRegistration::new(
575 "plugin",
576 name_owned.clone(),
577 Box::new(move || {
578 deregister_llm_execution_intercept(&name_owned)
579 .map(|_| ())
580 .map_err(|err| {
581 PluginError::RegistrationFailed(format!(
582 "llm execution intercept deregistration failed: {err}"
583 ))
584 })
585 }),
586 ));
587 Ok(())
588 }
589
590 pub fn register_llm_stream_execution_intercept(
592 &mut self,
593 name: &str,
594 priority: i32,
595 callback: LlmStreamExecutionFn,
596 ) -> Result<()> {
597 let qualified_name = self.qualify_name(name);
598 register_llm_stream_execution_intercept(&qualified_name, priority, callback).map_err(
599 |err| PluginError::RegistrationFailed(format!("llm stream execution intercept: {err}")),
600 )?;
601
602 let name_owned = qualified_name;
603 self.registrations.push(PluginRegistration::new(
604 "plugin",
605 name_owned.clone(),
606 Box::new(move || {
607 deregister_llm_stream_execution_intercept(&name_owned)
608 .map(|_| ())
609 .map_err(|err| {
610 PluginError::RegistrationFailed(format!(
611 "llm stream execution intercept deregistration failed: {err}"
612 ))
613 })
614 }),
615 ));
616 Ok(())
617 }
618
619 pub fn register_tool_request_intercept(
621 &mut self,
622 name: &str,
623 priority: i32,
624 break_chain: bool,
625 callback: ToolInterceptFn,
626 ) -> Result<()> {
627 let qualified_name = self.qualify_name(name);
628 register_tool_request_intercept(&qualified_name, priority, break_chain, callback).map_err(
629 |err| PluginError::RegistrationFailed(format!("tool request intercept: {err}")),
630 )?;
631
632 let name_owned = qualified_name;
633 self.registrations.push(PluginRegistration::new(
634 "plugin",
635 name_owned.clone(),
636 Box::new(move || {
637 deregister_tool_request_intercept(&name_owned)
638 .map(|_| ())
639 .map_err(|err| {
640 PluginError::RegistrationFailed(format!(
641 "tool request intercept deregistration failed: {err}"
642 ))
643 })
644 }),
645 ));
646 Ok(())
647 }
648
649 pub fn register_tool_execution_intercept(
651 &mut self,
652 name: &str,
653 priority: i32,
654 callback: ToolExecutionFn,
655 ) -> Result<()> {
656 let qualified_name = self.qualify_name(name);
657 register_tool_execution_intercept(&qualified_name, priority, callback).map_err(|err| {
658 PluginError::RegistrationFailed(format!("tool execution intercept: {err}"))
659 })?;
660
661 let name_owned = qualified_name;
662 self.registrations.push(PluginRegistration::new(
663 "plugin",
664 name_owned.clone(),
665 Box::new(move || {
666 deregister_tool_execution_intercept(&name_owned)
667 .map(|_| ())
668 .map_err(|err| {
669 PluginError::RegistrationFailed(format!(
670 "tool execution intercept deregistration failed: {err}"
671 ))
672 })
673 }),
674 ));
675 Ok(())
676 }
677
678 pub fn add_registration(&mut self, registration: PluginRegistration) {
680 self.registrations.push(registration);
681 }
682
683 pub fn extend_registrations(&mut self, registrations: Vec<PluginRegistration>) {
685 self.registrations.extend(registrations);
686 }
687
688 pub fn into_registrations(self) -> Vec<PluginRegistration> {
690 self.registrations
691 }
692}
693
694pub trait Plugin: Send + Sync + 'static {
696 fn plugin_kind(&self) -> &str;
698
699 fn allows_multiple_components(&self) -> bool {
704 true
705 }
706
707 fn validate(&self, plugin_config: &Map<String, Json>) -> Vec<ConfigDiagnostic>;
712
713 fn register<'a>(
719 &'a self,
720 plugin_config: &Map<String, Json>,
721 ctx: &'a mut PluginRegistrationContext,
722 ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>>;
723}
724
725pub fn register_plugin(plugin: Arc<dyn Plugin>) -> Result<()> {
747 let mut guard = PLUGIN_HANDLERS
748 .write()
749 .map_err(|err| PluginError::Internal(format!("plugin registry lock poisoned: {err}")))?;
750 let plugin_kind = plugin.plugin_kind().to_string();
751 if guard.contains_key(&plugin_kind) {
752 return Err(PluginError::RegistrationFailed(format!(
753 "plugin '{plugin_kind}' is already registered"
754 )));
755 }
756 guard.insert(plugin_kind, plugin);
757 Ok(())
758}
759
760pub fn ensure_builtin_plugins_registered() -> Result<()> {
765 let register_builtins = || {
766 crate::observability::plugin_component::register_observability_component()?;
767 crate::plugins::nemo_guardrails::component::register_nemo_guardrails_component()?;
768 crate::plugins::pricing::register_pricing_component()
769 };
770 match BUILTIN_PLUGIN_REGISTRATION.get_or_init(register_builtins) {
771 Ok(()) => Ok(()),
772 Err(err) => Err(clone_cached_plugin_error(err)),
773 }
774}
775
776fn clone_cached_plugin_error(err: &PluginError) -> PluginError {
777 match err {
778 PluginError::InvalidConfig(message) => PluginError::InvalidConfig(message.clone()),
779 PluginError::NotFound(message) => PluginError::NotFound(message.clone()),
780 PluginError::Serialization(err) => PluginError::Internal(err.to_string()),
781 PluginError::Internal(message) => PluginError::Internal(message.clone()),
782 PluginError::RegistrationFailed(message) => {
783 PluginError::RegistrationFailed(message.clone())
784 }
785 }
786}
787
788pub fn deregister_plugin(plugin_kind: &str) -> bool {
804 PLUGIN_HANDLERS
805 .write()
806 .ok()
807 .and_then(|mut guard| guard.remove(plugin_kind))
808 .is_some()
809}
810
811pub fn list_plugin_kinds() -> Vec<String> {
823 let _ = ensure_builtin_plugins_registered();
824 let mut kinds = PLUGIN_HANDLERS
825 .read()
826 .map(|guard| guard.keys().cloned().collect::<Vec<_>>())
827 .unwrap_or_default();
828 kinds.sort();
829 kinds
830}
831
832pub fn lookup_plugin(plugin_kind: &str) -> Option<Arc<dyn Plugin>> {
844 let _ = ensure_builtin_plugins_registered();
845 PLUGIN_HANDLERS
846 .read()
847 .ok()
848 .and_then(|guard| guard.get(plugin_kind).cloned())
849}
850
851pub fn validate_plugin_config(config: &PluginConfig) -> ConfigReport {
867 let _ = ensure_builtin_plugins_registered();
868 let mut report = ConfigReport::default();
869
870 if config.version != 1 {
871 push_policy_diag(
872 &mut report.diagnostics,
873 config.policy.unsupported_value,
874 "plugin.unsupported_config_version",
875 None,
876 Some("version".to_string()),
877 format!("plugin config version {} is unsupported", config.version),
878 );
879 }
880
881 validate_plugin_multiplicity(&mut report, config);
882
883 for component in &config.components {
884 let Some(plugin) = lookup_plugin(&component.kind) else {
885 push_policy_diag(
886 &mut report.diagnostics,
887 config.policy.unknown_component,
888 "plugin.unknown_component",
889 Some(component.kind.clone()),
890 None,
891 format!("plugin component kind '{}' is unsupported", component.kind),
892 );
893 continue;
894 };
895 report
896 .diagnostics
897 .extend(plugin.validate(&component.config));
898 }
899
900 report
901}
902
903fn layer_config(left: &mut Json, right: Json) {
910 match (left, right) {
911 (Json::Object(left), Json::Object(right)) => {
912 for (key, value) in right {
913 match (key.as_str(), left.get_mut(&key)) {
914 ("components", Some(existing)) => merge_plugin_components(existing, value),
915 (_, Some(existing)) => merge_json_value(existing, value),
916 (_, _) => {
917 left.insert(key, value);
918 }
919 }
920 }
921 }
922 (left, right) => *left = right,
923 }
924}
925
926fn merge_plugin_components(left: &mut Json, right: Json) {
928 let Json::Array(left_components) = left else {
929 *left = right;
930 return;
931 };
932 let Json::Array(right_components) = right else {
933 *left = right;
934 return;
935 };
936 let mut base_slots: HashMap<String, Vec<usize>> = HashMap::new();
937 for (index, component) in left_components.iter().enumerate() {
938 if let Some(kind) = component_kind(component) {
939 base_slots.entry(kind.to_string()).or_default().push(index);
940 }
941 }
942 let mut consumed: HashMap<String, usize> = HashMap::new();
943 for component in right_components {
944 let Some(kind) = component_kind(&component).map(str::to_owned) else {
945 left_components.push(component);
946 continue;
947 };
948 let nth = consumed.entry(kind.clone()).or_insert(0);
949 let slot = base_slots
950 .get(&kind)
951 .and_then(|slots| slots.get(*nth))
952 .copied();
953 *nth += 1;
954 match slot {
955 Some(index) if kind == "pricing" => {
956 merge_pricing_component(&mut left_components[index], component)
957 }
958 Some(index) => merge_json_value(&mut left_components[index], component),
959 None => left_components.push(component),
960 }
961 }
962}
963
964fn merge_json_value(left: &mut Json, right: Json) {
966 match (left, right) {
967 (Json::Object(left), Json::Object(right)) => {
968 for (key, value) in right {
969 match left.get_mut(&key) {
970 Some(existing) => merge_json_value(existing, value),
971 None => {
972 left.insert(key, value);
973 }
974 }
975 }
976 }
977 (left, right) => *left = right,
978 }
979}
980
981fn component_kind(component: &Json) -> Option<&str> {
982 component.get("kind").and_then(Json::as_str)
983}
984
985fn merge_pricing_component(existing: &mut Json, higher_priority: Json) {
988 let lower_priority_sources = pricing_component_sources(existing).cloned();
989 let higher_priority_sources = pricing_component_sources(&higher_priority).cloned();
990 merge_json_value(existing, higher_priority);
991
992 let Some(mut sources) = higher_priority_sources else {
993 return;
994 };
995 if let Some(lower_priority_sources) = lower_priority_sources {
996 sources.extend(lower_priority_sources);
997 }
998 set_pricing_component_sources(existing, sources);
999}
1000
1001fn pricing_component_sources(component: &Json) -> Option<&Vec<Json>> {
1002 component
1003 .get("config")
1004 .and_then(|config| config.get("sources"))
1005 .and_then(Json::as_array)
1006}
1007
1008fn set_pricing_component_sources(component: &mut Json, sources: Vec<Json>) {
1009 if let Some(config) = component.get_mut("config").and_then(Json::as_object_mut) {
1010 config.insert("sources".into(), Json::Array(sources));
1011 }
1012}
1013
1014#[cfg(feature = "schema")]
1016pub fn plugin_config_schema() -> Json {
1017 serde_json::to_value(schemars::schema_for!(PluginConfig))
1018 .expect("plugin config schema should serialize")
1019}
1020
1021#[doc(hidden)]
1042pub async fn initialize_plugins_exact(config: PluginConfig) -> Result<ConfigReport> {
1043 let report = validate_plugin_config(&config);
1044 if report.has_errors() {
1045 return Err(PluginError::InvalidConfig(join_error_messages(&report)));
1046 }
1047
1048 let previous = {
1049 let mut guard = ACTIVE_PLUGIN_CONFIGURATION.lock().map_err(|err| {
1050 PluginError::Internal(format!("active plugin configuration lock poisoned: {err}"))
1051 })?;
1052 guard.take()
1053 };
1054
1055 if let Some(mut previous_state) = previous {
1056 rollback_registrations(&mut previous_state.registrations);
1057 match initialize_plugin_components(&config).await {
1058 Ok(registrations) => {
1059 store_active_plugin_configuration(config, report.clone(), registrations)?;
1060 Ok(report)
1061 }
1062 Err(err) => match initialize_plugin_components(&previous_state.config).await {
1063 Ok(registrations) => {
1064 let previous_report = validate_plugin_config(&previous_state.config);
1065 store_active_plugin_configuration(
1066 previous_state.config,
1067 previous_report,
1068 registrations,
1069 )?;
1070 Err(err)
1071 }
1072 Err(restore_err) => Err(PluginError::RegistrationFailed(format!(
1073 "{err}; previous plugin configuration could not be restored: {restore_err}"
1074 ))),
1075 },
1076 }
1077 } else {
1078 let registrations = initialize_plugin_components(&config).await?;
1079 store_active_plugin_configuration(config, report.clone(), registrations)?;
1080 Ok(report)
1081 }
1082}
1083
1084pub async fn initialize_plugins(config: PluginConfig) -> Result<ConfigReport> {
1090 let mut base = resolve_default_file_plugin_config()?;
1091 layer_config(&mut base, serde_json::to_value(config)?);
1092 let config: PluginConfig = serde_json::from_value(base)?;
1093 initialize_plugins_exact(config).await
1094}
1095
1096fn resolve_default_file_plugin_config() -> Result<Json> {
1099 let paths =
1100 default_plugin_config_paths(std::env::current_dir().ok().as_deref(), user_config_dir());
1101 Ok(load_plugin_config_files(paths)?
1102 .map(|(value, _sources)| value)
1103 .unwrap_or_else(|| Json::Object(Map::new())))
1104}
1105
1106use std::path::{Path, PathBuf};
1107
1108#[doc(hidden)]
1112pub fn load_plugin_config_files<I>(paths: I) -> Result<Option<(Json, Vec<PathBuf>)>>
1113where
1114 I: IntoIterator<Item = PathBuf>,
1115{
1116 let mut merged = Json::Object(Map::new());
1117 let mut sources = Vec::new();
1118 for path in paths {
1119 if !path.exists() {
1120 continue;
1121 }
1122 let raw = std::fs::read_to_string(&path).map_err(|err| {
1123 PluginError::InvalidConfig(format!("failed to read {}: {err}", path.display()))
1124 })?;
1125 let parsed = raw.parse::<toml::Table>().map_err(|err| {
1126 PluginError::InvalidConfig(format!("invalid plugin TOML in {}: {err}", path.display()))
1127 })?;
1128 let document = serde_json::to_value(parsed)?;
1129 validate_unique_component_kinds(&path, &document)?;
1130 layer_config(&mut merged, document);
1131 sources.push(path);
1132 }
1133 Ok((!sources.is_empty()).then_some((merged, sources)))
1134}
1135
1136fn validate_unique_component_kinds(path: &Path, document: &Json) -> Result<()> {
1138 let Some(components) = document.get("components").and_then(Json::as_array) else {
1139 return Ok(());
1140 };
1141 let mut seen = HashSet::new();
1142 let mut duplicates = Vec::new();
1143 for component in components {
1144 if let Some(kind) = component_kind(component)
1145 && !seen.insert(kind)
1146 {
1147 duplicates.push(kind.to_string());
1148 }
1149 }
1150 if duplicates.is_empty() {
1151 return Ok(());
1152 }
1153 duplicates.sort();
1154 duplicates.dedup();
1155 Err(PluginError::InvalidConfig(format!(
1156 "duplicate plugin component kind in {}: {}; declare each kind once per plugins.toml",
1157 path.display(),
1158 duplicates.join(", ")
1159 )))
1160}
1161
1162#[doc(hidden)]
1166pub fn default_plugin_config_paths(cwd: Option<&Path>, user_dir: Option<PathBuf>) -> Vec<PathBuf> {
1167 let mut paths = vec![PathBuf::from("/etc/nemo-relay/plugins.toml")];
1168 if let Some(cwd) = cwd
1169 && let Some(project) = nearest_project_plugin_config(cwd)
1170 {
1171 paths.push(project);
1172 }
1173 if let Some(dir) = user_dir {
1174 paths.push(dir.join("plugins.toml"));
1175 }
1176 paths
1177}
1178
1179#[doc(hidden)]
1182pub fn nearest_project_plugin_config(start: &Path) -> Option<PathBuf> {
1183 start
1184 .ancestors()
1185 .map(|ancestor| ancestor.join(".nemo-relay").join("plugins.toml"))
1186 .find(|path| path.exists())
1187}
1188
1189#[doc(hidden)]
1192pub fn user_config_dir() -> Option<PathBuf> {
1193 if let Some(base) = std::env::var_os("XDG_CONFIG_HOME") {
1194 return Some(PathBuf::from(base).join("nemo-relay"));
1195 }
1196 std::env::var_os("HOME")
1197 .or_else(|| std::env::var_os("USERPROFILE"))
1198 .map(|home| PathBuf::from(home).join(".config/nemo-relay"))
1199}
1200
1201pub fn clear_plugin_configuration() -> Result<()> {
1217 let flush_error = crate::api::runtime::flush_subscribers()
1218 .err()
1219 .map(|error| error.to_string());
1220 let previous = {
1221 let mut guard = ACTIVE_PLUGIN_CONFIGURATION.lock().map_err(|err| {
1222 PluginError::Internal(format!("active plugin configuration lock poisoned: {err}"))
1223 })?;
1224 guard.take()
1225 };
1226 if let Some(mut previous_state) = previous {
1227 rollback_registrations(&mut previous_state.registrations);
1228 }
1229 if let Some(message) = flush_error {
1230 return Err(PluginError::Internal(message));
1231 }
1232 Ok(())
1233}
1234
1235pub fn active_plugin_report() -> Option<ConfigReport> {
1247 ACTIVE_PLUGIN_CONFIGURATION
1248 .lock()
1249 .ok()
1250 .and_then(|guard| guard.as_ref().map(|state| state.report.clone()))
1251}
1252
1253pub fn rollback_registrations(registrations: &mut Vec<PluginRegistration>) {
1258 for registration in registrations.iter_mut().rev() {
1259 let _ = (registration.deregister)();
1260 }
1261 registrations.clear();
1262}
1263
1264struct ActivePluginConfiguration {
1265 config: PluginConfig,
1266 report: ConfigReport,
1267 registrations: Vec<PluginRegistration>,
1268}
1269
1270async fn initialize_plugin_components(config: &PluginConfig) -> Result<Vec<PluginRegistration>> {
1271 ensure_builtin_plugins_registered()?;
1272 let totals = plugin_component_totals(config);
1273 let mut ordinals: HashMap<&str, usize> = HashMap::new();
1274 let mut registrations = vec![];
1275
1276 for component in config
1277 .components
1278 .iter()
1279 .filter(|component| component.enabled)
1280 {
1281 let Some(plugin) = lookup_plugin(&component.kind) else {
1282 rollback_registrations(&mut registrations);
1283 return Err(PluginError::NotFound(format!(
1284 "plugin component '{}' is not registered",
1285 component.kind
1286 )));
1287 };
1288
1289 let ordinal = ordinals
1290 .entry(component.kind.as_str())
1291 .and_modify(|value| *value += 1)
1292 .or_insert(1);
1293 let namespace = component_namespace(
1294 &component.kind,
1295 *ordinal,
1296 totals.get(component.kind.as_str()).copied().unwrap_or(1),
1297 );
1298
1299 let mut ctx = PluginRegistrationContext::with_namespace(namespace);
1300 if let Err(err) = plugin.register(&component.config, &mut ctx).await {
1301 let mut just_registered = ctx.into_registrations();
1302 rollback_registrations(&mut just_registered);
1303 rollback_registrations(&mut registrations);
1304 return Err(err);
1305 }
1306 registrations.extend(ctx.into_registrations());
1307 }
1308
1309 Ok(registrations)
1310}
1311
1312fn store_active_plugin_configuration(
1313 config: PluginConfig,
1314 report: ConfigReport,
1315 registrations: Vec<PluginRegistration>,
1316) -> Result<()> {
1317 let mut guard = ACTIVE_PLUGIN_CONFIGURATION.lock().map_err(|err| {
1318 PluginError::Internal(format!("active plugin configuration lock poisoned: {err}"))
1319 })?;
1320 *guard = Some(ActivePluginConfiguration {
1321 config,
1322 report,
1323 registrations,
1324 });
1325 Ok(())
1326}
1327
1328fn plugin_component_totals(config: &PluginConfig) -> HashMap<&str, usize> {
1329 let mut totals = HashMap::new();
1330 for component in &config.components {
1331 *totals.entry(component.kind.as_str()).or_insert(0) += 1;
1332 }
1333 totals
1334}
1335
1336fn component_namespace(kind: &str, ordinal: usize, total: usize) -> String {
1337 if total > 1 {
1338 format!("__nemo_relay_plugin__{kind}__{ordinal}__")
1339 } else {
1340 format!("__nemo_relay_plugin__{kind}__")
1341 }
1342}
1343
1344fn validate_plugin_multiplicity(report: &mut ConfigReport, config: &PluginConfig) {
1345 let totals = plugin_component_totals(config);
1346 let mut emitted = HashSet::new();
1347
1348 for component in &config.components {
1349 let count = totals
1350 .get(component.kind.as_str())
1351 .copied()
1352 .unwrap_or_default();
1353 if count <= 1 || !emitted.insert(component.kind.clone()) {
1354 continue;
1355 }
1356
1357 let allows_multiple = lookup_plugin(&component.kind)
1358 .map(|plugin| plugin.allows_multiple_components())
1359 .unwrap_or(true);
1360 if !allows_multiple {
1361 report.diagnostics.push(ConfigDiagnostic {
1362 level: DiagnosticLevel::Error,
1363 code: "plugin.duplicate_component".to_string(),
1364 component: Some(component.kind.clone()),
1365 field: None,
1366 message: format!(
1367 "plugin component kind '{}' may only appear once",
1368 component.kind
1369 ),
1370 });
1371 }
1372 }
1373}
1374
1375fn push_policy_diag(
1376 diagnostics: &mut Vec<ConfigDiagnostic>,
1377 behavior: UnsupportedBehavior,
1378 code: &str,
1379 component: Option<String>,
1380 field: Option<String>,
1381 message: String,
1382) {
1383 let level = match behavior {
1384 UnsupportedBehavior::Ignore => return,
1385 UnsupportedBehavior::Warn => DiagnosticLevel::Warning,
1386 UnsupportedBehavior::Error => DiagnosticLevel::Error,
1387 };
1388
1389 diagnostics.push(ConfigDiagnostic {
1390 level,
1391 code: code.to_string(),
1392 component,
1393 field,
1394 message,
1395 });
1396}
1397
1398fn join_error_messages(report: &ConfigReport) -> String {
1399 report
1400 .diagnostics
1401 .iter()
1402 .filter(|diag| diag.level == DiagnosticLevel::Error)
1403 .map(|diag| diag.message.as_str())
1404 .collect::<Vec<_>>()
1405 .join("; ")
1406}
1407
1408#[cfg(test)]
1409#[path = "../tests/unit/plugin_tests.rs"]
1410mod tests;