@@ -9,7 +9,12 @@ use axum::extract::{DefaultBodyLimit, State};
99use axum:: http:: HeaderMap ;
1010use axum:: routing:: { get, post} ;
1111use axum:: { Json , Router } ;
12- use nemo_relay:: plugin:: { PluginConfig , clear_plugin_configuration, initialize_plugins_exact} ;
12+ use nemo_relay:: plugin:: dynamic:: {
13+ DynamicPluginKind , NativePluginActivation , NativePluginLoadSpec , load_native_plugins,
14+ } ;
15+ use nemo_relay:: plugin:: {
16+ PluginComponentSpec , PluginConfig , clear_plugin_configuration, initialize_plugins_exact,
17+ } ;
1318use nemo_relay_adaptive:: plugin_component:: register_adaptive_component;
1419use nemo_relay_pii_redaction:: component:: register_pii_redaction_component;
1520use reqwest:: Client ;
@@ -21,6 +26,7 @@ use crate::adapters::{claude_code, codex, cursor, hermes};
2126use crate :: config:: GatewayConfig ;
2227use crate :: error:: CliError ;
2328use crate :: gateway;
29+ use crate :: plugins:: lifecycle:: ActiveDynamicPluginComponent ;
2430use crate :: session:: SessionManager ;
2531
2632const HTTP_CONNECT_TIMEOUT : Duration = Duration :: from_secs ( 30 ) ;
@@ -35,11 +41,11 @@ pub(crate) struct AppState {
3541 pub ( crate ) last_activity : Arc < Mutex < Instant > > ,
3642}
3743
38- /// Binds the configured address and serves until the process is stopped .
39- ///
40- /// Tests and transparent run mode use `serve_listener` directly so they can supply an already
41- /// bound ephemeral listener and optional shutdown channel.
42- pub ( crate ) async fn serve ( config : GatewayConfig ) -> Result < ( ) , CliError > {
44+ /// Binds the configured address and activates enabled dynamic plugins before serving .
45+ pub ( crate ) async fn serve_with_dynamic (
46+ config : GatewayConfig ,
47+ dynamic_plugins : Vec < ActiveDynamicPluginComponent > ,
48+ ) -> Result < ( ) , CliError > {
4349 let listener = TcpListener :: bind ( config. bind ) . await . map_err ( |err| {
4450 // Translate the common bind-failure (port already in use) into an actionable message.
4551 // Plain `io error: Address already in use (os error 48)` is unhelpful; the friendly
@@ -58,19 +64,31 @@ pub(crate) async fn serve(config: GatewayConfig) -> Result<(), CliError> {
5864 CliError :: Io ( err)
5965 }
6066 } ) ?;
61- serve_listener ( listener, config, None ) . await
67+ serve_listener_with_dynamic ( listener, config, dynamic_plugins , None ) . await
6268}
6369
6470/// Serves the gateway router on a caller-owned listener with optional graceful shutdown.
6571///
6672/// A provided shutdown receiver is best-effort: the send side may be dropped after the child agent
6773/// exits, and either receiving or channel closure is enough to let Axum drain the listener.
74+ #[ cfg( test) ]
6875pub ( crate ) async fn serve_listener (
6976 listener : TcpListener ,
7077 config : GatewayConfig ,
7178 shutdown : Option < oneshot:: Receiver < ( ) > > ,
7279) -> Result < ( ) , CliError > {
73- let plugin_activation = PluginActivation :: initialize ( config. plugin_config . clone ( ) ) . await ?;
80+ serve_listener_with_dynamic ( listener, config, Vec :: new ( ) , shutdown) . await
81+ }
82+
83+ /// Serves the gateway router and activates enabled dynamic plugin components.
84+ pub ( crate ) async fn serve_listener_with_dynamic (
85+ listener : TcpListener ,
86+ config : GatewayConfig ,
87+ dynamic_plugins : Vec < ActiveDynamicPluginComponent > ,
88+ shutdown : Option < oneshot:: Receiver < ( ) > > ,
89+ ) -> Result < ( ) , CliError > {
90+ let plugin_activation =
91+ PluginActivation :: initialize ( config. plugin_config . clone ( ) , dynamic_plugins) . await ?;
7492 let state = AppState :: new ( config) ;
7593 let sessions = state. sessions . clone ( ) ;
7694 let last_activity = state. last_activity . clone ( ) ;
@@ -197,35 +215,79 @@ async fn idle_shutdown_future(
197215
198216struct PluginActivation {
199217 active : bool ,
218+ native : Option < NativePluginActivation > ,
200219}
201220
202221impl PluginActivation {
203- async fn initialize ( config : Option < Value > ) -> Result < Self , CliError > {
204- let Some ( config) = config else {
205- return Ok ( Self { active : false } ) ;
222+ async fn initialize (
223+ config : Option < Value > ,
224+ dynamic_plugins : Vec < ActiveDynamicPluginComponent > ,
225+ ) -> Result < Self , CliError > {
226+ if config. is_none ( ) && dynamic_plugins. is_empty ( ) {
227+ return Ok ( Self {
228+ active : false ,
229+ native : None ,
230+ } ) ;
206231 } ;
207232 register_adaptive_component ( ) . map_err ( |error| {
208233 CliError :: Config ( format ! ( "adaptive plugin registration failed: {error}" ) )
209234 } ) ?;
210235 register_pii_redaction_component ( ) . map_err ( |error| {
211236 CliError :: Config ( format ! ( "PII redaction plugin registration failed: {error}" ) )
212237 } ) ?;
238+ let native_specs = dynamic_plugins
239+ . iter ( )
240+ . filter ( |plugin| plugin. kind == DynamicPluginKind :: RustDynamic )
241+ . map ( |plugin| NativePluginLoadSpec {
242+ plugin_id : plugin. plugin_id . clone ( ) ,
243+ manifest_ref : plugin. manifest_ref . clone ( ) ,
244+ } )
245+ . collect :: < Vec < _ > > ( ) ;
246+ let native =
247+ if native_specs. is_empty ( ) {
248+ None
249+ } else {
250+ Some ( load_native_plugins ( native_specs) . map_err ( |error| {
251+ CliError :: Config ( format ! ( "native plugin load failed: {error}" ) )
252+ } ) ?)
253+ } ;
213254 // Gateway already resolved its config; activate exactly (no re-discovery).
214- let plugin_config: PluginConfig = serde_json:: from_value ( config)
215- . map_err ( |error| CliError :: Config ( format ! ( "invalid plugin config: {error}" ) ) ) ?;
255+ let mut plugin_config: PluginConfig = match config {
256+ Some ( config) => serde_json:: from_value ( config)
257+ . map_err ( |error| CliError :: Config ( format ! ( "invalid plugin config: {error}" ) ) ) ?,
258+ None => PluginConfig :: default ( ) ,
259+ } ;
260+ plugin_config
261+ . components
262+ . extend (
263+ dynamic_plugins
264+ . into_iter ( )
265+ . map ( |plugin| PluginComponentSpec {
266+ kind : plugin. plugin_id ,
267+ enabled : true ,
268+ config : plugin. config ,
269+ } ) ,
270+ ) ;
216271 initialize_plugins_exact ( plugin_config)
217272 . await
218273 . map_err ( |error| CliError :: Config ( format ! ( "plugin activation failed: {error}" ) ) ) ?;
219- Ok ( Self { active : true } )
274+ Ok ( Self {
275+ active : true ,
276+ native,
277+ } )
220278 }
221279
222280 fn clear ( mut self ) -> Result < ( ) , CliError > {
223- if self . active {
281+ let result = if self . active {
224282 self . active = false ;
225283 clear_plugin_configuration ( )
226284 . map_err ( |error| CliError :: Config ( format ! ( "plugin teardown failed: {error}" ) ) ) ?;
227- }
228- Ok ( ( ) )
285+ Ok ( ( ) )
286+ } else {
287+ Ok ( ( ) )
288+ } ;
289+ self . native . take ( ) ;
290+ result
229291 }
230292}
231293
0 commit comments