1use std::cell::Cell;
13use std::collections::{HashMap, HashSet};
14use std::fmt;
15use std::future::Future;
16use std::panic::{AssertUnwindSafe, catch_unwind};
17use std::pin::Pin;
18use std::sync::atomic::{AtomicU64, Ordering};
19use std::sync::{Arc, LazyLock, Mutex, OnceLock, RwLock};
20
21use serde::{Deserialize, Serialize};
22use serde_json::{Map, Value as Json};
23use thiserror::Error;
24
25use crate::api::registry::{
26 deregister_llm_conditional_execution_guardrail, deregister_llm_execution_intercept,
27 deregister_llm_request_intercept, deregister_llm_sanitize_request_guardrail,
28 deregister_llm_sanitize_response_guardrail, deregister_llm_stream_execution_intercept,
29 deregister_mark_sanitize_guardrail, deregister_scope_sanitize_end_guardrail,
30 deregister_scope_sanitize_start_guardrail, deregister_tool_conditional_execution_guardrail,
31 deregister_tool_execution_intercept, deregister_tool_request_intercept,
32 deregister_tool_sanitize_request_guardrail, deregister_tool_sanitize_response_guardrail,
33 register_llm_conditional_execution_guardrail, register_llm_execution_intercept,
34 register_llm_request_intercept, register_llm_sanitize_request_guardrail,
35 register_llm_sanitize_response_guardrail, register_llm_stream_execution_intercept,
36 register_mark_sanitize_guardrail, register_scope_sanitize_end_guardrail,
37 register_scope_sanitize_start_guardrail, register_tool_conditional_execution_guardrail,
38 register_tool_execution_intercept, register_tool_request_intercept,
39 register_tool_sanitize_request_guardrail, register_tool_sanitize_response_guardrail,
40};
41use crate::api::runtime::{
42 EventSanitizeFn, EventSubscriberFn, LlmConditionalFn, LlmExecutionFn, LlmRequestInterceptFn,
43 LlmSanitizeRequestFn, LlmSanitizeResponseFn, LlmStreamExecutionFn, ToolConditionalFn,
44 ToolExecutionFn, ToolInterceptFn, ToolSanitizeFn,
45};
46use crate::api::subscriber::{deregister_subscriber, register_subscriber};
47pub use nemo_relay_types::plugin::{ConfigDiagnostic, DiagnosticLevel};
48
49pub mod dynamic;
50pub use dynamic::*;
51
52type PluginMap = HashMap<String, RegisteredPlugin>;
53
54struct RegisteredPlugin {
55 registration_id: u64,
56 owner: PluginRegistrationOwner,
57 plugin: Arc<dyn Plugin>,
58}
59
60#[derive(Clone, Copy, PartialEq, Eq)]
61enum PluginRegistrationOwner {
62 Builtin,
63 External,
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub(crate) enum PluginDeregistrationOutcome {
68 Removed,
69 Missing,
70 Replaced,
71}
72
73static PLUGIN_HANDLERS: LazyLock<RwLock<PluginMap>> = LazyLock::new(|| RwLock::new(HashMap::new()));
74static ACTIVE_PLUGIN_CONFIGURATION: LazyLock<Mutex<Option<ActivePluginConfiguration>>> =
75 LazyLock::new(|| Mutex::new(None));
76static LAST_FAILED_RUNTIME_DIAGNOSTICS_REPORT: LazyLock<Mutex<Option<ConfigReport>>> =
77 LazyLock::new(|| Mutex::new(None));
78static PLUGIN_MUTATION_OWNER: LazyLock<Mutex<PluginMutationOwner>> =
79 LazyLock::new(|| Mutex::new(PluginMutationOwner::Idle));
80static NEXT_PLUGIN_REGISTRATION_ID: AtomicU64 = AtomicU64::new(1);
81static NEXT_PLUGIN_HOST_OWNER_ID: AtomicU64 = AtomicU64::new(1);
82static PLUGIN_MUTATION_EXECUTOR: OnceLock<PluginMutationSender> = OnceLock::new();
83
84type PluginMutationJob = Pin<Box<dyn Future<Output = ()> + Send + 'static>>;
85type PluginMutationSender = tokio::sync::mpsc::UnboundedSender<PluginMutationJob>;
86
87thread_local! {
88 static IN_PLUGIN_MUTATION_EXECUTOR: Cell<bool> = const { Cell::new(false) };
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92enum PluginMutationOwner {
93 Idle,
94 Legacy,
95 Host(u64),
96}
97
98#[derive(Debug, Error)]
100pub enum PluginError {
101 #[error("invalid config: {0}")]
103 InvalidConfig(String),
104
105 #[error("conflict: {0}")]
107 Conflict(String),
108
109 #[error("not found: {0}")]
111 NotFound(String),
112
113 #[error("serialization error: {0}")]
115 Serialization(#[from] serde_json::Error),
116
117 #[error("internal error: {0}")]
119 Internal(String),
120
121 #[error("registration failed: {0}")]
123 RegistrationFailed(String),
124}
125
126pub type Result<T> = std::result::Result<T, PluginError>;
128
129pub(crate) const ATIF_RUNTIME_DELIVERY_FAILURE_MARKER: &str = "ATIF runtime delivery failures";
131pub(crate) const OTEL_RUNTIME_DELIVERY_FAILURE_MARKER: &str =
133 "OpenTelemetry runtime delivery failures";
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
137#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
138pub struct PluginConfig {
139 #[serde(default = "default_plugin_config_version")]
141 pub version: u32,
142 #[serde(default)]
144 pub components: Vec<PluginComponentSpec>,
145 #[serde(default)]
149 pub policy: ConfigPolicy,
150}
151
152impl Default for PluginConfig {
153 fn default() -> Self {
154 Self {
155 version: default_plugin_config_version(),
156 components: vec![],
157 policy: ConfigPolicy::default(),
158 }
159 }
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
164#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
165pub struct PluginComponentSpec {
166 pub kind: String,
168 #[serde(default = "default_enabled")]
173 pub enabled: bool,
174 #[serde(default)]
176 pub config: Map<String, Json>,
177}
178
179impl PluginComponentSpec {
180 pub fn new(kind: impl Into<String>) -> Self {
182 Self {
183 kind: kind.into(),
184 enabled: true,
185 config: Map::new(),
186 }
187 }
188}
189
190#[derive(Debug, Clone, Default, Serialize, Deserialize)]
192#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
193pub struct ConfigReport {
194 #[serde(default)]
196 pub diagnostics: Vec<ConfigDiagnostic>,
197 #[serde(default, skip_serializing_if = "Vec::is_empty")]
199 pub runtime_diagnostics: Vec<RuntimeDiagnostic>,
200}
201
202#[derive(Debug, Clone, Serialize, Deserialize)]
204#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
205pub struct RuntimeDiagnostic {
206 pub code: String,
208 pub component: String,
210 #[serde(skip_serializing_if = "Option::is_none")]
212 pub field: Option<String>,
213 pub message: String,
215 #[serde(skip_serializing_if = "Option::is_none")]
217 pub session_id: Option<String>,
218 pub count: u64,
220}
221
222impl ConfigReport {
223 pub fn has_errors(&self) -> bool {
225 self.diagnostics
226 .iter()
227 .any(|diag| diag.level == DiagnosticLevel::Error)
228 }
229}
230
231#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
233#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
234pub struct ConfigPolicy {
235 #[serde(default = "default_warn")]
237 pub unknown_component: UnsupportedBehavior,
238 #[serde(default = "default_warn")]
240 pub unknown_field: UnsupportedBehavior,
241 #[serde(default = "default_error")]
243 pub unsupported_value: UnsupportedBehavior,
244}
245
246impl Default for ConfigPolicy {
247 fn default() -> Self {
248 Self {
249 unknown_component: default_warn(),
250 unknown_field: default_warn(),
251 unsupported_value: default_error(),
252 }
253 }
254}
255
256pub fn apply_global_config_policy(
261 component_policy: ConfigPolicy,
262 global_policy: &ConfigPolicy,
263) -> ConfigPolicy {
264 let default_policy = ConfigPolicy::default();
265 ConfigPolicy {
266 unknown_component: if global_policy.unknown_component != default_policy.unknown_component {
267 global_policy.unknown_component
268 } else {
269 component_policy.unknown_component
270 },
271 unknown_field: if global_policy.unknown_field != default_policy.unknown_field {
272 global_policy.unknown_field
273 } else {
274 component_policy.unknown_field
275 },
276 unsupported_value: if global_policy.unsupported_value != default_policy.unsupported_value {
277 global_policy.unsupported_value
278 } else {
279 component_policy.unsupported_value
280 },
281 }
282}
283
284crate::editor_config! {
285 impl ConfigPolicy {
286 unknown_component => {
287 label: "unknown_component",
288 kind: Enum,
289 values: ["warn", "ignore", "error"],
290 },
291 unknown_field => {
292 label: "unknown_field",
293 kind: Enum,
294 values: ["warn", "ignore", "error"],
295 },
296 unsupported_value => {
297 label: "unsupported_value",
298 kind: Enum,
299 values: ["warn", "ignore", "error"],
300 },
301 }
302}
303
304#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
306#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
307#[serde(rename_all = "lowercase")]
308pub enum UnsupportedBehavior {
309 Ignore,
311 #[default]
313 Warn,
314 Error,
316}
317
318fn default_warn() -> UnsupportedBehavior {
319 UnsupportedBehavior::Warn
320}
321
322fn default_error() -> UnsupportedBehavior {
323 UnsupportedBehavior::Error
324}
325
326fn default_plugin_config_version() -> u32 {
327 1
328}
329
330fn default_enabled() -> bool {
331 true
332}
333
334pub struct PluginRegistration {
336 pub kind: String,
338 pub name: String,
340 deregister: Box<dyn FnMut() -> PluginRegistrationCleanupOutcome + Send>,
341}
342
343pub(crate) enum PluginRegistrationCleanupOutcome {
344 Removed,
345 RemovedWithError(PluginError),
346 NotRemoved(PluginError),
347}
348
349impl fmt::Debug for PluginRegistration {
350 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
351 f.debug_struct("PluginRegistration")
352 .field("kind", &self.kind)
353 .field("name", &self.name)
354 .finish_non_exhaustive()
355 }
356}
357
358impl PluginRegistration {
359 pub fn new(
361 kind: impl Into<String>,
362 name: impl Into<String>,
363 mut deregister: Box<dyn FnMut() -> Result<()> + Send>,
364 ) -> Self {
365 Self {
366 kind: kind.into(),
367 name: name.into(),
368 deregister: Box::new(move || match deregister() {
369 Ok(()) => PluginRegistrationCleanupOutcome::Removed,
370 Err(error) => PluginRegistrationCleanupOutcome::NotRemoved(error),
371 }),
372 }
373 }
374
375 pub(crate) fn new_with_outcome(
376 kind: impl Into<String>,
377 name: impl Into<String>,
378 deregister: Box<dyn FnMut() -> PluginRegistrationCleanupOutcome + Send>,
379 ) -> Self {
380 Self {
381 kind: kind.into(),
382 name: name.into(),
383 deregister,
384 }
385 }
386}
387
388#[derive(Default)]
394pub struct PluginRegistrationContext {
395 registrations: Vec<PluginRegistration>,
396 namespace: Option<String>,
397}
398
399impl PluginRegistrationContext {
400 pub fn new() -> Self {
402 Self::default()
403 }
404
405 pub fn with_namespace(namespace: impl Into<String>) -> Self {
407 Self {
408 registrations: vec![],
409 namespace: Some(namespace.into()),
410 }
411 }
412
413 pub fn qualify_name(&self, name: &str) -> String {
419 match &self.namespace {
420 Some(namespace) => format!("{namespace}{name}"),
421 None => name.to_string(),
422 }
423 }
424
425 pub fn register_subscriber(&mut self, name: &str, callback: EventSubscriberFn) -> Result<()> {
427 let qualified_name = self.qualify_name(name);
428 register_subscriber(&qualified_name, callback)
429 .map_err(|err| PluginError::RegistrationFailed(format!("subscriber: {err}")))?;
430
431 let name_owned = qualified_name;
432 self.registrations.push(PluginRegistration::new(
433 "plugin",
434 name_owned.clone(),
435 Box::new(move || {
436 deregister_subscriber(&name_owned)
437 .map(|_| ())
438 .map_err(|err| {
439 PluginError::RegistrationFailed(format!(
440 "subscriber deregistration failed: {err}"
441 ))
442 })
443 }),
444 ));
445 Ok(())
446 }
447
448 pub fn register_mark_sanitize_guardrail(
450 &mut self,
451 name: &str,
452 priority: i32,
453 callback: EventSanitizeFn,
454 ) -> Result<()> {
455 let qualified_name = self.qualify_name(name);
456 register_mark_sanitize_guardrail(&qualified_name, priority, callback)
457 .map_err(|err| PluginError::RegistrationFailed(format!("mark sanitizer: {err}")))?;
458 let name_owned = qualified_name;
459 self.registrations.push(PluginRegistration::new(
460 "plugin",
461 name_owned.clone(),
462 Box::new(move || {
463 deregister_mark_sanitize_guardrail(&name_owned)
464 .map(|_| ())
465 .map_err(|err| {
466 PluginError::RegistrationFailed(format!(
467 "mark sanitizer deregistration failed: {err}"
468 ))
469 })
470 }),
471 ));
472 Ok(())
473 }
474
475 pub fn register_scope_sanitize_start_guardrail(
477 &mut self,
478 name: &str,
479 priority: i32,
480 callback: EventSanitizeFn,
481 ) -> Result<()> {
482 let qualified_name = self.qualify_name(name);
483 register_scope_sanitize_start_guardrail(&qualified_name, priority, callback).map_err(
484 |err| PluginError::RegistrationFailed(format!("scope-start sanitizer: {err}")),
485 )?;
486 let name_owned = qualified_name;
487 self.registrations.push(PluginRegistration::new(
488 "plugin",
489 name_owned.clone(),
490 Box::new(move || {
491 deregister_scope_sanitize_start_guardrail(&name_owned)
492 .map(|_| ())
493 .map_err(|err| {
494 PluginError::RegistrationFailed(format!(
495 "scope-start sanitizer deregistration failed: {err}"
496 ))
497 })
498 }),
499 ));
500 Ok(())
501 }
502
503 pub fn register_scope_sanitize_end_guardrail(
505 &mut self,
506 name: &str,
507 priority: i32,
508 callback: EventSanitizeFn,
509 ) -> Result<()> {
510 let qualified_name = self.qualify_name(name);
511 register_scope_sanitize_end_guardrail(&qualified_name, priority, callback).map_err(
512 |err| PluginError::RegistrationFailed(format!("scope-end sanitizer: {err}")),
513 )?;
514 let name_owned = qualified_name;
515 self.registrations.push(PluginRegistration::new(
516 "plugin",
517 name_owned.clone(),
518 Box::new(move || {
519 deregister_scope_sanitize_end_guardrail(&name_owned)
520 .map(|_| ())
521 .map_err(|err| {
522 PluginError::RegistrationFailed(format!(
523 "scope-end sanitizer deregistration failed: {err}"
524 ))
525 })
526 }),
527 ));
528 Ok(())
529 }
530
531 pub fn register_llm_request_intercept(
533 &mut self,
534 name: &str,
535 priority: i32,
536 break_chain: bool,
537 callback: LlmRequestInterceptFn,
538 ) -> Result<()> {
539 let qualified_name = self.qualify_name(name);
540 register_llm_request_intercept(&qualified_name, priority, break_chain, callback).map_err(
541 |err| PluginError::RegistrationFailed(format!("llm request intercept: {err}")),
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_request_intercept(&name_owned)
550 .map(|_| ())
551 .map_err(|err| {
552 PluginError::RegistrationFailed(format!(
553 "llm request intercept deregistration failed: {err}"
554 ))
555 })
556 }),
557 ));
558 Ok(())
559 }
560
561 pub fn register_tool_sanitize_request_guardrail(
563 &mut self,
564 name: &str,
565 priority: i32,
566 callback: ToolSanitizeFn,
567 ) -> Result<()> {
568 let qualified_name = self.qualify_name(name);
569 register_tool_sanitize_request_guardrail(&qualified_name, priority, callback).map_err(
570 |err| {
571 PluginError::RegistrationFailed(format!("tool sanitize request guardrail: {err}"))
572 },
573 )?;
574
575 let name_owned = qualified_name;
576 self.registrations.push(PluginRegistration::new(
577 "plugin",
578 name_owned.clone(),
579 Box::new(move || {
580 deregister_tool_sanitize_request_guardrail(&name_owned)
581 .map(|_| ())
582 .map_err(|err| {
583 PluginError::RegistrationFailed(format!(
584 "tool sanitize request guardrail deregistration failed: {err}"
585 ))
586 })
587 }),
588 ));
589 Ok(())
590 }
591
592 pub fn register_tool_sanitize_response_guardrail(
594 &mut self,
595 name: &str,
596 priority: i32,
597 callback: ToolSanitizeFn,
598 ) -> Result<()> {
599 let qualified_name = self.qualify_name(name);
600 register_tool_sanitize_response_guardrail(&qualified_name, priority, callback).map_err(
601 |err| {
602 PluginError::RegistrationFailed(format!("tool sanitize response guardrail: {err}"))
603 },
604 )?;
605
606 let name_owned = qualified_name;
607 self.registrations.push(PluginRegistration::new(
608 "plugin",
609 name_owned.clone(),
610 Box::new(move || {
611 deregister_tool_sanitize_response_guardrail(&name_owned)
612 .map(|_| ())
613 .map_err(|err| {
614 PluginError::RegistrationFailed(format!(
615 "tool sanitize response guardrail deregistration failed: {err}"
616 ))
617 })
618 }),
619 ));
620 Ok(())
621 }
622
623 pub fn register_tool_conditional_execution_guardrail(
625 &mut self,
626 name: &str,
627 priority: i32,
628 callback: ToolConditionalFn,
629 ) -> Result<()> {
630 let qualified_name = self.qualify_name(name);
631 register_tool_conditional_execution_guardrail(&qualified_name, priority, callback)
632 .map_err(|err| {
633 PluginError::RegistrationFailed(format!(
634 "tool conditional execution guardrail: {err}"
635 ))
636 })?;
637
638 let name_owned = qualified_name;
639 self.registrations.push(PluginRegistration::new(
640 "plugin",
641 name_owned.clone(),
642 Box::new(move || {
643 deregister_tool_conditional_execution_guardrail(&name_owned)
644 .map(|_| ())
645 .map_err(|err| {
646 PluginError::RegistrationFailed(format!(
647 "tool conditional execution guardrail deregistration failed: {err}"
648 ))
649 })
650 }),
651 ));
652 Ok(())
653 }
654
655 pub fn register_llm_sanitize_request_guardrail(
657 &mut self,
658 name: &str,
659 priority: i32,
660 callback: LlmSanitizeRequestFn,
661 ) -> Result<()> {
662 let qualified_name = self.qualify_name(name);
663 register_llm_sanitize_request_guardrail(&qualified_name, priority, callback).map_err(
664 |err| PluginError::RegistrationFailed(format!("llm sanitize request guardrail: {err}")),
665 )?;
666
667 let name_owned = qualified_name;
668 self.registrations.push(PluginRegistration::new(
669 "plugin",
670 name_owned.clone(),
671 Box::new(move || {
672 deregister_llm_sanitize_request_guardrail(&name_owned)
673 .map(|_| ())
674 .map_err(|err| {
675 PluginError::RegistrationFailed(format!(
676 "llm sanitize request guardrail deregistration failed: {err}"
677 ))
678 })
679 }),
680 ));
681 Ok(())
682 }
683
684 pub fn register_llm_sanitize_response_guardrail(
686 &mut self,
687 name: &str,
688 priority: i32,
689 callback: LlmSanitizeResponseFn,
690 ) -> Result<()> {
691 let qualified_name = self.qualify_name(name);
692 register_llm_sanitize_response_guardrail(&qualified_name, priority, callback).map_err(
693 |err| {
694 PluginError::RegistrationFailed(format!("llm sanitize response guardrail: {err}"))
695 },
696 )?;
697
698 let name_owned = qualified_name;
699 self.registrations.push(PluginRegistration::new(
700 "plugin",
701 name_owned.clone(),
702 Box::new(move || {
703 deregister_llm_sanitize_response_guardrail(&name_owned)
704 .map(|_| ())
705 .map_err(|err| {
706 PluginError::RegistrationFailed(format!(
707 "llm sanitize response guardrail deregistration failed: {err}"
708 ))
709 })
710 }),
711 ));
712 Ok(())
713 }
714
715 pub fn register_llm_conditional_execution_guardrail(
717 &mut self,
718 name: &str,
719 priority: i32,
720 callback: LlmConditionalFn,
721 ) -> Result<()> {
722 let qualified_name = self.qualify_name(name);
723 register_llm_conditional_execution_guardrail(&qualified_name, priority, callback).map_err(
724 |err| {
725 PluginError::RegistrationFailed(format!(
726 "llm conditional execution guardrail: {err}"
727 ))
728 },
729 )?;
730
731 let name_owned = qualified_name;
732 self.registrations.push(PluginRegistration::new(
733 "plugin",
734 name_owned.clone(),
735 Box::new(move || {
736 deregister_llm_conditional_execution_guardrail(&name_owned)
737 .map(|_| ())
738 .map_err(|err| {
739 PluginError::RegistrationFailed(format!(
740 "llm conditional execution guardrail deregistration failed: {err}"
741 ))
742 })
743 }),
744 ));
745 Ok(())
746 }
747
748 pub fn register_llm_execution_intercept(
750 &mut self,
751 name: &str,
752 priority: i32,
753 callback: LlmExecutionFn,
754 ) -> Result<()> {
755 let qualified_name = self.qualify_name(name);
756 register_llm_execution_intercept(&qualified_name, priority, callback).map_err(|err| {
757 PluginError::RegistrationFailed(format!("llm execution intercept: {err}"))
758 })?;
759
760 let name_owned = qualified_name;
761 self.registrations.push(PluginRegistration::new(
762 "plugin",
763 name_owned.clone(),
764 Box::new(move || {
765 deregister_llm_execution_intercept(&name_owned)
766 .map(|_| ())
767 .map_err(|err| {
768 PluginError::RegistrationFailed(format!(
769 "llm execution intercept deregistration failed: {err}"
770 ))
771 })
772 }),
773 ));
774 Ok(())
775 }
776
777 pub fn register_llm_stream_execution_intercept(
779 &mut self,
780 name: &str,
781 priority: i32,
782 callback: LlmStreamExecutionFn,
783 ) -> Result<()> {
784 let qualified_name = self.qualify_name(name);
785 register_llm_stream_execution_intercept(&qualified_name, priority, callback).map_err(
786 |err| PluginError::RegistrationFailed(format!("llm stream execution intercept: {err}")),
787 )?;
788
789 let name_owned = qualified_name;
790 self.registrations.push(PluginRegistration::new(
791 "plugin",
792 name_owned.clone(),
793 Box::new(move || {
794 deregister_llm_stream_execution_intercept(&name_owned)
795 .map(|_| ())
796 .map_err(|err| {
797 PluginError::RegistrationFailed(format!(
798 "llm stream execution intercept deregistration failed: {err}"
799 ))
800 })
801 }),
802 ));
803 Ok(())
804 }
805
806 pub fn register_tool_request_intercept(
808 &mut self,
809 name: &str,
810 priority: i32,
811 break_chain: bool,
812 callback: ToolInterceptFn,
813 ) -> Result<()> {
814 let qualified_name = self.qualify_name(name);
815 register_tool_request_intercept(&qualified_name, priority, break_chain, callback).map_err(
816 |err| PluginError::RegistrationFailed(format!("tool request intercept: {err}")),
817 )?;
818
819 let name_owned = qualified_name;
820 self.registrations.push(PluginRegistration::new(
821 "plugin",
822 name_owned.clone(),
823 Box::new(move || {
824 deregister_tool_request_intercept(&name_owned)
825 .map(|_| ())
826 .map_err(|err| {
827 PluginError::RegistrationFailed(format!(
828 "tool request intercept deregistration failed: {err}"
829 ))
830 })
831 }),
832 ));
833 Ok(())
834 }
835
836 pub fn register_tool_execution_intercept(
838 &mut self,
839 name: &str,
840 priority: i32,
841 callback: ToolExecutionFn,
842 ) -> Result<()> {
843 let qualified_name = self.qualify_name(name);
844 register_tool_execution_intercept(&qualified_name, priority, callback).map_err(|err| {
845 PluginError::RegistrationFailed(format!("tool execution intercept: {err}"))
846 })?;
847
848 let name_owned = qualified_name;
849 self.registrations.push(PluginRegistration::new(
850 "plugin",
851 name_owned.clone(),
852 Box::new(move || {
853 deregister_tool_execution_intercept(&name_owned)
854 .map(|_| ())
855 .map_err(|err| {
856 PluginError::RegistrationFailed(format!(
857 "tool execution intercept deregistration failed: {err}"
858 ))
859 })
860 }),
861 ));
862 Ok(())
863 }
864
865 pub fn add_registration(&mut self, registration: PluginRegistration) {
867 self.registrations.push(registration);
868 }
869
870 pub fn extend_registrations(&mut self, registrations: Vec<PluginRegistration>) {
872 self.registrations.extend(registrations);
873 }
874
875 pub fn into_registrations(self) -> Vec<PluginRegistration> {
877 self.registrations
878 }
879}
880
881pub trait Plugin: Send + Sync + 'static {
883 fn plugin_kind(&self) -> &str;
885
886 fn allows_multiple_components(&self) -> bool {
891 true
892 }
893
894 fn validate(&self, plugin_config: &Map<String, Json>) -> Vec<ConfigDiagnostic>;
899
900 fn validate_with_policy(
907 &self,
908 plugin_config: &Map<String, Json>,
909 _policy: &ConfigPolicy,
910 ) -> Vec<ConfigDiagnostic> {
911 self.validate(plugin_config)
912 }
913
914 fn register<'a>(
920 &'a self,
921 plugin_config: &Map<String, Json>,
922 ctx: &'a mut PluginRegistrationContext,
923 ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>>;
924}
925
926pub fn register_plugin(plugin: Arc<dyn Plugin>) -> Result<()> {
948 register_plugin_with_owner(plugin, PluginRegistrationOwner::External).map(|_| ())
949}
950
951pub(crate) fn register_plugin_tracked(plugin: Arc<dyn Plugin>) -> Result<u64> {
952 register_plugin_with_owner(plugin, PluginRegistrationOwner::External)
953}
954
955pub(crate) fn register_builtin_plugin(plugin: Arc<dyn Plugin>) -> Result<()> {
956 let plugin_kind = plugin.plugin_kind();
957 {
958 let guard = PLUGIN_HANDLERS.read().map_err(|err| {
959 PluginError::Internal(format!("plugin registry lock poisoned: {err}"))
960 })?;
961 if let Some(existing) = guard.get(plugin_kind) {
962 if existing.owner == PluginRegistrationOwner::Builtin {
963 return Ok(());
964 }
965 return Err(plugin_already_registered_error(
966 plugin_kind,
967 PluginRegistrationOwner::Builtin,
968 ));
969 }
970 }
971
972 register_plugin_with_owner(plugin, PluginRegistrationOwner::Builtin).map(|_| ())
973}
974
975fn plugin_already_registered_error(
976 plugin_kind: &str,
977 owner: PluginRegistrationOwner,
978) -> PluginError {
979 let ownership = if owner == PluginRegistrationOwner::Builtin {
980 "reserved builtin "
981 } else {
982 ""
983 };
984 PluginError::RegistrationFailed(format!(
985 "{ownership}plugin '{plugin_kind}' is already registered"
986 ))
987}
988
989fn register_plugin_with_owner(
990 plugin: Arc<dyn Plugin>,
991 owner: PluginRegistrationOwner,
992) -> Result<u64> {
993 let mut guard = PLUGIN_HANDLERS
994 .write()
995 .map_err(|err| PluginError::Internal(format!("plugin registry lock poisoned: {err}")))?;
996 let plugin_kind = plugin.plugin_kind().to_string();
997 if let Some(existing) = guard.get(&plugin_kind) {
998 if owner == PluginRegistrationOwner::Builtin
999 && existing.owner == PluginRegistrationOwner::Builtin
1000 {
1001 return Ok(existing.registration_id);
1002 }
1003 return Err(plugin_already_registered_error(&plugin_kind, owner));
1004 }
1005 let registration_id = NEXT_PLUGIN_REGISTRATION_ID.fetch_add(1, Ordering::Relaxed);
1006 guard.insert(
1007 plugin_kind.clone(),
1008 RegisteredPlugin {
1009 registration_id,
1010 owner,
1011 plugin,
1012 },
1013 );
1014 log::info!(
1015 target: "nemo_relay.plugin",
1016 event = "plugin_registered",
1017 plugin_kind = plugin_kind.as_str(),
1018 registration_id = registration_id;
1019 "Plugin kind registered"
1020 );
1021 Ok(registration_id)
1022}
1023
1024pub fn ensure_builtin_plugins_registered() -> Result<()> {
1029 let all_registered = {
1030 let guard = PLUGIN_HANDLERS.read().map_err(|err| {
1031 PluginError::Internal(format!("plugin registry lock poisoned: {err}"))
1032 })?;
1033 [
1034 crate::observability::plugin_component::OBSERVABILITY_PLUGIN_KIND,
1035 crate::plugins::nemo_guardrails::component::NEMO_GUARDRAILS_PLUGIN_KIND,
1036 crate::plugins::model_pricing::PRICING_PLUGIN_KIND,
1037 ]
1038 .iter()
1039 .all(|kind| {
1040 guard
1041 .get(*kind)
1042 .is_some_and(|plugin| plugin.owner == PluginRegistrationOwner::Builtin)
1043 })
1044 };
1045 if all_registered {
1046 return Ok(());
1047 }
1048
1049 crate::observability::plugin_component::register_observability_component()?;
1053 crate::plugins::nemo_guardrails::component::register_nemo_guardrails_component()?;
1054 crate::plugins::model_pricing::register_pricing_component()
1055}
1056
1057pub fn deregister_plugin(plugin_kind: &str) -> bool {
1073 deregister_plugin_checked(plugin_kind).unwrap_or(false)
1074}
1075
1076pub(crate) fn deregister_plugin_checked(plugin_kind: &str) -> Result<bool> {
1077 let removed = PLUGIN_HANDLERS
1078 .write()
1079 .map(|mut guard| guard.remove(plugin_kind).is_some())
1080 .map_err(|err| PluginError::Internal(format!("plugin registry lock poisoned: {err}")))?;
1081 if removed {
1082 log::info!(
1083 target: "nemo_relay.plugin",
1084 event = "plugin_deregistered",
1085 plugin_kind = plugin_kind;
1086 "Plugin kind deregistered"
1087 );
1088 }
1089 Ok(removed)
1090}
1091
1092pub(crate) fn deregister_plugin_registration_checked(
1093 plugin_kind: &str,
1094 expected_registration_id: u64,
1095) -> Result<PluginDeregistrationOutcome> {
1096 let mut guard = PLUGIN_HANDLERS
1097 .write()
1098 .map_err(|err| PluginError::Internal(format!("plugin registry lock poisoned: {err}")))?;
1099 match guard.get(plugin_kind) {
1100 Some(plugin) if plugin.registration_id == expected_registration_id => {
1101 guard.remove(plugin_kind);
1102 log::info!(
1103 target: "nemo_relay.plugin",
1104 event = "plugin_deregistered",
1105 plugin_kind = plugin_kind,
1106 registration_id = expected_registration_id;
1107 "Plugin kind deregistered"
1108 );
1109 Ok(PluginDeregistrationOutcome::Removed)
1110 }
1111 Some(_) => Ok(PluginDeregistrationOutcome::Replaced),
1112 None => Ok(PluginDeregistrationOutcome::Missing),
1113 }
1114}
1115
1116pub fn list_plugin_kinds() -> Vec<String> {
1128 if ensure_builtin_plugins_registered().is_err() {
1129 return Vec::new();
1130 }
1131 let mut kinds = PLUGIN_HANDLERS
1132 .read()
1133 .map(|guard| guard.keys().cloned().collect::<Vec<_>>())
1134 .unwrap_or_default();
1135 kinds.sort();
1136 kinds
1137}
1138
1139pub fn lookup_plugin(plugin_kind: &str) -> Option<Arc<dyn Plugin>> {
1151 ensure_builtin_plugins_registered().ok()?;
1152 lookup_registered_plugin(plugin_kind)
1153}
1154
1155fn lookup_registered_plugin(plugin_kind: &str) -> Option<Arc<dyn Plugin>> {
1156 PLUGIN_HANDLERS.read().ok().and_then(|guard| {
1157 guard
1158 .get(plugin_kind)
1159 .map(|registered| Arc::clone(®istered.plugin))
1160 })
1161}
1162
1163pub fn validate_plugin_config(config: &PluginConfig) -> ConfigReport {
1179 let mut report = ConfigReport::default();
1180 if let Err(error) = ensure_builtin_plugins_registered() {
1181 report.diagnostics.push(ConfigDiagnostic {
1182 level: DiagnosticLevel::Error,
1183 code: "plugin.builtin_registration_failed".to_string(),
1184 component: None,
1185 field: None,
1186 message: format!("built-in plugin registration failed: {error}"),
1187 });
1188 return report;
1189 }
1190
1191 if config.version != 1 {
1192 push_policy_diag(
1193 &mut report.diagnostics,
1194 config.policy.unsupported_value,
1195 "plugin.unsupported_config_version",
1196 None,
1197 Some("version".to_string()),
1198 format!("plugin config version {} is unsupported", config.version),
1199 );
1200 }
1201
1202 validate_plugin_multiplicity(&mut report, config);
1203
1204 for component in &config.components {
1205 let Some(plugin) = lookup_registered_plugin(&component.kind) else {
1206 push_policy_diag(
1207 &mut report.diagnostics,
1208 config.policy.unknown_component,
1209 "plugin.unknown_component",
1210 Some(component.kind.clone()),
1211 None,
1212 format!("plugin component kind '{}' is unsupported", component.kind),
1213 );
1214 continue;
1215 };
1216 report
1217 .diagnostics
1218 .extend(plugin.validate_with_policy(&component.config, &config.policy));
1219 }
1220
1221 report
1222}
1223
1224fn layer_config(left: &mut Json, right: Json) {
1234 match (left, right) {
1235 (Json::Object(left), Json::Object(right)) => {
1236 for (key, value) in right {
1237 match (key.as_str(), left.get_mut(&key)) {
1238 ("components", Some(existing)) => merge_plugin_components(existing, value),
1239 (_, Some(existing)) => merge_json_value(existing, value),
1240 (_, _) => {
1241 left.insert(key, value);
1242 }
1243 }
1244 }
1245 }
1246 (left, right) => *left = right,
1247 }
1248}
1249
1250fn merge_plugin_components(left: &mut Json, right: Json) {
1252 let Json::Array(left_components) = left else {
1253 *left = right;
1254 return;
1255 };
1256 let Json::Array(right_components) = right else {
1257 *left = right;
1258 return;
1259 };
1260 let base_component_count = left_components.len();
1261 let mut consumed: HashMap<String, usize> = HashMap::new();
1262 for component in right_components {
1263 let Some(kind) = component_kind(&component).map(str::to_owned) else {
1264 left_components.push(component);
1265 continue;
1266 };
1267 let nth = consumed.entry(kind.clone()).or_insert(0);
1268 let slot = nth_component_by_kind(&left_components[..base_component_count], &kind, *nth);
1269 *nth += 1;
1270 match slot {
1271 Some(index) => merge_plugin_component(&mut left_components[index], component),
1272 None => left_components.push(component),
1273 }
1274 }
1275}
1276
1277fn merge_plugin_component(existing: &mut Json, higher_priority: Json) {
1283 let is_observability = component_kind(&higher_priority).or_else(|| component_kind(existing))
1284 == Some("observability");
1285 match (existing, higher_priority) {
1286 (Json::Object(existing), Json::Object(higher_priority)) => {
1287 for (key, value) in higher_priority {
1288 match (key.as_str(), existing.get_mut(&key)) {
1289 ("config", Some(existing_config)) => {
1290 merge_plugin_config_value(
1291 existing_config,
1292 value,
1293 &mut Vec::new(),
1294 is_observability,
1295 );
1296 }
1297 (_, Some(existing_value)) => merge_json_value(existing_value, value),
1298 (_, None) => {
1299 existing.insert(key, value);
1300 }
1301 }
1302 }
1303 }
1304 (existing, higher_priority) => *existing = higher_priority,
1305 }
1306}
1307
1308fn merge_plugin_config_value(
1310 lower_priority: &mut Json,
1311 higher_priority: Json,
1312 path: &mut Vec<String>,
1313 is_observability: bool,
1314) {
1315 match (lower_priority, higher_priority) {
1316 (Json::Object(lower_priority), Json::Object(higher_priority)) => {
1317 for (key, value) in higher_priority {
1318 path.push(key.clone());
1319 match lower_priority.get_mut(&key) {
1320 Some(existing) => {
1321 merge_plugin_config_value(existing, value, path, is_observability)
1322 }
1323 None => {
1324 lower_priority.insert(key, value);
1325 }
1326 }
1327 path.pop();
1328 }
1329 }
1330 (Json::Array(lower_priority), Json::Array(mut higher_priority))
1331 if plugin_config_list_concatenates(path, is_observability) =>
1332 {
1333 higher_priority.append(lower_priority);
1334 *lower_priority = higher_priority;
1335 }
1336 (lower_priority, higher_priority) => *lower_priority = higher_priority,
1337 }
1338}
1339
1340fn plugin_config_list_concatenates(path: &[String], is_observability: bool) -> bool {
1341 path.len() == 1
1342 || (is_observability
1343 && matches!(
1344 path,
1345 [section, field]
1346 if (section == "atof" && field == "sinks")
1347 || (section == "opentelemetry" && field == "endpoints")
1348 || (section == "atif" && field == "storage")
1349 ))
1350}
1351
1352fn merge_json_value(left: &mut Json, right: Json) {
1354 match (left, right) {
1355 (Json::Object(left), Json::Object(right)) => {
1356 for (key, value) in right {
1357 match left.get_mut(&key) {
1358 Some(existing) => merge_json_value(existing, value),
1359 None => {
1360 left.insert(key, value);
1361 }
1362 }
1363 }
1364 }
1365 (left, right) => *left = right,
1366 }
1367}
1368
1369fn component_kind(component: &Json) -> Option<&str> {
1370 component.get("kind").and_then(Json::as_str)
1371}
1372
1373fn nth_component_by_kind(components: &[Json], kind: &str, nth: usize) -> Option<usize> {
1374 components
1375 .iter()
1376 .enumerate()
1377 .filter(|(_index, component)| component_kind(component) == Some(kind))
1378 .nth(nth)
1379 .map(|(index, _component)| index)
1380}
1381
1382#[cfg(feature = "schema")]
1384pub fn plugin_config_schema() -> Json {
1385 serde_json::to_value(schemars::schema_for!(PluginConfig))
1386 .expect("plugin config schema should serialize")
1387}
1388
1389#[doc(hidden)]
1410pub async fn initialize_plugins_exact(config: PluginConfig) -> Result<ConfigReport> {
1411 initialize_plugins_with_diagnostics(config, Vec::new()).await
1412}
1413
1414async fn initialize_plugins_with_diagnostics(
1415 config: PluginConfig,
1416 diagnostics: Vec<ConfigDiagnostic>,
1417) -> Result<ConfigReport> {
1418 run_owned_plugin_mutation("plugin initialization", move || async move {
1419 let lease = LegacyPluginMutationLease::acquire()?;
1420 let rollback_failures = Arc::new(Mutex::new(Vec::new()));
1421 let initialization = tokio::spawn(initialize_plugins_exact_inner(
1422 config,
1423 Some(Arc::clone(&rollback_failures)),
1424 diagnostics,
1425 ))
1426 .await
1427 .map_err(|error| {
1428 PluginError::Internal(format!("plugin initialization task failed: {error}"))
1429 });
1430 let result = initialization.and_then(|result| result);
1431 let failures = rollback_failures
1432 .lock()
1433 .map(|failures| failures.clone())
1434 .unwrap_or_else(|lock_error| {
1435 vec![format!("rollback failure lock poisoned: {lock_error}")]
1436 });
1437 match result {
1438 Err(error) if !failures.is_empty() => {
1439 std::mem::forget(lease);
1440 Err(PluginError::RegistrationFailed(format!(
1441 concat!(
1442 "{}; initialization rollback was incomplete: {}; plugin ",
1443 "configuration mutations are disabled for this process because callbacks ",
1444 "may remain registered"
1445 ),
1446 error,
1447 failures.join("; ")
1448 )))
1449 }
1450 result => result,
1451 }
1452 })
1453 .await
1454}
1455
1456pub(crate) async fn run_owned_plugin_mutation<T, F, Fut>(
1457 operation_name: &'static str,
1458 operation: F,
1459) -> Result<T>
1460where
1461 T: Send + 'static,
1462 F: FnOnce() -> Fut + Send + 'static,
1463 Fut: Future<Output = Result<T>> + Send + 'static,
1464{
1465 if IN_PLUGIN_MUTATION_EXECUTOR.get() {
1466 return operation().await;
1467 }
1468
1469 let (result_tx, result_rx) = tokio::sync::oneshot::channel();
1470 plugin_mutation_executor()?
1471 .send(Box::pin(async move {
1472 let result = tokio::spawn(operation())
1473 .await
1474 .map_err(|error| {
1475 PluginError::Internal(format!("{operation_name} task failed: {error}"))
1476 })
1477 .and_then(|result| result);
1478 let _ = result_tx.send(result);
1479 }))
1480 .map_err(|_| {
1481 PluginError::Internal(format!(
1482 "failed to queue {operation_name}: executor stopped"
1483 ))
1484 })?;
1485 result_rx.await.map_err(|_| {
1486 PluginError::Internal(format!(
1487 "{operation_name} task stopped before returning a result"
1488 ))
1489 })?
1490}
1491
1492fn plugin_mutation_executor() -> Result<&'static PluginMutationSender> {
1493 if let Some(sender) = PLUGIN_MUTATION_EXECUTOR.get() {
1494 return Ok(sender);
1495 }
1496
1497 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel::<PluginMutationJob>();
1498 let (startup_tx, startup_rx) = std::sync::mpsc::sync_channel(1);
1499 std::thread::Builder::new()
1500 .name("nemo-relay-plugin-host".into())
1501 .spawn(move || {
1502 let runtime = match tokio::runtime::Builder::new_current_thread()
1503 .enable_all()
1504 .build()
1505 {
1506 Ok(runtime) => runtime,
1507 Err(error) => {
1508 let _ = startup_tx.send(Err(error.to_string()));
1509 return;
1510 }
1511 };
1512 let _ = startup_tx.send(Ok(()));
1513 IN_PLUGIN_MUTATION_EXECUTOR.set(true);
1514 runtime.block_on(async move {
1515 while let Some(job) = receiver.recv().await {
1516 job.await;
1517 }
1518 });
1519 })
1520 .map_err(|error| {
1521 PluginError::Internal(format!("failed to start plugin host executor: {error}"))
1522 })?;
1523 startup_rx
1524 .recv()
1525 .map_err(|error| {
1526 PluginError::Internal(format!(
1527 "plugin host executor stopped during startup: {error}"
1528 ))
1529 })?
1530 .map_err(|error| {
1531 PluginError::Internal(format!("failed to start plugin host runtime: {error}"))
1532 })?;
1533
1534 Ok(PLUGIN_MUTATION_EXECUTOR.get_or_init(|| sender))
1535}
1536
1537pub(crate) async fn initialize_plugins_exact_for_host(
1538 config: PluginConfig,
1539 owner_id: u64,
1540 rollback_failures: Arc<Mutex<Vec<String>>>,
1541 diagnostics: Vec<ConfigDiagnostic>,
1542) -> Result<ConfigReport> {
1543 verify_plugin_host_owner(owner_id)?;
1544 initialize_plugins_exact_inner(config, Some(rollback_failures), diagnostics).await
1545}
1546
1547async fn initialize_plugins_exact_inner(
1548 config: PluginConfig,
1549 rollback_failures: Option<Arc<Mutex<Vec<String>>>>,
1550 diagnostics: Vec<ConfigDiagnostic>,
1551) -> Result<ConfigReport> {
1552 let enabled_component_count = config
1553 .components
1554 .iter()
1555 .filter(|component| component.enabled)
1556 .count();
1557 log::info!(
1558 target: "nemo_relay.plugin",
1559 event = "plugin_configuration_activation_started",
1560 component_count = enabled_component_count;
1561 "Plugin configuration activation started"
1562 );
1563 let mut report = ConfigReport {
1564 diagnostics,
1565 ..ConfigReport::default()
1566 };
1567 report
1568 .diagnostics
1569 .extend(validate_plugin_config(&config).diagnostics);
1570 if report.has_errors() {
1571 return Err(PluginError::InvalidConfig(join_error_messages(&report)));
1572 }
1573
1574 let previous = {
1575 let mut guard = ACTIVE_PLUGIN_CONFIGURATION.lock().map_err(|err| {
1576 PluginError::Internal(format!("active plugin configuration lock poisoned: {err}"))
1577 })?;
1578 guard.take()
1579 };
1580
1581 if let Some(mut previous_state) = previous {
1582 {
1585 let mut guard = ACTIVE_PLUGIN_CONFIGURATION.lock().map_err(|err| {
1586 PluginError::Internal(format!("active plugin configuration lock poisoned: {err}"))
1587 })?;
1588 *guard = Some(ActivePluginConfiguration {
1589 config: previous_state.config.clone(),
1590 report: previous_state.report.clone(),
1591 registrations: Vec::new(),
1592 });
1593 }
1594 let teardown = rollback_registrations_checked(&mut previous_state.registrations);
1595 let teardown_report = ACTIVE_PLUGIN_CONFIGURATION
1596 .lock()
1597 .map_err(|err| {
1598 PluginError::Internal(format!("active plugin configuration lock poisoned: {err}"))
1599 })?
1600 .take()
1601 .map(|state| state.report);
1602 if !teardown.errors.is_empty() {
1603 if let Some(report) =
1604 teardown_report.filter(|report| !report.runtime_diagnostics.is_empty())
1605 && let Ok(mut guard) = LAST_FAILED_RUNTIME_DIAGNOSTICS_REPORT.lock()
1606 {
1607 *guard = Some(report);
1608 }
1609 if !teardown.callbacks_cleared {
1610 record_rollback_failures(rollback_failures.as_ref(), teardown.errors.clone());
1611 }
1612 return Err(PluginError::RegistrationFailed(format!(
1613 "previous plugin configuration could not be cleared: {}",
1614 teardown.errors.join("; ")
1615 )));
1616 }
1617 match initialize_plugin_components_catching_panics(
1618 config.clone(),
1619 rollback_failures.clone(),
1620 )
1621 .await
1622 {
1623 Ok(registrations) => {
1624 store_active_plugin_configuration(config, report.clone(), registrations)?;
1625 log::info!(
1626 target: "nemo_relay.plugin",
1627 event = "plugin_configuration_replaced",
1628 component_count = enabled_component_count;
1629 "Plugin configuration replaced"
1630 );
1631 Ok(report)
1632 }
1633 Err(err) => match initialize_plugin_components_catching_panics(
1634 previous_state.config.clone(),
1635 rollback_failures.clone(),
1636 )
1637 .await
1638 {
1639 Ok(registrations) => {
1640 store_active_plugin_configuration(
1641 previous_state.config,
1642 previous_state.report,
1643 registrations,
1644 )?;
1645 log::warn!(
1646 target: "nemo_relay.plugin",
1647 event = "plugin_configuration_restored",
1648 recovery = "previous_configuration";
1649 "Plugin activation failed; previous configuration restored"
1650 );
1651 Err(err)
1652 }
1653 Err(restore_err) => {
1654 log::error!(
1655 target: "nemo_relay.plugin",
1656 event = "plugin_rollback_failed",
1657 recovery = "previous_configuration";
1658 "Plugin activation failed and the previous configuration could not be restored"
1659 );
1660 Err(PluginError::RegistrationFailed(format!(
1661 "{err}; previous plugin configuration could not be restored: {restore_err}"
1662 )))
1663 }
1664 },
1665 }
1666 } else {
1667 let registrations =
1668 initialize_plugin_components_catching_panics(config.clone(), rollback_failures).await?;
1669 store_active_plugin_configuration(config, report.clone(), registrations)?;
1670 log::info!(
1671 target: "nemo_relay.plugin",
1672 event = "plugin_configuration_activated",
1673 component_count = enabled_component_count;
1674 "Plugin configuration activated"
1675 );
1676 Ok(report)
1677 }
1678}
1679
1680async fn initialize_plugin_components_catching_panics(
1681 config: PluginConfig,
1682 rollback_failures: Option<Arc<Mutex<Vec<String>>>>,
1683) -> Result<Vec<PluginRegistration>> {
1684 tokio::spawn(async move { initialize_plugin_components(&config, rollback_failures).await })
1685 .await
1686 .map_err(|error| {
1687 PluginError::Internal(format!(
1688 "plugin component initialization task failed: {error}"
1689 ))
1690 })?
1691}
1692
1693pub async fn initialize_plugins(config: PluginConfig) -> Result<ConfigReport> {
1703 let resolved = resolve_plugin_config(config)?;
1704 initialize_plugins_with_diagnostics(resolved.config, resolved.diagnostics).await
1705}
1706
1707pub(crate) fn resolve_plugin_config(config: PluginConfig) -> Result<ResolvedPluginConfig> {
1712 let discovered = resolve_default_file_plugin_config()?;
1713 resolve_programmatic_plugin_config(discovered, config)
1714}
1715
1716fn resolve_programmatic_plugin_config(
1717 discovered: DiscoveredPluginConfig,
1718 config: PluginConfig,
1719) -> Result<ResolvedPluginConfig> {
1720 let mut diagnostics = inherited_plugin_config_diagnostics(&discovered.sources);
1721 diagnostics.extend(programmatic_enable_override_diagnostics(
1722 &discovered.value,
1723 &discovered.enabled_sources,
1724 &config,
1725 ));
1726 let mut base = discovered.value;
1727 layer_config(&mut base, plugin_config_overlay_value(&config)?);
1728 Ok(ResolvedPluginConfig {
1729 config: serde_json::from_value(base)?,
1730 diagnostics,
1731 })
1732}
1733
1734pub(crate) struct ResolvedPluginConfig {
1735 pub(crate) config: PluginConfig,
1736 pub(crate) diagnostics: Vec<ConfigDiagnostic>,
1737}
1738
1739fn plugin_config_overlay_value(config: &PluginConfig) -> Result<Json> {
1748 let mut overlay = serde_json::to_value(config)?;
1749 let Json::Object(root) = &mut overlay else {
1750 return Ok(overlay);
1751 };
1752
1753 if config.version == default_plugin_config_version() {
1754 root.remove("version");
1755 }
1756
1757 remove_default_policy_overlay(root, &config.policy);
1758
1759 Ok(overlay)
1760}
1761
1762fn remove_default_policy_overlay(root: &mut Map<String, Json>, config: &ConfigPolicy) {
1763 let Some(Json::Object(policy)) = root.get_mut("policy") else {
1764 return;
1765 };
1766 let defaults = ConfigPolicy::default();
1767 for (field, is_default) in [
1768 (
1769 "unknown_component",
1770 config.unknown_component == defaults.unknown_component,
1771 ),
1772 (
1773 "unknown_field",
1774 config.unknown_field == defaults.unknown_field,
1775 ),
1776 (
1777 "unsupported_value",
1778 config.unsupported_value == defaults.unsupported_value,
1779 ),
1780 ] {
1781 if is_default {
1782 policy.remove(field);
1783 }
1784 }
1785 if policy.is_empty() {
1786 root.remove("policy");
1787 }
1788}
1789
1790fn resolve_default_file_plugin_config() -> Result<DiscoveredPluginConfig> {
1793 let paths =
1794 default_plugin_config_paths(std::env::current_dir().ok().as_deref(), user_config_dir());
1795 let documents = read_plugin_config_files(paths)?;
1796 resolve_discovered_plugin_config(documents)
1797}
1798
1799fn resolve_discovered_plugin_config(
1800 documents: Vec<(PathBuf, Json)>,
1801) -> Result<DiscoveredPluginConfig> {
1802 let enabled_sources = component_enabled_sources(&documents);
1803 let (value, sources) = merge_plugin_config_documents(documents)?
1804 .unwrap_or_else(|| (Json::Object(Map::new()), Vec::new()));
1805 Ok(DiscoveredPluginConfig {
1806 value,
1807 enabled_sources,
1808 sources,
1809 })
1810}
1811
1812struct DiscoveredPluginConfig {
1813 value: Json,
1814 enabled_sources: HashMap<String, ComponentEnabledSource>,
1815 sources: Vec<PathBuf>,
1816}
1817
1818struct ComponentEnabledSource {
1819 enabled: bool,
1820 path: PathBuf,
1821}
1822
1823use std::path::{Path, PathBuf};
1824
1825#[doc(hidden)]
1829pub fn load_plugin_config_files<I>(paths: I) -> Result<Option<(Json, Vec<PathBuf>)>>
1830where
1831 I: IntoIterator<Item = PathBuf>,
1832{
1833 merge_plugin_config_documents(read_plugin_config_files(paths)?)
1834}
1835
1836fn read_plugin_config_files<I>(paths: I) -> Result<Vec<(PathBuf, Json)>>
1837where
1838 I: IntoIterator<Item = PathBuf>,
1839{
1840 let mut documents = Vec::new();
1841 for path in deduplicate_plugin_config_paths(paths) {
1842 if !path.exists() {
1843 continue;
1844 }
1845 let raw = std::fs::read_to_string(&path).map_err(|err| {
1846 PluginError::InvalidConfig(format!("failed to read {}: {err}", path.display()))
1847 })?;
1848 let parsed = raw.parse::<toml::Table>().map_err(|err| {
1849 PluginError::InvalidConfig(format!("invalid plugin TOML in {}: {err}", path.display()))
1850 })?;
1851 documents.push((path, serde_json::to_value(parsed)?));
1852 }
1853 Ok(documents)
1854}
1855
1856fn component_enabled_sources(
1857 documents: &[(PathBuf, Json)],
1858) -> HashMap<String, ComponentEnabledSource> {
1859 let mut sources = HashMap::new();
1860 for (path, document) in documents {
1861 let Some(components) = document.get("components").and_then(Json::as_array) else {
1862 continue;
1863 };
1864 for component in components {
1865 let Some(kind) = component_kind(component) else {
1866 continue;
1867 };
1868 if let Some(enabled) = component.get("enabled").and_then(Json::as_bool) {
1869 sources.insert(
1870 kind.to_string(),
1871 ComponentEnabledSource {
1872 enabled,
1873 path: path.clone(),
1874 },
1875 );
1876 }
1877 }
1878 }
1879 sources
1880}
1881
1882fn inherited_plugin_config_diagnostics(sources: &[PathBuf]) -> Vec<ConfigDiagnostic> {
1883 sources
1884 .iter()
1885 .map(|source| {
1886 let source = source.display().to_string();
1887 log::warn!(
1888 target: "nemo_relay.plugin",
1889 event = "plugin_configuration_inherited",
1890 config_path = source.as_str();
1891 "Inherited plugin configuration from discovered file"
1892 );
1893 ConfigDiagnostic {
1894 level: DiagnosticLevel::Warning,
1895 code: "plugin.configuration_inherited".to_string(),
1896 component: None,
1897 field: None,
1898 message: format!("inherited plugin configuration from discovered file: {source}"),
1899 }
1900 })
1901 .collect()
1902}
1903
1904fn programmatic_enable_override_diagnostics(
1905 discovered: &Json,
1906 enabled_sources: &HashMap<String, ComponentEnabledSource>,
1907 programmatic: &PluginConfig,
1908) -> Vec<ConfigDiagnostic> {
1909 let Some(discovered_components) = discovered.get("components").and_then(Json::as_array) else {
1910 return Vec::new();
1911 };
1912 let mut consumed = HashMap::new();
1913 let mut diagnostics = Vec::new();
1914 for component in &programmatic.components {
1915 let nth = consumed.entry(component.kind.as_str()).or_insert(0usize);
1916 let discovered_component =
1917 nth_component_by_kind(discovered_components, &component.kind, *nth)
1918 .and_then(|index| discovered_components.get(index));
1919 *nth += 1;
1920 let discovered_enabled = discovered_component
1921 .and_then(|component| component.get("enabled"))
1922 .and_then(Json::as_bool);
1923 let file_disabled = discovered_enabled == Some(false)
1924 || (discovered_enabled.is_none()
1925 && enabled_sources
1926 .get(&component.kind)
1927 .is_some_and(|source| !source.enabled));
1928 if !component.enabled || !file_disabled {
1929 continue;
1930 }
1931
1932 let source = enabled_sources
1933 .get(&component.kind)
1934 .map(|source| format!(" from {}", source.path.display()))
1935 .unwrap_or_default();
1936 diagnostics.push(ConfigDiagnostic {
1937 level: DiagnosticLevel::Warning,
1938 code: "plugin.component_reenabled".to_string(),
1939 component: Some(component.kind.clone()),
1940 field: Some("enabled".to_string()),
1941 message: format!(
1942 "programmatic configuration enabled plugin component '{}' and overrode enabled = false{source}",
1943 component.kind
1944 ),
1945 });
1946 }
1947 diagnostics
1948}
1949
1950#[doc(hidden)]
1953pub fn deduplicate_plugin_config_paths<I>(paths: I) -> Vec<PathBuf>
1954where
1955 I: IntoIterator<Item = PathBuf>,
1956{
1957 let paths = paths.into_iter().collect::<Vec<_>>();
1958 let mut seen = HashSet::new();
1959 let mut unique = Vec::with_capacity(paths.len());
1960 for path in paths.into_iter().rev() {
1961 let identity = path.canonicalize().unwrap_or_else(|_| path.clone());
1962 if seen.insert(identity) {
1963 unique.push(path);
1964 }
1965 }
1966 unique.reverse();
1967 unique
1968}
1969
1970#[doc(hidden)]
1974pub fn merge_plugin_config_documents<I>(documents: I) -> Result<Option<(Json, Vec<PathBuf>)>>
1975where
1976 I: IntoIterator<Item = (PathBuf, Json)>,
1977{
1978 let mut merged = Json::Object(Map::new());
1979 let mut sources = Vec::new();
1980 for (path, mut document) in documents {
1981 validate_plugin_config_version(&path, &document)?;
1982 validate_unique_component_kinds(&path, &document)?;
1983
1984 filter_disabled_plugin_components(&mut document);
1985 layer_config(&mut merged, document);
1986 sources.push(path);
1987 }
1988 Ok((!sources.is_empty()).then_some((merged, sources)))
1989}
1990
1991fn filter_disabled_plugin_components(document: &mut Json) {
1993 let Some(components) = document.get_mut("components").and_then(Json::as_array_mut) else {
1994 return;
1995 };
1996 components.retain(|component| component.get("enabled").and_then(Json::as_bool) != Some(false));
1997}
1998
1999fn validate_plugin_config_version(path: &Path, document: &Json) -> Result<()> {
2002 let Some(raw_version) = document.get("version") else {
2003 return Ok(());
2004 };
2005 let version = serde_json::from_value::<u32>(raw_version.clone()).map_err(|error| {
2006 PluginError::InvalidConfig(format!(
2007 "invalid plugin config version in {}: {error}",
2008 path.display()
2009 ))
2010 })?;
2011 if version == default_plugin_config_version() {
2012 return Ok(());
2013 }
2014 Err(PluginError::InvalidConfig(format!(
2015 "plugin config version {version} in {} is unsupported; expected {}",
2016 path.display(),
2017 default_plugin_config_version()
2018 )))
2019}
2020
2021fn validate_unique_component_kinds(path: &Path, document: &Json) -> Result<()> {
2023 let Some(components) = document.get("components").and_then(Json::as_array) else {
2024 return Ok(());
2025 };
2026 let mut seen = HashSet::new();
2027 let mut duplicates = Vec::new();
2028 for component in components {
2029 if let Some(kind) = component_kind(component)
2030 && !seen.insert(kind)
2031 {
2032 duplicates.push(kind.to_string());
2033 }
2034 }
2035 if duplicates.is_empty() {
2036 return Ok(());
2037 }
2038 duplicates.sort();
2039 duplicates.dedup();
2040 Err(PluginError::InvalidConfig(format!(
2041 "duplicate plugin component kind in {}: {}; declare each kind once per plugins.toml",
2042 path.display(),
2043 duplicates.join(", ")
2044 )))
2045}
2046
2047#[doc(hidden)]
2051pub fn default_plugin_config_paths(cwd: Option<&Path>, user_dir: Option<PathBuf>) -> Vec<PathBuf> {
2052 let mut paths = Vec::new();
2053 if let Some(dir) = user_dir {
2054 paths.push(dir.join("plugins.toml"));
2055 }
2056 if let Some(cwd) = cwd
2057 && let Some(project) = nearest_project_plugin_config(cwd)
2058 {
2059 paths.push(project);
2060 }
2061 paths.push(PathBuf::from("/etc/nemo-relay/plugins.toml"));
2062 paths
2063}
2064
2065#[doc(hidden)]
2068pub fn nearest_project_plugin_config(start: &Path) -> Option<PathBuf> {
2069 start
2070 .ancestors()
2071 .map(|ancestor| ancestor.join(".nemo-relay").join("plugins.toml"))
2072 .find(|path| path.exists())
2073}
2074
2075#[doc(hidden)]
2078pub fn user_config_dir() -> Option<PathBuf> {
2079 if let Some(base) = std::env::var_os("XDG_CONFIG_HOME") {
2080 return Some(PathBuf::from(base).join("nemo-relay"));
2081 }
2082 std::env::var_os("HOME")
2083 .or_else(|| std::env::var_os("USERPROFILE"))
2084 .map(|home| PathBuf::from(home).join(".config/nemo-relay"))
2085}
2086
2087pub fn clear_plugin_configuration() -> Result<()> {
2103 let lease = LegacyPluginMutationLease::acquire()?;
2104 let outcome = clear_plugin_configuration_inner();
2105 if !outcome.callbacks_cleared {
2106 std::mem::forget(lease);
2109 log::error!(
2110 target: "nemo_relay.plugin",
2111 event = "plugin_cleanup_failed",
2112 callbacks_cleared = false;
2113 "Plugin configuration cleanup was incomplete"
2114 );
2115 return Err(PluginError::RegistrationFailed(format!(
2116 concat!(
2117 "{}; plugin configuration mutations are disabled for this process because ",
2118 "callbacks may remain registered"
2119 ),
2120 outcome
2121 .result
2122 .err()
2123 .map(|error| error.to_string())
2124 .unwrap_or_else(|| "plugin teardown was incomplete".into())
2125 )));
2126 }
2127 if outcome.result.is_ok() {
2128 log::info!(
2129 target: "nemo_relay.plugin",
2130 event = "plugin_configuration_cleared";
2131 "Plugin configuration cleared"
2132 );
2133 }
2134 outcome.result
2135}
2136
2137pub(crate) fn clear_plugin_configuration_for_host(owner_id: u64) -> PluginHostClearOutcome {
2138 if let Err(error) = verify_plugin_host_owner(owner_id) {
2139 return PluginHostClearOutcome {
2140 result: Err(error),
2141 callbacks_cleared: false,
2142 };
2143 }
2144 clear_plugin_configuration_inner()
2145}
2146
2147pub(crate) struct PluginHostClearOutcome {
2148 pub(crate) result: Result<()>,
2149 pub(crate) callbacks_cleared: bool,
2150}
2151
2152fn clear_plugin_configuration_inner() -> PluginHostClearOutcome {
2153 let flush_error = crate::api::runtime::subscriber_dispatcher::flush_queued_subscribers()
2154 .err()
2155 .map(|error| error.to_string());
2156 let mut registrations = {
2157 let mut guard = match ACTIVE_PLUGIN_CONFIGURATION.lock() {
2158 Ok(guard) => guard,
2159 Err(err) => {
2160 return PluginHostClearOutcome {
2161 result: Err(PluginError::Internal(format!(
2162 "active plugin configuration lock poisoned: {err}"
2163 ))),
2164 callbacks_cleared: false,
2165 };
2166 }
2167 };
2168 guard
2169 .as_mut()
2170 .map(|state| std::mem::take(&mut state.registrations))
2171 };
2172 let deregistration = registrations
2175 .as_mut()
2176 .map(rollback_registrations_checked)
2177 .unwrap_or_default();
2178 let teardown_report = match ACTIVE_PLUGIN_CONFIGURATION.lock() {
2179 Ok(mut guard) => guard.take().map(|state| state.report),
2180 Err(err) => {
2181 return PluginHostClearOutcome {
2182 result: Err(PluginError::Internal(format!(
2183 "active plugin configuration lock poisoned: {err}"
2184 ))),
2185 callbacks_cleared: false,
2186 };
2187 }
2188 };
2189 let deregistration_error = (!deregistration.errors.is_empty()).then(|| {
2190 PluginError::RegistrationFailed(format!(
2191 "plugin teardown failed: {}",
2192 deregistration.errors.join("; ")
2193 ))
2194 });
2195 let result = match (flush_error, deregistration_error) {
2196 (None, None) => Ok(()),
2197 (Some(flush), None) => Err(PluginError::Internal(flush)),
2198 (None, Some(deregister)) => Err(deregister),
2199 (Some(flush), Some(deregister)) => Err(PluginError::RegistrationFailed(format!(
2200 "{deregister}; subscriber flush also failed: {flush}"
2201 ))),
2202 };
2203 if result.is_ok() {
2204 if let Ok(mut guard) = LAST_FAILED_RUNTIME_DIAGNOSTICS_REPORT.lock() {
2205 *guard = None;
2206 }
2207 } else if let Some(report) =
2208 teardown_report.filter(|report| !report.runtime_diagnostics.is_empty())
2209 && let Ok(mut guard) = LAST_FAILED_RUNTIME_DIAGNOSTICS_REPORT.lock()
2210 {
2211 *guard = Some(report);
2212 }
2213 PluginHostClearOutcome {
2214 result,
2215 callbacks_cleared: deregistration.callbacks_cleared,
2216 }
2217}
2218
2219pub(crate) fn plugin_configuration_is_active() -> Result<bool> {
2220 ACTIVE_PLUGIN_CONFIGURATION
2221 .lock()
2222 .map(|guard| guard.is_some())
2223 .map_err(|err| {
2224 PluginError::Internal(format!("active plugin configuration lock poisoned: {err}"))
2225 })
2226}
2227
2228pub(crate) struct PluginHostLease {
2229 owner_id: u64,
2230}
2231
2232impl PluginHostLease {
2233 pub(crate) fn owner_id(&self) -> u64 {
2234 self.owner_id
2235 }
2236}
2237
2238impl Drop for PluginHostLease {
2239 fn drop(&mut self) {
2240 if let Ok(mut owner) = PLUGIN_MUTATION_OWNER.lock()
2241 && *owner == PluginMutationOwner::Host(self.owner_id)
2242 {
2243 *owner = PluginMutationOwner::Idle;
2244 }
2245 }
2246}
2247
2248pub(crate) fn acquire_plugin_host_lease() -> Result<PluginHostLease> {
2249 let mut owner = PLUGIN_MUTATION_OWNER.lock().map_err(|err| {
2250 PluginError::Internal(format!("plugin mutation owner lock poisoned: {err}"))
2251 })?;
2252 if *owner != PluginMutationOwner::Idle {
2253 return Err(plugin_mutation_conflict(*owner));
2254 }
2255 if plugin_configuration_is_active()? {
2256 return Err(PluginError::Conflict(
2257 concat!(
2258 "a static plugin configuration is already active; to combine static and ",
2259 "dynamic plugins, provide the static components as the base configuration to ",
2260 "dynamic plugin activation before calling plugin initialization"
2261 )
2262 .into(),
2263 ));
2264 }
2265 let owner_id = NEXT_PLUGIN_HOST_OWNER_ID.fetch_add(1, Ordering::Relaxed);
2266 *owner = PluginMutationOwner::Host(owner_id);
2267 Ok(PluginHostLease { owner_id })
2268}
2269
2270fn verify_plugin_host_owner(owner_id: u64) -> Result<()> {
2271 let owner = PLUGIN_MUTATION_OWNER.lock().map_err(|err| {
2272 PluginError::Internal(format!("plugin mutation owner lock poisoned: {err}"))
2273 })?;
2274 if *owner == PluginMutationOwner::Host(owner_id) {
2275 Ok(())
2276 } else {
2277 Err(PluginError::Conflict(
2278 "dynamic plugin host no longer owns plugin configuration".into(),
2279 ))
2280 }
2281}
2282
2283struct LegacyPluginMutationLease;
2284
2285impl LegacyPluginMutationLease {
2286 fn acquire() -> Result<Self> {
2287 let mut owner = PLUGIN_MUTATION_OWNER.lock().map_err(|err| {
2288 PluginError::Internal(format!("plugin mutation owner lock poisoned: {err}"))
2289 })?;
2290 if *owner != PluginMutationOwner::Idle {
2291 return Err(plugin_mutation_conflict(*owner));
2292 }
2293 *owner = PluginMutationOwner::Legacy;
2294 Ok(Self)
2295 }
2296}
2297
2298impl Drop for LegacyPluginMutationLease {
2299 fn drop(&mut self) {
2300 if let Ok(mut owner) = PLUGIN_MUTATION_OWNER.lock()
2301 && *owner == PluginMutationOwner::Legacy
2302 {
2303 *owner = PluginMutationOwner::Idle;
2304 }
2305 }
2306}
2307
2308fn plugin_mutation_conflict(owner: PluginMutationOwner) -> PluginError {
2309 let message = match owner {
2310 PluginMutationOwner::Idle => "plugin configuration is available",
2311 PluginMutationOwner::Legacy => "another plugin configuration mutation is in progress",
2312 PluginMutationOwner::Host(_) => {
2313 "plugin configuration is owned by an active dynamic plugin host"
2314 }
2315 };
2316 PluginError::Conflict(message.into())
2317}
2318
2319pub fn active_plugin_report() -> Option<ConfigReport> {
2332 let active_report = ACTIVE_PLUGIN_CONFIGURATION
2333 .lock()
2334 .ok()
2335 .and_then(|guard| guard.as_ref().map(|state| state.report.clone()));
2336 active_report.or_else(|| {
2337 LAST_FAILED_RUNTIME_DIAGNOSTICS_REPORT
2338 .lock()
2339 .ok()
2340 .and_then(|guard| guard.clone())
2341 })
2342}
2343
2344pub fn record_active_plugin_runtime_diagnostic(diagnostic: RuntimeDiagnostic) {
2346 let Ok(mut guard) = ACTIVE_PLUGIN_CONFIGURATION.lock() else {
2347 return;
2348 };
2349 let Some(state) = guard.as_mut() else {
2350 return;
2351 };
2352 if let Some(existing) = state
2353 .report
2354 .runtime_diagnostics
2355 .iter_mut()
2356 .find(|existing| {
2357 existing.code == diagnostic.code
2358 && existing.component == diagnostic.component
2359 && existing.field == diagnostic.field
2360 })
2361 {
2362 existing.message = diagnostic.message;
2363 existing.session_id = diagnostic.session_id;
2364 existing.count += 1;
2365 } else {
2366 state.report.runtime_diagnostics.push(diagnostic);
2367 }
2368}
2369
2370pub fn rollback_registrations(registrations: &mut Vec<PluginRegistration>) {
2375 let _ = rollback_registrations_checked(registrations);
2376}
2377
2378struct PluginRollbackOutcome {
2379 errors: Vec<String>,
2380 callbacks_cleared: bool,
2381}
2382
2383impl Default for PluginRollbackOutcome {
2384 fn default() -> Self {
2385 Self {
2386 errors: Vec::new(),
2387 callbacks_cleared: true,
2388 }
2389 }
2390}
2391
2392fn rollback_registrations_checked(
2393 registrations: &mut Vec<PluginRegistration>,
2394) -> PluginRollbackOutcome {
2395 let mut outcome = PluginRollbackOutcome::default();
2396 for registration in registrations.iter_mut().rev() {
2397 match catch_unwind(AssertUnwindSafe(|| (registration.deregister)())) {
2398 Ok(PluginRegistrationCleanupOutcome::Removed) => {}
2399 Ok(PluginRegistrationCleanupOutcome::RemovedWithError(error)) => {
2400 outcome.errors.push(format!(
2401 "{} registration '{}' reported a delivery failure: {error}",
2402 registration.kind, registration.name
2403 ));
2404 }
2405 Ok(PluginRegistrationCleanupOutcome::NotRemoved(error)) => {
2406 outcome.callbacks_cleared = false;
2407 outcome.errors.push(format!(
2408 "{} registration '{}' could not be removed: {error}",
2409 registration.kind, registration.name
2410 ));
2411 }
2412 Err(payload) => {
2413 outcome.callbacks_cleared = false;
2414 outcome.errors.push(format!(
2415 "{} registration '{}' could not be removed: deregistration panicked: {}",
2416 registration.kind,
2417 registration.name,
2418 panic_payload_message(payload)
2419 ));
2420 }
2421 }
2422 }
2423 registrations.clear();
2424 outcome
2425}
2426
2427fn panic_payload_message(payload: Box<dyn std::any::Any + Send>) -> String {
2428 payload
2429 .downcast_ref::<&str>()
2430 .map(|message| (*message).to_string())
2431 .or_else(|| payload.downcast_ref::<String>().cloned())
2432 .unwrap_or_else(|| "unknown panic payload".into())
2433}
2434
2435struct ActivePluginConfiguration {
2436 config: PluginConfig,
2437 report: ConfigReport,
2438 registrations: Vec<PluginRegistration>,
2439}
2440
2441async fn initialize_plugin_components(
2442 config: &PluginConfig,
2443 rollback_failures: Option<Arc<Mutex<Vec<String>>>>,
2444) -> Result<Vec<PluginRegistration>> {
2445 ensure_builtin_plugins_registered()?;
2446 let totals = plugin_component_totals(config);
2447 let mut ordinals: HashMap<&str, usize> = HashMap::new();
2448 let mut registrations = PendingPluginRegistrations::new(rollback_failures.clone());
2449
2450 for component in config
2451 .components
2452 .iter()
2453 .filter(|component| component.enabled)
2454 {
2455 let Some(plugin) = lookup_registered_plugin(&component.kind) else {
2456 return Err(PluginError::NotFound(format!(
2457 "plugin component '{}' is not registered",
2458 component.kind
2459 )));
2460 };
2461
2462 let ordinal = ordinals
2463 .entry(component.kind.as_str())
2464 .and_modify(|value| *value += 1)
2465 .or_insert(1);
2466 let namespace = component_namespace(
2467 &component.kind,
2468 *ordinal,
2469 totals.get(component.kind.as_str()).copied().unwrap_or(1),
2470 );
2471
2472 let mut pending =
2473 PendingPluginRegistrationContext::new(namespace, rollback_failures.clone());
2474 plugin
2475 .register(&component.config, &mut pending.context)
2476 .await?;
2477 registrations.extend(pending.take());
2478 }
2479
2480 Ok(registrations.take())
2481}
2482
2483struct PendingPluginRegistrations {
2484 registrations: Vec<PluginRegistration>,
2485 rollback_failures: Option<Arc<Mutex<Vec<String>>>>,
2486}
2487
2488impl PendingPluginRegistrations {
2489 fn new(rollback_failures: Option<Arc<Mutex<Vec<String>>>>) -> Self {
2490 Self {
2491 registrations: Vec::new(),
2492 rollback_failures,
2493 }
2494 }
2495
2496 fn extend(&mut self, registrations: Vec<PluginRegistration>) {
2497 self.registrations.extend(registrations);
2498 }
2499
2500 fn take(&mut self) -> Vec<PluginRegistration> {
2501 std::mem::take(&mut self.registrations)
2502 }
2503}
2504
2505impl Drop for PendingPluginRegistrations {
2506 fn drop(&mut self) {
2507 let outcome = rollback_registrations_checked(&mut self.registrations);
2508 if !outcome.callbacks_cleared {
2509 record_rollback_failures(self.rollback_failures.as_ref(), outcome.errors);
2510 }
2511 }
2512}
2513
2514struct PendingPluginRegistrationContext {
2515 context: PluginRegistrationContext,
2516 rollback_failures: Option<Arc<Mutex<Vec<String>>>>,
2517}
2518
2519impl PendingPluginRegistrationContext {
2520 fn new(namespace: String, rollback_failures: Option<Arc<Mutex<Vec<String>>>>) -> Self {
2521 Self {
2522 context: PluginRegistrationContext::with_namespace(namespace),
2523 rollback_failures,
2524 }
2525 }
2526
2527 fn take(&mut self) -> Vec<PluginRegistration> {
2528 std::mem::take(&mut self.context.registrations)
2529 }
2530}
2531
2532impl Drop for PendingPluginRegistrationContext {
2533 fn drop(&mut self) {
2534 let outcome = rollback_registrations_checked(&mut self.context.registrations);
2535 if !outcome.callbacks_cleared {
2536 record_rollback_failures(self.rollback_failures.as_ref(), outcome.errors);
2537 }
2538 }
2539}
2540
2541fn record_rollback_failures(
2542 rollback_failures: Option<&Arc<Mutex<Vec<String>>>>,
2543 errors: Vec<String>,
2544) {
2545 if errors.is_empty() {
2546 return;
2547 }
2548 if let Some(rollback_failures) = rollback_failures
2549 && let Ok(mut recorded) = rollback_failures.lock()
2550 {
2551 recorded.extend(errors);
2552 }
2553}
2554
2555fn store_active_plugin_configuration(
2556 config: PluginConfig,
2557 report: ConfigReport,
2558 registrations: Vec<PluginRegistration>,
2559) -> Result<()> {
2560 let mut guard = ACTIVE_PLUGIN_CONFIGURATION.lock().map_err(|err| {
2561 PluginError::Internal(format!("active plugin configuration lock poisoned: {err}"))
2562 })?;
2563 *guard = Some(ActivePluginConfiguration {
2564 config,
2565 report,
2566 registrations,
2567 });
2568 if let Ok(mut guard) = LAST_FAILED_RUNTIME_DIAGNOSTICS_REPORT.lock() {
2569 *guard = None;
2570 }
2571 Ok(())
2572}
2573
2574fn plugin_component_totals(config: &PluginConfig) -> HashMap<&str, usize> {
2575 let mut totals = HashMap::new();
2576 for component in &config.components {
2577 *totals.entry(component.kind.as_str()).or_insert(0) += 1;
2578 }
2579 totals
2580}
2581
2582fn component_namespace(kind: &str, ordinal: usize, total: usize) -> String {
2583 if total > 1 {
2584 format!("__nemo_relay_plugin__{kind}__{ordinal}__")
2585 } else {
2586 format!("__nemo_relay_plugin__{kind}__")
2587 }
2588}
2589
2590fn validate_plugin_multiplicity(report: &mut ConfigReport, config: &PluginConfig) {
2591 let totals = plugin_component_totals(config);
2592 let mut emitted = HashSet::new();
2593
2594 for component in &config.components {
2595 let count = totals
2596 .get(component.kind.as_str())
2597 .copied()
2598 .unwrap_or_default();
2599 if count <= 1 || !emitted.insert(component.kind.clone()) {
2600 continue;
2601 }
2602
2603 let allows_multiple = lookup_registered_plugin(&component.kind)
2604 .map(|plugin| plugin.allows_multiple_components())
2605 .unwrap_or(true);
2606 if !allows_multiple {
2607 report.diagnostics.push(ConfigDiagnostic {
2608 level: DiagnosticLevel::Error,
2609 code: "plugin.duplicate_component".to_string(),
2610 component: Some(component.kind.clone()),
2611 field: None,
2612 message: format!(
2613 "plugin component kind '{}' may only appear once",
2614 component.kind
2615 ),
2616 });
2617 }
2618 }
2619}
2620
2621fn push_policy_diag(
2622 diagnostics: &mut Vec<ConfigDiagnostic>,
2623 behavior: UnsupportedBehavior,
2624 code: &str,
2625 component: Option<String>,
2626 field: Option<String>,
2627 message: String,
2628) {
2629 let level = match behavior {
2630 UnsupportedBehavior::Ignore => return,
2631 UnsupportedBehavior::Warn => DiagnosticLevel::Warning,
2632 UnsupportedBehavior::Error => DiagnosticLevel::Error,
2633 };
2634
2635 diagnostics.push(ConfigDiagnostic {
2636 level,
2637 code: code.to_string(),
2638 component,
2639 field,
2640 message,
2641 });
2642}
2643
2644fn join_error_messages(report: &ConfigReport) -> String {
2645 report
2646 .diagnostics
2647 .iter()
2648 .filter(|diag| diag.level == DiagnosticLevel::Error)
2649 .map(|diag| diag.message.as_str())
2650 .collect::<Vec<_>>()
2651 .join("; ")
2652}
2653
2654#[cfg(test)]
2655#[path = "../tests/unit/plugin_tests.rs"]
2656mod tests;