返回 CodeWhale
native_presets.rs
根目录 / crates / tui / src / plugins / native_presets.rs
1 //! Pure roster facts tied to existing Native entry receipts. No new profile,
2 //! session, filesystem root or writable catalog authority.
3 use super::{PluginRegistry, activation::PluginActivationCapability};
4 use crate::extension_host::composition_scope::NativePresetRef;
5 use crate::extension_host::protocol::EntryRef;
6 use serde::Deserialize;
7 use std::collections::{BTreeMap, BTreeSet};
8 use std::io::Read;
9 use std::path::Path;
10
11 const MARKER: &str = "// codewhale-native-preset-v1 ";
12 const MAX_ENTRY: u64 = 16 * 1024;
13
14 #[derive(Debug, Clone, Deserialize)]
15 #[serde(deny_unknown_fields)]
16 pub(crate) struct NativePresetMetadata {
17 pub id: String,
18 pub trust: String,
19 pub name: Option<String>,
20 pub description: Option<String>,
21 pub order: Option<f64>,
22 pub is_default: bool,
23 pub broken: Option<String>,
24 }
25
26 pub(crate) fn metadata_from_bytes(bytes: &[u8]) -> Option<NativePresetMetadata> {
27 let source = std::str::from_utf8(bytes).ok()?;
28 let first = source.lines().next()?.strip_prefix(MARKER)?;
29 let data: NativePresetMetadata = serde_json::from_str(first).ok()?;
30 if data.id.is_empty()
31 || data.id.len() > 64
32 || !data.id.as_bytes()[0].is_ascii_alphanumeric()
33 || !data
34 .id
35 .bytes()
36 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == b'-')
37 || !matches!(data.trust.as_str(), "system" | "user")
38 || data.name.as_ref().is_some_and(|v| v.len() > 4096)
39 || data
40 .description
41 .as_ref()
42 .is_some_and(|v| v.len() > 16 * 1024)
43 || data.order.is_some_and(|v| !v.is_finite())
44 || data.broken.is_some()
45 {
46 return None;
47 }
48 Some(data)
49 }
50
51 pub(crate) fn metadata(
52 plugins: &PluginRegistry,
53 preset: &NativePresetRef,
54 ) -> Option<NativePresetMetadata> {
55 let plugin = plugins.get(&preset.plugin_id)?;
56 if plugin.content_hash != preset.content_hash
57 || !plugin.component_active(PluginActivationCapability::Native)
58 {
59 return None;
60 }
61 let root = plugin.staged_root.as_deref()?;
62 let path = Path::new(&preset.entry.path);
63 // Reuse the sole discovery-to-stage translator: it resolves both the
64 // root and entry, proves confinement and requires an actual Native entry.
65 if !plugin.components.native.iter().any(|source| {
66 super::runtime::staged_component_path(&plugin.canonical_root, root, source)
67 .is_ok_and(|admitted| admitted == path)
68 }) {
69 return None;
70 }
71 let mut file = crate::plugins::manifest::open_bundle_file(path).ok()?;
72 let mut bytes = Vec::new();
73 file.by_ref()
74 .take(MAX_ENTRY + 1)
75 .read_to_end(&mut bytes)
76 .ok()?;
77 if bytes.len() > MAX_ENTRY as usize || crate::hashing::sha256_hex(&bytes) != preset.entry.sha256
78 {
79 return None;
80 }
81 metadata_from_bytes(&bytes)
82 }
83
84 fn reviewed_entry(
85 source: &super::runtime::PluginComponentSource,
86 checked: &mut BTreeMap<String, Option<super::manifest::ValidatedManifest>>,
87 ) -> Option<NativePresetRef> {
88 let id = source.authority.plugin_id.to_string();
89 let receipt = checked.entry(id.clone()).or_insert_with(|| {
90 let validated =
91 super::manifest::PluginManifest::validate_from_path(&source.authority.staged_manifest)
92 .ok()?;
93 (validated.content_hash == source.authority.content_hash
94 && validated.capability_hash == source.authority.capability_hash)
95 .then_some(validated)
96 });
97 let receipt = receipt.as_ref()?;
98 // Component paths and the validated receipt share canonical identity;
99 // the selected manifest's lexical alias is not a containment root.
100 let relative = source.path.strip_prefix(&receipt.canonical_root).ok()?;
101 Some(NativePresetRef {
102 plugin_id: id,
103 content_hash: source.authority.content_hash.clone(),
104 entry: EntryRef {
105 path: source.path.to_string_lossy().into_owned(),
106 sha256: receipt.native_entry_hashes.get(relative)?.clone(),
107 },
108 })
109 }
110
111 /// New roster choices come from a whole admitted bundle validation, not a
112 /// fresh hash of an unmounted mutable file mistaken for a reviewed receipt.
113 pub(crate) fn admitted_entries(
114 plugins: &PluginRegistry,
115 ) -> Vec<(
116 NativePresetRef,
117 NativePresetMetadata,
118 super::types::PluginAuthority,
119 )> {
120 let (sources, _) =
121 super::runtime::active_component_sources(plugins, PluginActivationCapability::Native);
122 let mut checked = BTreeMap::new();
123 let mut result = Vec::new();
124 for source in sources {
125 // Ordinary Native modules have no catalog metadata; do not rehash
126 // their whole bundles just to rediscover their existing roster view.
127 // active_component_sources already validated the authority and used
128 // the shared translator to prove this canonical entry is confined.
129 let Ok(mut file) = super::manifest::open_bundle_file(&source.path) else {
130 continue;
131 };
132 let mut header = Vec::new();
133 if file
134 .by_ref()
135 .take(MAX_ENTRY + 1)
136 .read_to_end(&mut header)
137 .is_err()
138 || header.len() > MAX_ENTRY as usize
139 || metadata_from_bytes(&header).is_none()
140 {
141 continue;
142 }
143 let Some(preset) = reviewed_entry(&source, &mut checked) else {
144 continue;
145 };
146 if let Some(data) = metadata(plugins, &preset) {
147 result.push((preset, data, source.authority));
148 }
149 }
150 result
151 }
152
153 /// One caller snapshot: each raw catalog chooses one default; ordinary Native
154 /// plugins keep their existing complete entry set. The manager may own a union
155 /// across callers but no caller receives a union of that catalog's presets.
156 pub(crate) fn default_selection(
157 plugins: &PluginRegistry,
158 ) -> (Vec<NativePresetRef>, BTreeSet<String>) {
159 let (sources, _) =
160 super::runtime::active_component_sources(plugins, PluginActivationCapability::Native);
161 let admitted = admitted_entries(plugins);
162 if admitted.is_empty() {
163 return (Vec::new(), BTreeSet::new());
164 }
165 let mut normal = Vec::new();
166 let mut catalogs: BTreeMap<String, Vec<(NativePresetRef, NativePresetMetadata)>> =
167 BTreeMap::new();
168 for (preset, data, _) in admitted {
169 catalogs
170 .entry(preset.plugin_id.clone())
171 .or_default()
172 .push((preset, data));
173 }
174 let mut checked = BTreeMap::new();
175 for source in sources {
176 if catalogs.contains_key(source.authority.plugin_id.as_str()) {
177 continue;
178 }
179 if let Some(entry) = reviewed_entry(&source, &mut checked) {
180 normal.push(entry);
181 }
182 }
183 if catalogs.is_empty() {
184 return (Vec::new(), BTreeSet::new());
185 }
186 let mut unselected = BTreeSet::new();
187 for (id, rows) in catalogs {
188 if let Some((preset, _)) = rows.into_iter().find(|(_, data)| data.is_default) {
189 normal.push(preset);
190 } else {
191 // Absence is upstream data, never permission to mount the first row.
192 unselected.insert(id);
193 }
194 }
195 (normal, unselected)
196 }
197
198 #[cfg(test)]
199 mod tests {
200 use super::*;
201 #[test]
202 fn native_preset_metadata_is_data_and_cannot_mint_trust_or_an_entry() {
203 let good = br#"// codewhale-native-preset-v1 {"id":"reviewer","trust":"user","name":"Repo reviewer","description":"bounded","is_default":true}
204 export function apply() {}"#;
205 assert_eq!(metadata_from_bytes(good).unwrap().id, "reviewer");
206 for source in [
207 r#"{"id":"../escape","trust":"user","is_default":true}"#,
208 r#"{"id":"ok","trust":"admin","is_default":true}"#,
209 r#"{"id":"ok","trust":"user","is_default":true,"entry":{"path":"outside"}}"#,
210 r#"{"id":"ok","trust":"user","is_default":true,"broken":"missing"}"#,
211 ] {
212 assert!(metadata_from_bytes(format!("{MARKER}{source}\n").as_bytes()).is_none());
213 }
214 }
215 }
216
216 lines RUST