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 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(®istry) 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}