返回 CodeWhale
runtime.rs
根目录 / crates / tui / src / plugins / runtime.rs
1 //! Runtime adapters for reviewed, content-addressed plugin components.
2 //!
3 //! This module is the only place that translates mutable discovery paths into
4 //! immutable staged component paths. Consumers still revalidate the attached
5 //! [`PluginAuthority`] at their execution boundary so disable, revoke, and
6 //! uninstall transitions in another process fail closed immediately.
7
8 use std::path::{Path, PathBuf};
9
10 use super::PluginRegistry;
11 use super::activation::PluginActivationCapability;
12 use super::registry::verify_plugin_component_authority;
13 use super::types::{LoadedPlugin, PluginAuthority, PluginScope};
14
15 #[derive(Debug, Clone, PartialEq, Eq)]
16 pub struct PluginComponentSource {
17 pub plugin_name: String,
18 pub path: PathBuf,
19 pub authority: PluginAuthority,
20 }
21
22 fn component_paths(plugin: &LoadedPlugin, capability: PluginActivationCapability) -> &[PathBuf] {
23 match capability {
24 PluginActivationCapability::Commands => &plugin.components.commands,
25 PluginActivationCapability::Agents => &plugin.components.agents,
26 PluginActivationCapability::Hooks => &plugin.components.hooks,
27 PluginActivationCapability::Skills
28 | PluginActivationCapability::McpStdio
29 | PluginActivationCapability::McpRemote
30 | PluginActivationCapability::Lsp
31 | PluginActivationCapability::Native
32 | PluginActivationCapability::FilesystemRoots
33 | PluginActivationCapability::LifecycleMutation => &[],
34 }
35 }
36
37 fn scope_precedence(scope: PluginScope) -> u8 {
38 match scope {
39 PluginScope::Workspace => 0,
40 PluginScope::User => 1,
41 PluginScope::Builtin => 2,
42 }
43 }
44
45 /// Resolve one active component kind into immutable staged paths.
46 ///
47 /// Workspace bundles win same-name collisions over user and built-in bundles;
48 /// consumers retain their existing non-plugin precedence above this list.
49 pub fn active_component_sources(
50 registry: &PluginRegistry,
51 capability: PluginActivationCapability,
52 ) -> (Vec<PluginComponentSource>, Vec<String>) {
53 let mut plugins = registry
54 .active_plugins()
55 .into_iter()
56 .filter(|plugin| plugin.component_active(capability))
57 .collect::<Vec<_>>();
58 plugins.sort_by(|left, right| {
59 scope_precedence(left.scope)
60 .cmp(&scope_precedence(right.scope))
61 .then_with(|| left.name().cmp(right.name()))
62 .then_with(|| left.id.cmp(&right.id))
63 });
64
65 let mut sources = Vec::new();
66 let mut errors = Vec::new();
67 for plugin in plugins {
68 let Some(authority) = registry.authority_for(plugin.id.as_str()) else {
69 errors.push(format!(
70 "Plugin `{}` has no persisted runtime authority",
71 plugin.name()
72 ));
73 continue;
74 };
75 if let Err(reason) = verify_plugin_component_authority(&authority, capability) {
76 errors.push(format!(
77 "Plugin `{}` {} adapter was denied: {reason}",
78 plugin.name(),
79 capability.as_str()
80 ));
81 continue;
82 }
83 let Some(staged_root) = plugin.staged_root.as_deref() else {
84 errors.push(format!(
85 "Plugin `{}` has no immutable runtime snapshot",
86 plugin.name()
87 ));
88 continue;
89 };
90 for source_path in component_paths(plugin, capability) {
91 match staged_component_path(&plugin.canonical_root, staged_root, source_path) {
92 Ok(path) => sources.push(PluginComponentSource {
93 plugin_name: plugin.name().to_string(),
94 path,
95 authority: authority.clone(),
96 }),
97 Err(reason) => errors.push(format!(
98 "Plugin `{}` {} component was denied: {reason}",
99 plugin.name(),
100 capability.as_str()
101 )),
102 }
103 }
104 }
105 (sources, errors)
106 }
107
108 fn staged_component_path(
109 canonical_root: &Path,
110 staged_root: &Path,
111 source_path: &Path,
112 ) -> Result<PathBuf, String> {
113 let relative = source_path
114 .strip_prefix(canonical_root)
115 .map_err(|_| "reviewed component escaped the plugin root".to_string())?;
116 let staged_root = staged_root
117 .canonicalize()
118 .map_err(|_| "runtime snapshot is unavailable".to_string())?;
119 let candidate = staged_root.join(relative);
120 let candidate = candidate
121 .canonicalize()
122 .map_err(|_| "runtime component is unavailable".to_string())?;
123 if !candidate.starts_with(&staged_root) {
124 return Err("runtime component escaped the immutable snapshot".to_string());
125 }
126 Ok(candidate)
127 }
128
128 lines RUST