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