Skip to main content

lash_core/plugin/
runtime_impl.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::sync::Arc;
3use std::sync::{Mutex as StdMutex, Weak};
4
5use super::*;
6
7#[derive(Clone)]
8pub struct PluginHost {
9    factories: Arc<Vec<Arc<dyn PluginFactory>>>,
10    lashlang_abilities: lashlang::LashlangAbilities,
11    lashlang_language_features: lashlang::LashlangLanguageFeatures,
12    lashlang_resources: lashlang::ResourceCatalog,
13    sessions: Arc<StdMutex<BTreeMap<String, Weak<PluginSession>>>>,
14}
15
16struct BuildPluginSessionRequest<'a> {
17    session_id: String,
18    parent_session_id: Option<String>,
19    snapshot: Option<&'a PluginSessionSnapshot>,
20    tool_surface_overlay: ToolSurfaceContribution,
21    tool_snapshot: Option<crate::ToolState>,
22    authority: SessionAuthorityContext,
23}
24
25#[derive(Clone, Debug, Default)]
26pub struct SessionAuthorityContext {
27    pub tool_access: SessionToolAccess,
28    pub subagent: Option<SubagentSessionContext>,
29}
30
31impl PluginHost {
32    pub fn empty() -> Self {
33        Self::new(Vec::new())
34    }
35
36    pub fn new(factories: Vec<Arc<dyn PluginFactory>>) -> Self {
37        let override_ids: BTreeSet<&'static str> =
38            factories.iter().map(|factory| factory.id()).collect();
39        let mut all_factories = super::builtin_plugin_factories();
40        if !override_ids.is_empty() {
41            all_factories.retain(|factory| !override_ids.contains(factory.id()));
42        }
43        all_factories.extend(factories);
44        let lashlang_abilities = all_factories.iter().fold(
45            lashlang::LashlangAbilities::default(),
46            |abilities, factory| abilities.union(factory.lashlang_abilities()),
47        );
48        let lashlang_language_features = all_factories.iter().fold(
49            lashlang::LashlangLanguageFeatures::default(),
50            |features, factory| features.union(factory.lashlang_language_features()),
51        );
52        let lashlang_resources = all_factories.iter().fold(
53            lashlang::ResourceCatalog::new(),
54            |mut resources, factory| {
55                resources.extend(factory.lashlang_resources());
56                resources
57            },
58        );
59        Self {
60            factories: Arc::new(all_factories),
61            lashlang_abilities,
62            lashlang_language_features,
63            lashlang_resources,
64            sessions: Arc::new(StdMutex::new(BTreeMap::new())),
65        }
66    }
67
68    pub fn with_lashlang_abilities(mut self, abilities: lashlang::LashlangAbilities) -> Self {
69        self.lashlang_abilities = abilities;
70        self
71    }
72
73    pub fn with_lashlang_language_features(
74        mut self,
75        language_features: lashlang::LashlangLanguageFeatures,
76    ) -> Self {
77        self.lashlang_language_features = language_features;
78        self
79    }
80
81    pub fn with_lashlang_resources(mut self, resources: lashlang::ResourceCatalog) -> Self {
82        self.lashlang_resources = resources;
83        self
84    }
85
86    pub fn isolated_registry(&self) -> Self {
87        Self {
88            factories: Arc::clone(&self.factories),
89            lashlang_abilities: self.lashlang_abilities,
90            lashlang_language_features: self.lashlang_language_features,
91            lashlang_resources: self.lashlang_resources.clone(),
92            sessions: Arc::new(StdMutex::new(BTreeMap::new())),
93        }
94    }
95
96    pub fn lashlang_abilities(&self) -> lashlang::LashlangAbilities {
97        self.lashlang_abilities
98    }
99
100    pub fn lashlang_language_features(&self) -> lashlang::LashlangLanguageFeatures {
101        self.lashlang_language_features
102    }
103
104    pub fn lashlang_resources(&self) -> lashlang::ResourceCatalog {
105        self.lashlang_resources.clone()
106    }
107
108    pub fn factories(&self) -> &[Arc<dyn PluginFactory>] {
109        self.factories.as_ref().as_slice()
110    }
111
112    pub fn build_session(
113        &self,
114        session_id: impl Into<String>,
115        snapshot: Option<&PluginSessionSnapshot>,
116    ) -> Result<Arc<PluginSession>, PluginError> {
117        self.build_session_with_surface(
118            session_id,
119            snapshot,
120            ToolSurfaceContribution::default(),
121            None,
122        )
123    }
124
125    /// Variant of [`build_session`] that records the caller as the
126    /// parent of the new session. Plugin factories read
127    /// [`PluginSessionContext::is_root_session`] to gate root-only
128    /// behavior; anything that goes through the plain `build_session`
129    /// is treated as a root session by default.
130    pub fn build_session_with_parent(
131        &self,
132        session_id: impl Into<String>,
133        parent_session_id: Option<String>,
134        snapshot: Option<&PluginSessionSnapshot>,
135        authority: SessionAuthorityContext,
136    ) -> Result<Arc<PluginSession>, PluginError> {
137        self.build_session_with_parent_and_surface(
138            session_id,
139            parent_session_id,
140            snapshot,
141            ToolSurfaceContribution::default(),
142            None,
143            authority,
144        )
145    }
146
147    pub fn build_session_with_parent_and_surface(
148        &self,
149        session_id: impl Into<String>,
150        parent_session_id: Option<String>,
151        snapshot: Option<&PluginSessionSnapshot>,
152        tool_surface_overlay: ToolSurfaceContribution,
153        tool_snapshot: Option<crate::ToolState>,
154        authority: SessionAuthorityContext,
155    ) -> Result<Arc<PluginSession>, PluginError> {
156        self.build_session_inner(BuildPluginSessionRequest {
157            session_id: session_id.into(),
158            parent_session_id,
159            snapshot,
160            tool_surface_overlay,
161            tool_snapshot,
162            authority,
163        })
164    }
165
166    pub fn build_session_with_surface(
167        &self,
168        session_id: impl Into<String>,
169        snapshot: Option<&PluginSessionSnapshot>,
170        tool_surface_overlay: ToolSurfaceContribution,
171        tool_snapshot: Option<crate::ToolState>,
172    ) -> Result<Arc<PluginSession>, PluginError> {
173        self.build_session_inner(BuildPluginSessionRequest {
174            session_id: session_id.into(),
175            parent_session_id: None,
176            snapshot,
177            tool_surface_overlay,
178            tool_snapshot,
179            authority: SessionAuthorityContext::default(),
180        })
181    }
182
183    fn build_session_inner(
184        &self,
185        request: BuildPluginSessionRequest<'_>,
186    ) -> Result<Arc<PluginSession>, PluginError> {
187        let BuildPluginSessionRequest {
188            session_id,
189            parent_session_id,
190            snapshot,
191            tool_surface_overlay,
192            tool_snapshot,
193            authority,
194        } = request;
195        let ctx = PluginSessionContext {
196            session_id,
197            tool_access: authority.tool_access.clone(),
198            subagent: authority.subagent.clone(),
199            lashlang_abilities: self.lashlang_abilities,
200            lashlang_language_features: self.lashlang_language_features,
201            parent_session_id,
202        };
203        let session_id = ctx.session_id.clone();
204        let mut tool_snapshot = tool_snapshot;
205        if let Some(snapshot) = &mut tool_snapshot {
206            let hidden_tools = &authority.tool_access.hidden_tools;
207            if !hidden_tools.is_empty() {
208                snapshot.retain(|name, _| !hidden_tools.contains(name));
209            }
210        }
211        let mut plugins = Vec::new();
212        let mut reg = PluginRegistrar::new();
213        for factory in self.factories() {
214            let plugin = factory.build(&ctx)?;
215            reg.registering_plugin_id = Some(plugin.id().to_string());
216            plugin.register(&mut reg)?;
217            reg.registering_plugin_id = None;
218            plugins.push(plugin);
219        }
220        let mut contributions = reg.contributions;
221        let protocol_session = contributions.protocol_session.take().ok_or_else(|| {
222            PluginError::Registration("missing protocol session capability".to_string())
223        })?;
224        let protocol_driver = contributions.protocol_driver.take().ok_or_else(|| {
225            PluginError::Registration("missing protocol driver capability".to_string())
226        })?;
227        contributions.protocol_session = Some(protocol_session);
228        contributions.protocol_driver = Some(protocol_driver);
229        contributions
230            .turn_context_transforms
231            .sort_by_key(|entry| std::cmp::Reverse(entry.0));
232        contributions
233            .history_rewriters
234            .sort_by_key(|entry| std::cmp::Reverse(entry.0));
235        let host_events = crate::HostEventCatalog::from_events(contributions.host_events.clone())
236            .map_err(|message| {
237            PluginError::Registration(format!("invalid host event catalog: {message}"))
238        })?;
239        let mut lashlang_resources = self.lashlang_resources.clone();
240        for event in host_events.events() {
241            lashlang_resources
242                .add_trigger_source_constructor(
243                    event.source_type().split('.'),
244                    lashlang::TypeExpr::Object(Vec::new()),
245                    event.payload_type().clone(),
246                )
247                .map_err(|err| {
248                    PluginError::Registration(format!(
249                        "invalid host event trigger source `{}.{}`: {err}",
250                        event.alias, event.event
251                    ))
252                })?;
253        }
254        let trigger_registry = contributions.trigger_registry.clone().ok_or_else(|| {
255            PluginError::Registration("missing session trigger registry".to_string())
256        })?;
257        let registry = match tool_snapshot {
258            Some(snapshot) => Arc::new(
259                crate::ToolRegistry::from_tool_providers(contributions.tool_providers.clone())
260                    .map_err(|err| {
261                        PluginError::Registration(format!("failed to build tool registry: {err}"))
262                    })?
263                    .fork_with_state(snapshot)
264                    .map_err(|err| {
265                        PluginError::Session(format!(
266                            "tool state cannot be applied to this plugin host session: {err}"
267                        ))
268                    })?,
269            ),
270            None => Arc::new(
271                crate::ToolRegistry::from_tool_providers(contributions.tool_providers.clone())
272                    .map_err(|err| {
273                        PluginError::Registration(format!("failed to build tool registry: {err}"))
274                    })?,
275            ),
276        };
277        let tools = Arc::clone(&registry) as Arc<dyn ToolProvider>;
278
279        let session = Arc::new(PluginSession {
280            host: self.clone(),
281            session_id: ctx.session_id,
282            plugins,
283            tools,
284            tool_registry: registry,
285            tool_surface_overlay,
286            tool_access: authority.tool_access,
287            subagent: authority.subagent,
288            lashlang_abilities: self.lashlang_abilities,
289            lashlang_language_features: self.lashlang_language_features,
290            lashlang_resources,
291            host_events,
292            trigger_registry,
293            contributions,
294        });
295        self.register_session(&session_id, &session)?;
296        let ready = SessionReadyContext {
297            session_id: session.session_id.clone(),
298            host: self.clone(),
299        };
300        for plugin in &session.plugins {
301            plugin.session_ready(ready.clone())?;
302        }
303        if let Some(snapshot) = snapshot {
304            session.restore(snapshot)?;
305        }
306        Ok(session)
307    }
308
309    pub async fn invoke_plugin_action_sessionless(
310        &self,
311        name: &str,
312        args: serde_json::Value,
313    ) -> Result<ToolResult, PluginError> {
314        let session = self.build_session(
315            format!("__external__-{}", uuid::Uuid::new_v4().simple()),
316            None,
317        )?;
318        session
319            .invoke_plugin_action(
320                name,
321                args,
322                None,
323                false,
324                Arc::new(NoopSessionManager),
325                Arc::new(NoopSessionManager),
326                Arc::new(NoopSessionManager),
327                Arc::new(crate::UnavailableProcessService),
328            )
329            .await
330            .map_err(|err| PluginError::Invoke(err.to_string()))
331    }
332
333    fn register_session(
334        &self,
335        session_id: &str,
336        session: &Arc<PluginSession>,
337    ) -> Result<(), PluginError> {
338        let mut sessions = self.sessions.lock().map_err(|_| {
339            PluginError::Session("plugin host session registry poisoned".to_string())
340        })?;
341        if let Some(existing) = sessions.get(session_id).and_then(Weak::upgrade) {
342            if !Arc::ptr_eq(&existing, session) {
343                return Err(PluginError::Session(format!(
344                    "session `{session_id}` is already registered on this plugin host"
345                )));
346            }
347            return Ok(());
348        }
349        sessions.insert(session_id.to_string(), Arc::downgrade(session));
350        Ok(())
351    }
352
353    pub fn unregister_session(&self, session_id: &str) -> Result<(), PluginError> {
354        let mut sessions = self.sessions.lock().map_err(|_| {
355            PluginError::Session("plugin host session registry poisoned".to_string())
356        })?;
357        sessions.remove(session_id);
358        Ok(())
359    }
360
361    pub fn session(&self, session_id: &str) -> Result<Arc<PluginSession>, PluginActionInvokeError> {
362        let mut sessions = self
363            .sessions
364            .lock()
365            .map_err(|_| PluginActionInvokeError::SessionRegistryPoisoned)?;
366        let Some(weak) = sessions.get(session_id).cloned() else {
367            return Err(PluginActionInvokeError::UnknownSession(
368                session_id.to_string(),
369            ));
370        };
371        match weak.upgrade() {
372            Some(session) => Ok(session),
373            None => {
374                sessions.remove(session_id);
375                Err(PluginActionInvokeError::UnknownSession(
376                    session_id.to_string(),
377                ))
378            }
379        }
380    }
381
382    #[expect(
383        clippy::too_many_arguments,
384        reason = "host action invocation wires the runtime service bundle at the plugin boundary"
385    )]
386    pub async fn invoke_plugin_action_for_session(
387        &self,
388        session_id: &str,
389        name: &str,
390        args: serde_json::Value,
391        sessions: Arc<dyn SessionStateService>,
392        session_lifecycle: Arc<dyn SessionLifecycleService>,
393        session_graph: Arc<dyn SessionGraphService>,
394        processes: Arc<dyn crate::ProcessService>,
395    ) -> Result<ToolResult, PluginActionInvokeError> {
396        let session = self.session(session_id)?;
397        session
398            .invoke_plugin_action(
399                name,
400                args,
401                Some(session_id.to_string()),
402                false,
403                sessions,
404                session_lifecycle,
405                session_graph,
406                processes,
407            )
408            .await
409    }
410}