1use crate::audit::{self, TrustLevel};
24use crate::permission::{
25 PermissionConfig, PermissionContext, PermissionDecision, StoredPermission, StoredTrustLevel,
26};
27use crate::{LoadedPlugin, LoaderError, PluginLoader};
28use std::collections::HashMap;
29use std::path::{Path, PathBuf};
30use std::sync::Arc;
31use tokio::sync::RwLock;
32
33#[derive(Clone)]
35pub struct PluginRegistry {
36 inner: Arc<RwLock<RegistryInner>>,
37 loader: Arc<PluginLoader>,
38 permission: Option<Arc<PermissionConfig>>,
39}
40
41struct RegistryInner {
42 plugins: HashMap<String, PluginEntry>,
44 path_to_command: HashMap<PathBuf, String>,
46}
47
48struct PluginEntry {
49 plugin: LoadedPlugin,
50 source_path: Option<PathBuf>,
51}
52
53impl PluginRegistry {
54 pub fn new() -> Result<Self, LoaderError> {
56 Ok(Self {
57 inner: Arc::new(RwLock::new(RegistryInner {
58 plugins: HashMap::new(),
59 path_to_command: HashMap::new(),
60 })),
61 loader: Arc::new(PluginLoader::new()?),
62 permission: None,
63 })
64 }
65
66 pub fn with_loader(loader: PluginLoader) -> Self {
68 Self {
69 inner: Arc::new(RwLock::new(RegistryInner {
70 plugins: HashMap::new(),
71 path_to_command: HashMap::new(),
72 })),
73 loader: Arc::new(loader),
74 permission: None,
75 }
76 }
77
78 pub fn with_permissions(config: PermissionConfig) -> Result<Self, LoaderError> {
83 Ok(Self {
84 inner: Arc::new(RwLock::new(RegistryInner {
85 plugins: HashMap::new(),
86 path_to_command: HashMap::new(),
87 })),
88 loader: Arc::new(PluginLoader::new()?),
89 permission: Some(Arc::new(config)),
90 })
91 }
92
93 pub fn set_permissions(&mut self, config: PermissionConfig) {
95 self.permission = Some(Arc::new(config));
96 }
97
98 pub async fn load_plugin(&self, path: impl AsRef<Path>) -> Result<String, LoaderError> {
100 let path = path.as_ref();
101 let wasm_bytes = tokio::fs::read(path).await.map_err(|e| {
102 LoaderError::MemoryAccess(format!("Failed to read file {}: {}", path.display(), e))
103 })?;
104
105 let plugin = self.loader.load(&wasm_bytes)?;
106 let command_name = plugin.manifest.command.name.clone();
107
108 let mut inner = self.inner.write().await;
109
110 if let Some(old_cmd) = inner.path_to_command.remove(path) {
112 inner.plugins.remove(&old_cmd);
113 }
114
115 inner
117 .path_to_command
118 .insert(path.to_path_buf(), command_name.clone());
119 inner.plugins.insert(
120 command_name.clone(),
121 PluginEntry {
122 plugin,
123 source_path: Some(path.to_path_buf()),
124 },
125 );
126
127 tracing::info!(command = %command_name, path = %path.display(), "Plugin loaded");
128 Ok(command_name)
129 }
130
131 pub async fn register(&self, plugin: LoadedPlugin) -> String {
133 let command_name = plugin.manifest.command.name.clone();
134
135 let mut inner = self.inner.write().await;
136 inner.plugins.insert(
137 command_name.clone(),
138 PluginEntry {
139 plugin,
140 source_path: None,
141 },
142 );
143
144 tracing::info!(command = %command_name, "Plugin registered");
145 command_name
146 }
147
148 pub async fn unload_by_path(&self, path: impl AsRef<Path>) -> Option<String> {
150 let path = path.as_ref();
151 let mut inner = self.inner.write().await;
152
153 if let Some(command_name) = inner.path_to_command.remove(path) {
154 inner.plugins.remove(&command_name);
155 tracing::info!(command = %command_name, path = %path.display(), "Plugin unloaded");
156 Some(command_name)
157 } else {
158 None
159 }
160 }
161
162 pub async fn unload(&self, command_name: &str) -> bool {
164 let mut inner = self.inner.write().await;
165
166 if let Some(entry) = inner.plugins.remove(command_name) {
167 if let Some(path) = entry.source_path {
168 inner.path_to_command.remove(&path);
169 }
170 tracing::info!(command = %command_name, "Plugin unloaded");
171 true
172 } else {
173 false
174 }
175 }
176
177 pub async fn reload_by_path(&self, path: impl AsRef<Path>) -> Result<String, LoaderError> {
179 self.load_plugin(path).await
180 }
181
182 pub async fn list_commands(&self) -> Vec<String> {
184 let inner = self.inner.read().await;
185 inner.plugins.keys().cloned().collect()
186 }
187
188 pub async fn has_command(&self, command_name: &str) -> bool {
190 let inner = self.inner.read().await;
191 inner.plugins.contains_key(command_name)
192 }
193
194 pub async fn execute(
203 &self,
204 command_name: &str,
205 args: &[String],
206 ) -> Result<sen_plugin_api::ExecuteResult, RegistryError> {
207 let mut inner = self.inner.write().await;
208
209 let entry = inner
210 .plugins
211 .get_mut(command_name)
212 .ok_or_else(|| RegistryError::CommandNotFound(command_name.to_string()))?;
213
214 if let Some(ref perm_config) = self.permission {
216 let capabilities = &entry.plugin.manifest.capabilities;
217
218 let _ = perm_config
220 .audit
221 .record(audit::permission_requested(command_name, capabilities));
222
223 let key =
225 perm_config
226 .store
227 .make_key(command_name, None, perm_config.strategy.granularity());
228 let stored = perm_config.store.get(&key).ok().flatten();
229
230 let ctx = PermissionContext {
232 plugin_name: command_name,
233 command_path: &[],
234 requested: capabilities,
235 granted: stored.as_ref().map(|s| &s.capabilities),
236 interactive: perm_config.prompt.is_interactive(),
237 };
238
239 let decision = if let Some(ref stored_perm) = stored {
241 if stored_perm.has_escalated(capabilities) {
242 let _ = perm_config.audit.record(audit::escalation_detected(
244 command_name,
245 &stored_perm.capabilities,
246 capabilities,
247 ));
248 perm_config.strategy.on_escalation(&ctx)
249 } else {
250 perm_config.strategy.check(&ctx)
251 }
252 } else {
253 perm_config.strategy.check(&ctx)
254 };
255
256 match decision {
258 PermissionDecision::Allow => {
259 let _ = perm_config.audit.record(audit::permission_granted(
260 command_name,
261 capabilities,
262 TrustLevel::Permanent,
263 ));
264 }
265 PermissionDecision::Deny(reason) => {
266 let _ = perm_config.audit.record(audit::permission_denied(
267 command_name,
268 capabilities,
269 &reason,
270 ));
271 return Err(RegistryError::PermissionDenied {
272 plugin: command_name.to_string(),
273 reason,
274 });
275 }
276 PermissionDecision::Prompt => {
277 let prompt_result = if let Some(ref stored_perm) = stored {
279 perm_config.prompt.prompt_escalation(
280 command_name,
281 &stored_perm.capabilities,
282 capabilities,
283 )
284 } else {
285 perm_config.prompt.prompt(command_name, capabilities)
286 };
287
288 match prompt_result {
289 Ok(result) if result.is_allowed() => {
290 if result.should_persist() {
292 let trust_level =
293 result.to_trust_level().unwrap_or(StoredTrustLevel::Session);
294 let stored_perm =
295 StoredPermission::new(capabilities.clone(), trust_level);
296 let _ = perm_config.store.set(&key, stored_perm);
297 }
298
299 let audit_trust = match result.to_trust_level() {
300 Some(StoredTrustLevel::Permanent) => TrustLevel::Permanent,
301 Some(StoredTrustLevel::Session) => TrustLevel::Session,
302 None => TrustLevel::Once,
303 };
304 let _ = perm_config.audit.record(audit::permission_granted(
305 command_name,
306 capabilities,
307 audit_trust,
308 ));
309 }
310 Ok(_) | Err(_) => {
311 let _ = perm_config.audit.record(audit::permission_denied(
312 command_name,
313 capabilities,
314 "User denied permission",
315 ));
316 return Err(RegistryError::PermissionDenied {
317 plugin: command_name.to_string(),
318 reason: "User denied permission".to_string(),
319 });
320 }
321 }
322 }
323 PermissionDecision::AllowPartial(_reduced) => {
324 let _ = perm_config.audit.record(audit::permission_granted(
327 command_name,
328 capabilities,
329 TrustLevel::Once,
330 ));
331 }
332 }
333 }
334
335 entry
336 .plugin
337 .instance
338 .execute(args)
339 .map_err(RegistryError::Execution)
340 }
341
342 pub async fn get_manifest(&self, command_name: &str) -> Option<sen_plugin_api::PluginManifest> {
344 let inner = self.inner.read().await;
345 inner
346 .plugins
347 .get(command_name)
348 .map(|e| e.plugin.manifest.clone())
349 }
350
351 pub async fn get_all_manifests(&self) -> Vec<sen_plugin_api::PluginManifest> {
353 let inner = self.inner.read().await;
354 inner
355 .plugins
356 .values()
357 .map(|e| e.plugin.manifest.clone())
358 .collect()
359 }
360
361 pub async fn len(&self) -> usize {
363 let inner = self.inner.read().await;
364 inner.plugins.len()
365 }
366
367 pub async fn is_empty(&self) -> bool {
369 let inner = self.inner.read().await;
370 inner.plugins.is_empty()
371 }
372}
373
374#[derive(Debug, thiserror::Error)]
376pub enum RegistryError {
377 #[error("Command not found: {0}")]
378 CommandNotFound(String),
379
380 #[error("Plugin execution failed: {0}")]
381 Execution(#[source] LoaderError),
382
383 #[error("Permission denied for plugin '{plugin}': {reason}")]
384 PermissionDenied { plugin: String, reason: String },
385}
386
387#[cfg(test)]
388mod tests {
389 use super::*;
390 use crate::audit::MemoryAuditSink;
391 use crate::permission::{
392 AutoPromptHandler, MemoryPermissionStore, PermissionPresets, PermissionStore, PromptResult,
393 RecordingPromptHandler,
394 };
395
396 const HELLO_PLUGIN_WASM: &[u8] = include_bytes!(
397 "../../examples/hello-plugin/target/wasm32-unknown-unknown/release/hello_plugin.wasm"
398 );
399
400 #[tokio::test]
405 async fn test_registry_register_and_execute() {
406 let registry = PluginRegistry::new().unwrap();
407 let loader = PluginLoader::new().unwrap();
408 let plugin = loader.load(HELLO_PLUGIN_WASM).unwrap();
409
410 let cmd = registry.register(plugin).await;
411 assert_eq!(cmd, "hello");
412 assert!(registry.has_command("hello").await);
413
414 let result = registry
415 .execute("hello", &["World".to_string()])
416 .await
417 .unwrap();
418 match result {
419 sen_plugin_api::ExecuteResult::Success(output) => {
420 assert_eq!(output, "Hello, World!");
421 }
422 _ => panic!("Expected success"),
423 }
424 }
425
426 #[tokio::test]
427 async fn test_registry_unload() {
428 let registry = PluginRegistry::new().unwrap();
429 let loader = PluginLoader::new().unwrap();
430 let plugin = loader.load(HELLO_PLUGIN_WASM).unwrap();
431
432 registry.register(plugin).await;
433 assert!(registry.has_command("hello").await);
434
435 registry.unload("hello").await;
436 assert!(!registry.has_command("hello").await);
437 }
438
439 #[tokio::test]
440 async fn test_registry_list_commands() {
441 let registry = PluginRegistry::new().unwrap();
442 let loader = PluginLoader::new().unwrap();
443 let plugin = loader.load(HELLO_PLUGIN_WASM).unwrap();
444
445 registry.register(plugin).await;
446
447 let commands = registry.list_commands().await;
448 assert_eq!(commands, vec!["hello"]);
449 }
450
451 #[tokio::test]
456 async fn test_registry_with_permissions_trust_all() {
457 let config = PermissionPresets::trust_all_dangerous();
459 let registry = PluginRegistry::with_permissions(config).unwrap();
460
461 let loader = PluginLoader::new().unwrap();
462 let plugin = loader.load(HELLO_PLUGIN_WASM).unwrap();
463 registry.register(plugin).await;
464
465 let result = registry
466 .execute("hello", &["World".to_string()])
467 .await
468 .unwrap();
469
470 match result {
471 sen_plugin_api::ExecuteResult::Success(output) => {
472 assert_eq!(output, "Hello, World!");
473 }
474 _ => panic!("Expected success with trust_all"),
475 }
476 }
477
478 #[tokio::test]
479 async fn test_registry_with_permissions_testing_preset() {
480 let config = PermissionPresets::testing();
482 let registry = PluginRegistry::with_permissions(config).unwrap();
483
484 let loader = PluginLoader::new().unwrap();
485 let plugin = loader.load(HELLO_PLUGIN_WASM).unwrap();
486 registry.register(plugin).await;
487
488 let result = registry
489 .execute("hello", &["World".to_string()])
490 .await
491 .unwrap();
492
493 match result {
494 sen_plugin_api::ExecuteResult::Success(output) => {
495 assert_eq!(output, "Hello, World!");
496 }
497 _ => panic!("Expected success with testing preset"),
498 }
499 }
500
501 #[tokio::test]
502 async fn test_registry_with_permissions_deny_on_prompt() {
503 let config = PermissionConfig::new(
505 crate::permission::DefaultPermissionStrategy,
506 MemoryPermissionStore::new(),
507 AutoPromptHandler::always_deny(),
508 crate::audit::NullAuditSink,
509 crate::permission::TrustFlagConfig::default(),
510 );
511
512 let registry = PluginRegistry::with_permissions(config).unwrap();
513
514 let loader = PluginLoader::new().unwrap();
515 let mut plugin = loader.load(HELLO_PLUGIN_WASM).unwrap();
516
517 plugin.manifest.capabilities = sen_plugin_api::Capabilities::default()
519 .with_stdio(sen_plugin_api::StdioCapability::stdout_only());
520
521 registry.register(plugin).await;
522
523 let result = registry.execute("hello", &["World".to_string()]).await;
524
525 match result {
526 Err(RegistryError::PermissionDenied { plugin, reason }) => {
527 assert_eq!(plugin, "hello");
528 assert!(reason.contains("denied"));
529 }
530 Ok(_) => panic!("Expected PermissionDenied error"),
531 Err(e) => panic!("Unexpected error: {:?}", e),
532 }
533 }
534
535 #[tokio::test]
536 async fn test_registry_with_permissions_audit_logging() {
537 let audit_sink = std::sync::Arc::new(MemoryAuditSink::new());
539
540 let config = PermissionConfig {
541 strategy: std::sync::Arc::new(crate::permission::DefaultPermissionStrategy),
542 store: std::sync::Arc::new(MemoryPermissionStore::new()),
543 prompt: std::sync::Arc::new(AutoPromptHandler::always_allow()),
544 audit: audit_sink.clone(),
545 trust_flags: crate::permission::TrustFlagConfig::default(),
546 };
547
548 let registry = PluginRegistry::with_permissions(config).unwrap();
549
550 let loader = PluginLoader::new().unwrap();
551 let mut plugin = loader.load(HELLO_PLUGIN_WASM).unwrap();
552 plugin.manifest.capabilities = sen_plugin_api::Capabilities::default()
553 .with_stdio(sen_plugin_api::StdioCapability::stdout_only());
554
555 registry.register(plugin).await;
556 let _ = registry.execute("hello", &["World".to_string()]).await;
557
558 let events = audit_sink.events();
560 assert!(!events.is_empty(), "Should have audit events");
561
562 let request_events =
564 audit_sink.find_by_type(crate::audit::AuditEventType::PermissionRequested);
565 assert!(
566 !request_events.is_empty(),
567 "Should have permission request event"
568 );
569 }
570
571 #[tokio::test]
572 async fn test_registry_with_permissions_stores_grant() {
573 let store = std::sync::Arc::new(MemoryPermissionStore::new());
575
576 let config = PermissionConfig {
577 strategy: std::sync::Arc::new(crate::permission::DefaultPermissionStrategy),
578 store: store.clone(),
579 prompt: std::sync::Arc::new(AutoPromptHandler::always_allow()),
580 audit: std::sync::Arc::new(crate::audit::NullAuditSink),
581 trust_flags: crate::permission::TrustFlagConfig::default(),
582 };
583
584 let registry = PluginRegistry::with_permissions(config).unwrap();
585
586 let loader = PluginLoader::new().unwrap();
587 let mut plugin = loader.load(HELLO_PLUGIN_WASM).unwrap();
588 plugin.manifest.capabilities = sen_plugin_api::Capabilities::default()
589 .with_stdio(sen_plugin_api::StdioCapability::stdout_only());
590
591 registry.register(plugin).await;
592 let _ = registry.execute("hello", &["World".to_string()]).await;
593
594 let stored = store.get("hello").unwrap();
596 assert!(stored.is_some(), "Permission should be stored after grant");
597 }
598
599 #[tokio::test]
600 async fn test_registry_with_permissions_prompt_recording() {
601 let prompt_handler =
603 std::sync::Arc::new(RecordingPromptHandler::new(PromptResult::AllowAlways));
604
605 let config = PermissionConfig {
606 strategy: std::sync::Arc::new(crate::permission::DefaultPermissionStrategy),
607 store: std::sync::Arc::new(MemoryPermissionStore::new()),
608 prompt: prompt_handler.clone(),
609 audit: std::sync::Arc::new(crate::audit::NullAuditSink),
610 trust_flags: crate::permission::TrustFlagConfig::default(),
611 };
612
613 let registry = PluginRegistry::with_permissions(config).unwrap();
614
615 let loader = PluginLoader::new().unwrap();
616 let mut plugin = loader.load(HELLO_PLUGIN_WASM).unwrap();
617 plugin.manifest.capabilities = sen_plugin_api::Capabilities::default()
618 .with_stdio(sen_plugin_api::StdioCapability::stdout_only());
619
620 registry.register(plugin).await;
621 let _ = registry.execute("hello", &["World".to_string()]).await;
622
623 assert_eq!(
625 prompt_handler.prompt_count(),
626 1,
627 "Should have prompted once"
628 );
629 let prompts = prompt_handler.prompts();
630 assert_eq!(prompts[0].plugin, "hello");
631 }
632
633 #[tokio::test]
634 async fn test_registry_without_permissions_skips_check() {
635 let registry = PluginRegistry::new().unwrap();
637
638 let loader = PluginLoader::new().unwrap();
639 let mut plugin = loader.load(HELLO_PLUGIN_WASM).unwrap();
640
641 plugin.manifest.capabilities = sen_plugin_api::Capabilities::default()
643 .with_fs_read(vec![sen_plugin_api::PathPattern::new("/")]);
644
645 registry.register(plugin).await;
646
647 let result = registry
649 .execute("hello", &["World".to_string()])
650 .await
651 .unwrap();
652
653 match result {
654 sen_plugin_api::ExecuteResult::Success(output) => {
655 assert_eq!(output, "Hello, World!");
656 }
657 _ => panic!("Expected success without permission config"),
658 }
659 }
660
661 #[tokio::test]
662 async fn test_registry_empty_capabilities_allowed() {
663 let prompt_handler = std::sync::Arc::new(RecordingPromptHandler::new(PromptResult::Deny));
665
666 let config = PermissionConfig {
667 strategy: std::sync::Arc::new(crate::permission::DefaultPermissionStrategy),
668 store: std::sync::Arc::new(MemoryPermissionStore::new()),
669 prompt: prompt_handler.clone(),
670 audit: std::sync::Arc::new(crate::audit::NullAuditSink),
671 trust_flags: crate::permission::TrustFlagConfig::default(),
672 };
673
674 let registry = PluginRegistry::with_permissions(config).unwrap();
675
676 let loader = PluginLoader::new().unwrap();
677 let plugin = loader.load(HELLO_PLUGIN_WASM).unwrap();
678 registry.register(plugin).await;
681 let result = registry.execute("hello", &["World".to_string()]).await;
682
683 assert!(result.is_ok(), "Empty capabilities should be allowed");
685 assert_eq!(
686 prompt_handler.prompt_count(),
687 0,
688 "Should not prompt for empty caps"
689 );
690 }
691}