1use std::collections::HashMap;
40use std::sync::Arc;
41use std::time::{Duration, Instant};
42
43use serde_json::Value;
44use thiserror::Error;
45
46use super::context_provider::{
47 resolve_context_provider, ContextProviderId, ContextProviderLookupError,
48 RegisteredContextProviderDescriptor, SharedContextProviderCatalog,
49};
50use super::lifecycle::{
51 validate_runtime_provider_declarations, validate_runtime_tool_declarations,
52};
53use super::manager::{DeferredExtensionRecord, SharedDeferredRecords};
54use super::runtime::process::ProcessExtension;
55use super::runtime::ExtensionHandler;
56use crate::tools::activation::SessionId;
57use crate::tools::catalog::SchemaDigest;
58
59pub const DEFAULT_IDLE_MAX: Duration = Duration::from_secs(300);
61pub const MAX_LIVE_LEASES: usize = 64;
63pub const REAP_SCAN_MAX: usize = MAX_LIVE_LEASES;
66const CLEANUP_TOTAL_TIMEOUT: Duration = Duration::from_secs(5);
68const TRANSPORT_DETAIL_MAX: usize = 200;
70const EXTENSION_REPORTED_PREFIX: &str = "Extension error:";
73
74#[derive(Debug, Error)]
77pub enum ExtensionLeaseError {
78 #[error("extension '{0}' has no retained deferred launch record; lease revoked")]
79 NotDeferred(String),
80 #[error("extension '{0}' manifest failed re-validation at lease acquisition; lease revoked")]
81 ManifestInvalid(String),
82 #[error(
83 "extension '{0}' launch record changed since the lease was acquired; lease invalidated"
84 )]
85 LaunchDrift(String),
86 #[error("extension '{0}' no longer declares tool '{1}' with the pinned schema; lease revoked")]
87 DeclarationDrift(String, String),
88 #[error("extension '{0}' runtime declarations do not match the manifest ({1}); child terminated without any call")]
89 DeclarationMismatch(String, &'static str),
90 #[error("extension '{0}' did not register expected tool '{1}'; poisoned lease terminated without calling")]
91 NameNotListed(String, String),
92 #[error("extension '{0}' registered a schema for tool '{1}' that does not match the pinned digest; poisoned lease terminated without calling")]
93 SchemaMismatch(String, String),
94 #[error("extension lease for '{0}' was revoked before the call could start")]
95 Revoked(String),
96 #[error("extension lease capacity of {0} live leases reached; try again after idle leases are reaped")]
97 Capacity(usize),
98 #[error("extension '{0}' transport failure: {1}")]
99 Transport(String, String),
100 #[error("extension '{plugin}' reported a tool error ({length} bytes, content withheld)")]
101 ExtensionReported { plugin: String, length: usize },
102}
103
104impl ExtensionLeaseError {
105 pub fn revokes_exact_grant(&self) -> bool {
111 matches!(
112 self,
113 Self::NotDeferred(_)
114 | Self::ManifestInvalid(_)
115 | Self::LaunchDrift(_)
116 | Self::DeclarationDrift(_, _)
117 | Self::DeclarationMismatch(_, _)
118 | Self::NameNotListed(_, _)
119 | Self::SchemaMismatch(_, _)
120 )
121 }
122}
123
124fn bound_detail(detail: &str) -> String {
128 if detail.len() <= TRANSPORT_DETAIL_MAX {
129 return detail.to_string();
130 }
131 let mut cut = TRANSPORT_DETAIL_MAX;
132 while cut > 0 && !detail.is_char_boundary(cut) {
133 cut -= 1;
134 }
135 format!("{}… ({} bytes total)", &detail[..cut], detail.len())
136}
137
138fn record_fingerprint(record: &DeferredExtensionRecord) -> SchemaDigest {
141 SchemaDigest::of_schema(&serde_json::json!({
142 "manifest": serde_json::to_value(&record.manifest).unwrap_or(Value::Null),
143 "cwd": record.cwd.as_ref().map(|p| p.display().to_string()),
144 "config": record.config,
145 }))
146}
147
148struct LeaseInner {
149 fingerprint: SchemaDigest,
150 handler: Arc<ProcessExtension>,
151 call_permit: tokio::sync::Mutex<()>,
154 listing: HashMap<String, SchemaDigest>,
157 gate: std::sync::Mutex<bool>,
159 last_used: std::sync::Mutex<Instant>,
160 liveness: Arc<()>,
162}
163
164impl LeaseInner {
165 fn cancelled(&self) -> bool {
166 *self
167 .gate
168 .lock()
169 .unwrap_or_else(std::sync::PoisonError::into_inner)
170 }
171
172 fn set_cancelled(&self) {
173 *self
174 .gate
175 .lock()
176 .unwrap_or_else(std::sync::PoisonError::into_inner) = true;
177 }
178
179 fn interrupt(&self) {
180 self.set_cancelled();
181
182 if self.call_permit.try_lock().is_err() {
189 let handler = Arc::clone(&self.handler);
190 if let Ok(handle) = tokio::runtime::Handle::try_current() {
191 handle.spawn(async move {
192 handler.force_shutdown().await;
193 });
194 }
195 }
196 }
197}
198
199enum LeaseState {
200 Starting {
203 token: u64,
204 done: tokio::sync::watch::Receiver<bool>,
205 },
206 Ready(Arc<LeaseInner>),
207}
208
209pub struct ExtensionRuntimeManager {
211 leases: std::sync::Mutex<HashMap<(String, String), LeaseState>>,
212 records: SharedDeferredRecords,
215 context_catalog: SharedContextProviderCatalog,
220 idle_max: Duration,
221 next_token: std::sync::atomic::AtomicU64,
222 host_scope: std::sync::OnceLock<SessionId>,
229}
230
231impl ExtensionRuntimeManager {
232 pub(crate) fn new(
233 records: SharedDeferredRecords,
234 context_catalog: SharedContextProviderCatalog,
235 idle_max: Duration,
236 ) -> Self {
237 Self {
238 leases: std::sync::Mutex::new(HashMap::new()),
239 records,
240 context_catalog,
241 idle_max,
242 next_token: std::sync::atomic::AtomicU64::new(1),
243 host_scope: std::sync::OnceLock::new(),
244 }
245 }
246
247 pub fn resolve_context_provider(
257 &self,
258 requested: Option<&ContextProviderId>,
259 ) -> Result<RegisteredContextProviderDescriptor, ContextProviderLookupError> {
260 let catalog = self
261 .context_catalog
262 .lock()
263 .unwrap_or_else(std::sync::PoisonError::into_inner);
264 resolve_context_provider(&catalog, requested).cloned()
265 }
266
267 pub fn bind_host_scope(&self, session: SessionId) {
271 let _ = self.host_scope.set(session);
272 }
273
274 pub fn host_scope(&self) -> SessionId {
276 self.host_scope
277 .get_or_init(|| {
278 SessionId::parse("extension-host-scope")
279 .expect("static host scope identity is valid")
280 })
281 .clone()
282 }
283
284 fn current_record(&self, plugin: &str) -> Option<DeferredExtensionRecord> {
285 self.records
286 .lock()
287 .unwrap_or_else(std::sync::PoisonError::into_inner)
288 .get(plugin)
289 .cloned()
290 }
291
292 pub(crate) fn declared_tool_digest(
298 &self,
299 plugin: &str,
300 tool_name: &str,
301 ) -> Option<SchemaDigest> {
302 let record = self.current_record(plugin)?;
303 record
304 .manifest
305 .deferred
306 .as_ref()?
307 .tools
308 .iter()
309 .find(|tool| tool.name == tool_name)
310 .map(|tool| SchemaDigest::of_schema(&tool.input_schema))
311 }
312
313 fn cancel_state(state: LeaseState) {
318 match state {
319 LeaseState::Ready(inner) => {
320 inner.interrupt();
321 Self::spawn_bounded_cleanup(inner);
322 }
323 LeaseState::Starting { .. } => {}
326 }
327 }
328
329 fn spawn_bounded_cleanup(inner: Arc<LeaseInner>) {
330 if let Ok(handle) = tokio::runtime::Handle::try_current() {
331 handle.spawn(async move {
332 let _ = tokio::time::timeout(CLEANUP_TOTAL_TIMEOUT, async {
333 let _permit = inner.call_permit.lock().await;
336 inner.handler.shutdown().await;
337 })
338 .await;
339 });
340 }
341 }
344
345 pub fn lease_count(&self) -> usize {
347 self.leases
348 .lock()
349 .unwrap_or_else(std::sync::PoisonError::into_inner)
350 .len()
351 }
352
353 pub fn revoke_plugin_lease(&self, session: &SessionId, plugin: &str) {
356 let removed = {
357 let mut map = self
358 .leases
359 .lock()
360 .unwrap_or_else(std::sync::PoisonError::into_inner);
361 map.remove(&(session.as_str().to_string(), plugin.to_string()))
362 };
363 if let Some(state) = removed {
364 Self::cancel_state(state);
365 }
366 }
367
368 pub fn revoke_exact_tool(&self, session: &SessionId, plugin: &str, tool_name: &str) {
372 let _ = tool_name; self.revoke_plugin_lease(session, plugin);
374 }
375
376 pub fn revoke_plugin_all_sessions(&self, plugin: &str) -> usize {
379 let removed: Vec<LeaseState> = {
380 let mut map = self
381 .leases
382 .lock()
383 .unwrap_or_else(std::sync::PoisonError::into_inner);
384 let keys: Vec<(String, String)> =
385 map.keys().filter(|(_, p)| p == plugin).cloned().collect();
386 keys.into_iter().filter_map(|k| map.remove(&k)).collect()
387 };
388 let count = removed.len();
389 for state in removed {
390 Self::cancel_state(state);
391 }
392 count
393 }
394
395 pub fn terminate_session(&self, session: &SessionId) -> usize {
397 let removed: Vec<LeaseState> = {
398 let mut map = self
399 .leases
400 .lock()
401 .unwrap_or_else(std::sync::PoisonError::into_inner);
402 let keys: Vec<(String, String)> = map
403 .keys()
404 .filter(|(s, _)| s == session.as_str())
405 .cloned()
406 .collect();
407 keys.into_iter().filter_map(|k| map.remove(&k)).collect()
408 };
409 let count = removed.len();
410 for state in removed {
411 Self::cancel_state(state);
412 }
413 count
414 }
415
416 pub fn terminate_all(&self) {
418 let removed: Vec<LeaseState> = {
419 let mut map = self
420 .leases
421 .lock()
422 .unwrap_or_else(std::sync::PoisonError::into_inner);
423 map.drain().map(|(_, v)| v).collect()
424 };
425 for state in removed {
426 Self::cancel_state(state);
427 }
428 }
429
430 pub fn reap_idle(&self) {
433 let removed: Vec<LeaseState> = {
434 let mut map = self
435 .leases
436 .lock()
437 .unwrap_or_else(std::sync::PoisonError::into_inner);
438 let idle: Vec<(String, String)> = map
439 .iter()
440 .take(REAP_SCAN_MAX)
441 .filter(|(_, state)| match state {
442 LeaseState::Ready(inner) => {
443 inner
444 .last_used
445 .lock()
446 .unwrap_or_else(std::sync::PoisonError::into_inner)
447 .elapsed()
448 >= self.idle_max
449 }
450 LeaseState::Starting { .. } => false,
451 })
452 .map(|(k, _)| k.clone())
453 .collect();
454 idle.into_iter().filter_map(|k| map.remove(&k)).collect()
455 };
456 for state in removed {
457 Self::cancel_state(state);
458 }
459 }
460
461 async fn acquire_ready(
468 &self,
469 session: &SessionId,
470 plugin: &str,
471 record: &DeferredExtensionRecord,
472 validated: &super::manifest::ValidatedExtensionManifest,
473 ) -> Result<Arc<LeaseInner>, ExtensionLeaseError> {
474 let expected_fingerprint = record_fingerprint(record);
475 let key = (session.as_str().to_string(), plugin.to_string());
476 let inner: Arc<LeaseInner> = loop {
477 enum Acquire {
481 Use(Arc<LeaseInner>),
482 CancelStale(LeaseState),
483 Wait(tokio::sync::watch::Receiver<bool>),
484 Start {
485 token: u64,
486 done_tx: tokio::sync::watch::Sender<bool>,
487 },
488 AtCapacity,
489 }
490 let action = {
491 let mut map = self
492 .leases
493 .lock()
494 .unwrap_or_else(std::sync::PoisonError::into_inner);
495 match map.get(&key) {
496 Some(LeaseState::Ready(inner)) => {
497 if inner.cancelled() || inner.fingerprint != expected_fingerprint {
498 match map.remove(&key) {
499 Some(state) => Acquire::CancelStale(state),
500 None => continue,
501 }
502 } else {
503 Acquire::Use(Arc::clone(inner))
504 }
505 }
506 Some(LeaseState::Starting { done, .. }) => Acquire::Wait(done.clone()),
507 None => {
508 if map.len() >= MAX_LIVE_LEASES {
509 Acquire::AtCapacity
510 } else {
511 let token = self
512 .next_token
513 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
514 let (done_tx, done_rx) = tokio::sync::watch::channel(false);
515 map.insert(
516 key.clone(),
517 LeaseState::Starting {
518 token,
519 done: done_rx,
520 },
521 );
522 Acquire::Start { token, done_tx }
523 }
524 }
525 }
526 };
527 match action {
528 Acquire::Use(inner) => break inner,
529 Acquire::CancelStale(state) => {
530 let was_drift = matches!(
534 &state,
535 LeaseState::Ready(inner) if inner.fingerprint != expected_fingerprint
536 && !inner.cancelled()
537 );
538 Self::cancel_state(state);
539 if was_drift {
540 return Err(ExtensionLeaseError::LaunchDrift(plugin.to_string()));
541 }
542 continue;
543 }
544 Acquire::AtCapacity => {
545 self.reap_idle();
546 let still_full = self
547 .leases
548 .lock()
549 .unwrap_or_else(std::sync::PoisonError::into_inner)
550 .len()
551 >= MAX_LIVE_LEASES;
552 if still_full {
553 return Err(ExtensionLeaseError::Capacity(MAX_LIVE_LEASES));
554 }
555 continue;
556 }
557 Acquire::Wait(mut follower) => {
558 if !*follower.borrow() {
560 let _ = follower.changed().await;
561 }
562 continue;
563 }
564 Acquire::Start { token, done_tx } => {
565 let started = self
566 .start_lease(plugin, record, validated, expected_fingerprint.clone())
567 .await;
568 let ours = {
569 let mut map = self
570 .leases
571 .lock()
572 .unwrap_or_else(std::sync::PoisonError::into_inner);
573 let ours = matches!(
574 map.get(&key),
575 Some(LeaseState::Starting { token: t, .. }) if *t == token
576 );
577 match &started {
578 Ok(inner) if ours => {
579 map.insert(key.clone(), LeaseState::Ready(Arc::clone(inner)));
580 }
581 Err(_) if ours => {
582 map.remove(&key);
583 }
584 _ => {}
585 }
586 ours
587 };
588 match started {
589 Ok(inner) => {
590 if !ours {
591 inner.set_cancelled();
594 inner.handler.shutdown().await;
595 let _ = done_tx.send(true);
596 return Err(ExtensionLeaseError::Revoked(plugin.to_string()));
597 }
598 let _ = done_tx.send(true);
599 break inner;
600 }
601 Err(err) => {
602 let _ = done_tx.send(true);
603 return Err(err);
604 }
605 }
606 }
607 }
608 };
609
610 Ok(inner)
611 }
612
613 async fn acquire_handler_scope(
621 &self,
622 plugin: &str,
623 ) -> Result<Arc<LeaseInner>, ExtensionLeaseError> {
624 let session = self.host_scope();
625 let (record, validated) = self.record_and_validate(&session, plugin)?;
626 self.reap_idle();
627 self.acquire_ready(&session, plugin, &record, &validated)
628 .await
629 }
630
631 fn has_ready_lease(&self, session: &SessionId, plugin: &str) -> bool {
633 let map = self
634 .leases
635 .lock()
636 .unwrap_or_else(std::sync::PoisonError::into_inner);
637 matches!(
638 map.get(&(session.as_str().to_string(), plugin.to_string())),
639 Some(LeaseState::Ready(inner)) if !inner.cancelled()
640 )
641 }
642
643 fn record_and_validate(
648 &self,
649 session: &SessionId,
650 plugin: &str,
651 ) -> Result<
652 (
653 DeferredExtensionRecord,
654 super::manifest::ValidatedExtensionManifest,
655 ),
656 ExtensionLeaseError,
657 > {
658 let Some(record) = self.current_record(plugin) else {
659 self.revoke_plugin_lease(session, plugin);
660 return Err(ExtensionLeaseError::NotDeferred(plugin.to_string()));
661 };
662 let Ok(validated) = record.manifest.validate(plugin) else {
663 self.revoke_plugin_lease(session, plugin);
664 return Err(ExtensionLeaseError::ManifestInvalid(plugin.to_string()));
665 };
666 Ok((record, validated))
667 }
668
669 pub async fn call_exact(
673 &self,
674 session: &SessionId,
675 plugin: &str,
676 tool_name: &str,
677 expected_digest: &SchemaDigest,
678 params: Value,
679 ) -> Result<Value, ExtensionLeaseError> {
680 self.call_exact_for(session, plugin, tool_name, expected_digest, params, None)
681 .await
682 }
683
684 pub async fn call_exact_for(
688 &self,
689 session: &SessionId,
690 plugin: &str,
691 tool_name: &str,
692 expected_digest: &SchemaDigest,
693 params: Value,
694 conversation_id: Option<&str>,
695 ) -> Result<Value, ExtensionLeaseError> {
696 let (record, validated) = self.record_and_validate(session, plugin)?;
698 let declared_tools = record
701 .manifest
702 .deferred
703 .as_ref()
704 .map(|d| d.tools.as_slice())
705 .unwrap_or(&[]);
706 let still_declared = declared_tools.iter().any(|t| {
707 t.name == tool_name && &SchemaDigest::of_schema(&t.input_schema) == expected_digest
708 });
709 if !still_declared {
710 self.revoke_plugin_lease(session, plugin);
711 return Err(ExtensionLeaseError::DeclarationDrift(
712 plugin.to_string(),
713 tool_name.to_string(),
714 ));
715 }
716 self.reap_idle();
717
718 let inner = self
720 .acquire_ready(session, plugin, &record, &validated)
721 .await?;
722 match inner.listing.get(tool_name) {
725 None => {
726 self.revoke_plugin_lease(session, plugin);
727 return Err(ExtensionLeaseError::NameNotListed(
728 plugin.to_string(),
729 tool_name.to_string(),
730 ));
731 }
732 Some(listed) if listed != expected_digest => {
733 self.revoke_plugin_lease(session, plugin);
734 return Err(ExtensionLeaseError::SchemaMismatch(
735 plugin.to_string(),
736 tool_name.to_string(),
737 ));
738 }
739 Some(_) => {}
740 }
741
742 let permit = inner.call_permit.lock().await;
745 if inner.cancelled() {
746 return Err(ExtensionLeaseError::Revoked(plugin.to_string()));
747 }
748 let result = inner
749 .handler
750 .call_tool_once_no_spawn(tool_name, params, conversation_id)
751 .await;
752 if inner.cancelled() {
753 inner.handler.shutdown().await;
756 }
757 drop(permit);
758 *inner
759 .last_used
760 .lock()
761 .unwrap_or_else(std::sync::PoisonError::into_inner) = Instant::now();
762 result.map_err(|e| {
763 if let Some(rest) = e.strip_prefix(EXTENSION_REPORTED_PREFIX) {
764 ExtensionLeaseError::ExtensionReported {
766 plugin: plugin.to_string(),
767 length: rest.trim().len(),
768 }
769 } else {
770 ExtensionLeaseError::Transport(plugin.to_string(), bound_detail(&e))
771 }
772 })
773 }
774
775 async fn start_lease(
780 &self,
781 plugin: &str,
782 record: &DeferredExtensionRecord,
783 validated: &super::manifest::ValidatedExtensionManifest,
784 fingerprint: SchemaDigest,
785 ) -> Result<Arc<LeaseInner>, ExtensionLeaseError> {
786 let manifest = &record.manifest;
787 let process = ProcessExtension::spawn_with_memory_policy(
788 plugin,
789 &manifest.command,
790 &manifest.args,
791 record.cwd.clone(),
792 record.exclusive_memory,
793 )
794 .await
795 .map_err(|e| ExtensionLeaseError::Transport(plugin.to_string(), bound_detail(&e)))?;
796 process.set_permissions(validated.permissions.clone()).await;
797 let caps = match process
798 .initialize(record.cwd.clone(), record.config.clone())
799 .await
800 {
801 Ok(caps) => caps,
802 Err(e) => {
803 process.shutdown().await;
804 let err = if let Some(rest) = e.strip_prefix(EXTENSION_REPORTED_PREFIX) {
805 ExtensionLeaseError::ExtensionReported {
806 plugin: plugin.to_string(),
807 length: rest.trim().len(),
808 }
809 } else {
810 ExtensionLeaseError::Transport(plugin.to_string(), bound_detail(&e))
811 };
812 return Err(err);
813 }
814 };
815 let deferred = manifest.deferred.as_ref();
816 let declared_tools = deferred.map(|d| d.tools.as_slice()).unwrap_or(&[]);
817 let declared_providers = deferred.map(|d| d.providers.as_slice()).unwrap_or(&[]);
818 if let Err(reason) = validate_runtime_tool_declarations(declared_tools, &caps.tools) {
819 process.shutdown().await;
820 return Err(ExtensionLeaseError::DeclarationMismatch(
821 plugin.to_string(),
822 reason,
823 ));
824 }
825 if let Err(reason) =
826 validate_runtime_provider_declarations(declared_providers, &caps.providers)
827 {
828 process.shutdown().await;
829 return Err(ExtensionLeaseError::DeclarationMismatch(
830 plugin.to_string(),
831 reason,
832 ));
833 }
834 for decl in &caps.capabilities {
835 if super::runtime::process::validate_capability(decl, &validated.permissions).is_err() {
836 process.shutdown().await;
837 return Err(ExtensionLeaseError::DeclarationMismatch(
838 plugin.to_string(),
839 "capability_declaration_rejected",
840 ));
841 }
842 }
843 let mut listing = HashMap::new();
844 for spec in &caps.tools {
845 listing.insert(
846 spec.name.clone(),
847 SchemaDigest::of_schema(&spec.input_schema),
848 );
849 }
850 Ok(Arc::new(LeaseInner {
851 fingerprint,
852 handler: Arc::new(process),
853 call_permit: tokio::sync::Mutex::new(()),
854 listing,
855 gate: std::sync::Mutex::new(false),
856 last_used: std::sync::Mutex::new(Instant::now()),
857 liveness: Arc::new(()),
858 }))
859 }
860
861 #[doc(hidden)]
864 pub fn lease_liveness_for_tests(
865 &self,
866 session: &SessionId,
867 plugin: &str,
868 ) -> Option<std::sync::Weak<()>> {
869 let map = self
870 .leases
871 .lock()
872 .unwrap_or_else(std::sync::PoisonError::into_inner);
873 match map.get(&(session.as_str().to_string(), plugin.to_string())) {
874 Some(LeaseState::Ready(inner)) => Some(Arc::downgrade(&inner.liveness)),
875 _ => None,
876 }
877 }
878}
879
880impl Drop for ExtensionRuntimeManager {
881 fn drop(&mut self) {
882 self.terminate_all();
883 }
884}
885
886impl std::fmt::Debug for ExtensionRuntimeManager {
887 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
888 f.debug_struct("ExtensionRuntimeManager")
889 .field("leases", &self.lease_count())
890 .finish_non_exhaustive()
891 }
892}
893
894#[derive(Clone)]
898pub struct ExtensionLeaseCapability {
899 session: SessionId,
900 manager: Arc<ExtensionRuntimeManager>,
901}
902
903impl ExtensionLeaseCapability {
904 pub fn new(session: SessionId, manager: Arc<ExtensionRuntimeManager>) -> Self {
905 Self { session, manager }
906 }
907
908 pub fn session(&self) -> &SessionId {
910 &self.session
911 }
912
913 pub async fn call_exact(
914 &self,
915 plugin: &str,
916 tool_name: &str,
917 expected_digest: &SchemaDigest,
918 params: Value,
919 ) -> Result<Value, ExtensionLeaseError> {
920 self.manager
921 .call_exact(&self.session, plugin, tool_name, expected_digest, params)
922 .await
923 }
924
925 pub async fn call_exact_for(
928 &self,
929 plugin: &str,
930 tool_name: &str,
931 expected_digest: &SchemaDigest,
932 params: Value,
933 conversation_id: Option<&str>,
934 ) -> Result<Value, ExtensionLeaseError> {
935 self.manager
936 .call_exact_for(
937 &self.session,
938 plugin,
939 tool_name,
940 expected_digest,
941 params,
942 conversation_id,
943 )
944 .await
945 }
946}
947
948impl std::fmt::Debug for ExtensionLeaseCapability {
949 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
950 f.debug_struct("ExtensionLeaseCapability")
951 .finish_non_exhaustive()
952 }
953}
954
955pub struct ExtensionSessionEndGuard {
961 session: SessionId,
962 manager: Arc<ExtensionRuntimeManager>,
963}
964
965impl ExtensionSessionEndGuard {
966 pub fn new(session: SessionId, manager: Arc<ExtensionRuntimeManager>) -> Self {
967 Self { session, manager }
968 }
969}
970
971impl Drop for ExtensionSessionEndGuard {
972 fn drop(&mut self) {
973 self.manager.terminate_session(&self.session);
974 }
975}
976
977impl std::fmt::Debug for ExtensionSessionEndGuard {
978 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
979 f.debug_struct("ExtensionSessionEndGuard")
980 .finish_non_exhaustive()
981 }
982}
983
984pub struct LazyExtensionHandler {
993 plugin: String,
994 manager: Arc<ExtensionRuntimeManager>,
995}
996
997impl LazyExtensionHandler {
998 pub fn new(plugin: &str, manager: Arc<ExtensionRuntimeManager>) -> Self {
999 Self {
1000 plugin: plugin.to_string(),
1001 manager,
1002 }
1003 }
1004
1005 async fn live(&self) -> Result<Arc<LeaseInner>, String> {
1008 self.manager
1009 .acquire_handler_scope(&self.plugin)
1010 .await
1011 .map_err(|e| e.to_string())
1012 }
1013
1014 fn touch(inner: &LeaseInner) {
1015 *inner
1016 .last_used
1017 .lock()
1018 .unwrap_or_else(std::sync::PoisonError::into_inner) = Instant::now();
1019 }
1020}
1021
1022#[async_trait::async_trait]
1023impl ExtensionHandler for LazyExtensionHandler {
1024 fn id(&self) -> &str {
1025 &self.plugin
1026 }
1027
1028 async fn handle(
1029 &self,
1030 event: &crate::extensions::hooks::events::HookEvent,
1031 ) -> crate::extensions::hooks::events::HookResult {
1032 match self.live().await {
1037 Ok(inner) => {
1038 let result = inner.handler.handle(event).await;
1039 Self::touch(&inner);
1040 result
1041 }
1042 Err(error) => {
1043 tracing::warn!(
1044 extension = %self.plugin,
1045 error = %error,
1046 "Deferred extension could not start for hook event — continuing",
1047 );
1048 crate::extensions::hooks::events::HookResult::Continue
1049 }
1050 }
1051 }
1052
1053 async fn call_tool(&self, _name: &str, _input: Value) -> Result<Value, String> {
1054 Err(format!(
1060 "extension '{}' is deferred: tool calls are only served through the \
1061 gate-authorized exact activation path",
1062 self.plugin
1063 ))
1064 }
1065
1066 async fn provider_complete(
1067 &self,
1068 params: super::runtime::process::ProviderCompleteParams,
1069 ) -> Result<super::runtime::process::ProviderCompleteResult, String> {
1070 let inner = self.live().await?;
1071 let result = inner.handler.provider_complete(params).await;
1072 Self::touch(&inner);
1073 result
1074 }
1075
1076 async fn provider_stream(
1077 &self,
1078 params: super::runtime::process::ProviderCompleteParams,
1079 sink: tokio::sync::mpsc::Sender<super::runtime::process::ProviderStreamEvent>,
1080 ) -> Result<super::runtime::process::ProviderCompleteResult, String> {
1081 let inner = self.live().await?;
1082 let result = inner.handler.provider_stream(params, sink).await;
1083 Self::touch(&inner);
1084 result
1085 }
1086
1087 async fn invoke_command(
1088 &self,
1089 command: &str,
1090 args: Vec<String>,
1091 request_id: &str,
1092 sink: crate::extensions::invoke_output::InvokeEventSink,
1093 ) -> Result<Value, String> {
1094 let inner = self.live().await?;
1095 let result = inner
1096 .handler
1097 .invoke_command(command, args, request_id, sink)
1098 .await;
1099 Self::touch(&inner);
1100 result
1101 }
1102
1103 async fn get_info(&self) -> Result<crate::extensions::info::PluginInfo, String> {
1104 Err("deferred extension info is unavailable without activation".to_string())
1106 }
1107
1108 async fn sidecar_spawn_args(&self) -> Result<crate::sidecar::spawn::SidecarSpawnArgs, String> {
1109 let inner = self.live().await?;
1111 let result = inner.handler.sidecar_spawn_args().await;
1112 Self::touch(&inner);
1113 result
1114 }
1115
1116 async fn settings_editor_open(&self, category: &str, field: &str) -> Result<Value, String> {
1117 let inner = self.live().await?;
1118 let result = inner.handler.settings_editor_open(category, field).await;
1119 Self::touch(&inner);
1120 result
1121 }
1122
1123 async fn settings_editor_key(
1124 &self,
1125 category: &str,
1126 field: &str,
1127 key: &str,
1128 ) -> Result<Value, String> {
1129 let inner = self.live().await?;
1130 let result = inner
1131 .handler
1132 .settings_editor_key(category, field, key)
1133 .await;
1134 Self::touch(&inner);
1135 result
1136 }
1137
1138 async fn settings_editor_commit(
1139 &self,
1140 category: &str,
1141 field: &str,
1142 value: Value,
1143 ) -> Result<Value, String> {
1144 let inner = self.live().await?;
1145 let result = inner
1146 .handler
1147 .settings_editor_commit(category, field, value)
1148 .await;
1149 Self::touch(&inner);
1150 result
1151 }
1152
1153 async fn shutdown(&self) {
1154 self.manager.revoke_plugin_all_sessions(&self.plugin);
1156 }
1157
1158 async fn health(&self) -> super::runtime::ExtensionHealth {
1159 if self
1161 .manager
1162 .has_ready_lease(&self.manager.host_scope(), &self.plugin)
1163 {
1164 super::runtime::ExtensionHealth::Running
1165 } else {
1166 super::runtime::ExtensionHealth::Loaded
1167 }
1168 }
1169}
1170
1171impl std::fmt::Debug for LazyExtensionHandler {
1172 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1173 f.debug_struct("LazyExtensionHandler")
1174 .field("plugin", &self.plugin)
1175 .finish_non_exhaustive()
1176 }
1177}