返回 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 // Extension-host entry modules; only active under the v4 policy.
28 PluginActivationCapability::Native => &plugin.components.native,
29 PluginActivationCapability::Skills
30 | PluginActivationCapability::McpStdio
31 | PluginActivationCapability::McpRemote
32 | PluginActivationCapability::Lsp
33 | PluginActivationCapability::FilesystemRoots
34 | PluginActivationCapability::LifecycleMutation => &[],
35 }
36 }
37
38 /// Why `path` cannot be an extension-host entry, if it cannot.
39 ///
40 /// A `native` entry is one `.mjs`, `.js` or `.mts` ES module file: the host imports
41 /// exactly that file and re-hashes it first. This is the one statement of the
42 /// rule. Discovery reports it as an error diagnostic (so `/plugin validate`
43 /// and the review screen show it) and activation refuses the entry, both only
44 /// while the activation policy supports `Native` (`[features]
45 /// extension_host`). With the flag off, `native` stays inventory-only and any
46 /// path is accepted as before.
47 ///
48 /// `is_regular_file` comes from the caller's own view of the bundle (the
49 /// validated manifest's hashed files, or the activation read), so this
50 /// function touches no filesystem.
51 #[must_use]
52 pub fn native_entry_problem(path: &Path, is_regular_file: bool) -> Option<&'static str> {
53 const RULE: &str = "a native entry must be one .mjs, .js or .mts ES module file";
54 let is_module = path
55 .extension()
56 .is_some_and(|extension| extension == "mjs" || extension == "js" || extension == "mts");
57 (!is_module || !is_regular_file).then_some(RULE)
58 }
59
60 fn scope_precedence(scope: PluginScope) -> u8 {
61 match scope {
62 PluginScope::Workspace => 0,
63 PluginScope::User => 1,
64 PluginScope::Builtin => 2,
65 }
66 }
67
68 /// Resolve one active component kind into immutable staged paths.
69 ///
70 /// Workspace bundles win same-name collisions over user and built-in bundles;
71 /// consumers retain their existing non-plugin precedence above this list.
72 pub fn active_component_sources(
73 registry: &PluginRegistry,
74 capability: PluginActivationCapability,
75 ) -> (Vec<PluginComponentSource>, Vec<String>) {
76 let mut plugins = registry
77 .active_plugins()
78 .into_iter()
79 .filter(|plugin| plugin.component_active(capability))
80 .collect::<Vec<_>>();
81 plugins.sort_by(|left, right| {
82 scope_precedence(left.scope)
83 .cmp(&scope_precedence(right.scope))
84 .then_with(|| left.name().cmp(right.name()))
85 .then_with(|| left.id.cmp(&right.id))
86 });
87
88 let mut sources = Vec::new();
89 let mut errors = Vec::new();
90 for plugin in plugins {
91 let Some(authority) = registry.authority_for(plugin.id.as_str()) else {
92 errors.push(format!(
93 "Plugin `{}` has no persisted runtime authority",
94 plugin.name()
95 ));
96 continue;
97 };
98 if let Err(reason) = verify_plugin_component_authority(&authority, capability) {
99 errors.push(format!(
100 "Plugin `{}` {} adapter was denied: {reason}",
101 plugin.name(),
102 capability.as_str()
103 ));
104 continue;
105 }
106 let Some(staged_root) = plugin.staged_root.as_deref() else {
107 errors.push(format!(
108 "Plugin `{}` has no immutable runtime snapshot",
109 plugin.name()
110 ));
111 continue;
112 };
113 for source_path in component_paths(plugin, capability) {
114 match staged_component_path(&plugin.canonical_root, staged_root, source_path) {
115 Ok(path) => sources.push(PluginComponentSource {
116 plugin_name: plugin.name().to_string(),
117 path,
118 authority: authority.clone(),
119 }),
120 Err(reason) => errors.push(format!(
121 "Plugin `{}` {} component was denied: {reason}",
122 plugin.name(),
123 capability.as_str()
124 )),
125 }
126 }
127 }
128 (sources, errors)
129 }
130
131 pub(super) fn staged_component_path(
132 canonical_root: &Path,
133 staged_root: &Path,
134 source_path: &Path,
135 ) -> Result<PathBuf, String> {
136 let relative = source_path
137 .strip_prefix(canonical_root)
138 .map_err(|_| "reviewed component escaped the plugin root".to_string())?;
139 let staged_root = staged_root
140 .canonicalize()
141 .map_err(|_| "runtime snapshot is unavailable".to_string())?;
142 let candidate = staged_root.join(relative);
143 let candidate = candidate
144 .canonicalize()
145 .map_err(|_| "runtime component is unavailable".to_string())?;
146 if !candidate.starts_with(&staged_root) {
147 return Err("runtime component escaped the immutable snapshot".to_string());
148 }
149 Ok(candidate)
150 }
151
151 lines RUST