返回 CodeWhale
types.rs
根目录 / crates / tui / src / plugins / types.rs
1 use std::collections::HashMap;
2 use std::fmt;
3 use std::path::PathBuf;
4
5 use serde::{Deserialize, Serialize};
6
7 use super::activation::{PluginActivationCapability, PluginActivationPolicy};
8 use super::manifest::{
9 PluginCompatibility, PluginInventory, PluginManifest, ResolvedPluginComponents,
10 };
11
12 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
13 #[serde(rename_all = "snake_case")]
14 pub enum PluginScope {
15 Builtin,
16 User,
17 Workspace,
18 }
19
20 impl PluginScope {
21 #[must_use]
22 pub fn as_str(self) -> &'static str {
23 match self {
24 Self::Builtin => "builtin",
25 Self::User => "user",
26 Self::Workspace => "workspace",
27 }
28 }
29 }
30
31 impl fmt::Display for PluginScope {
32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 f.write_str(self.as_str())
34 }
35 }
36
37 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
38 #[serde(rename_all = "snake_case")]
39 pub enum PluginOrigin {
40 Builtin,
41 CodeWhaleHome,
42 Workspace,
43 }
44
45 impl PluginOrigin {
46 #[must_use]
47 pub fn as_str(self) -> &'static str {
48 match self {
49 Self::Builtin => "codewhale-builtin",
50 Self::CodeWhaleHome => "codewhale-home",
51 Self::Workspace => "workspace-codewhale",
52 }
53 }
54 }
55
56 impl fmt::Display for PluginOrigin {
57 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58 f.write_str(self.as_str())
59 }
60 }
61
62 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
63 #[serde(transparent)]
64 pub struct PluginId(pub String);
65
66 impl PluginId {
67 #[must_use]
68 pub fn as_str(&self) -> &str {
69 &self.0
70 }
71 }
72
73 /// Persistable proof of the exact reviewed plugin authority attached to a
74 /// Skill or MCP server. Runtime contexts keep this receipt instead of a
75 /// pointer to mutable process-global discovery state, and revalidate it at
76 /// every side-effect boundary.
77 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
78 #[serde(deny_unknown_fields)]
79 pub struct PluginAuthority {
80 pub plugin_id: PluginId,
81 pub plugin_name: String,
82 pub workspace: PathBuf,
83 pub state_path: PathBuf,
84 pub source_manifest: PathBuf,
85 pub staged_manifest: PathBuf,
86 pub content_hash: String,
87 pub capability_hash: String,
88 /// Monotonic generation of this plugin's persisted authority. Any trust,
89 /// enablement, disablement, or revocation transition invalidates receipts
90 /// held by another process immediately, even when hashes are unchanged.
91 pub state_generation: u64,
92 }
93
94 impl fmt::Display for PluginId {
95 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96 f.write_str(&self.0)
97 }
98 }
99
100 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
101 #[serde(rename_all = "snake_case")]
102 pub enum PluginDiagnosticLevel {
103 Warning,
104 Error,
105 }
106
107 #[derive(Debug, Clone, PartialEq, Eq)]
108 pub struct PluginDiagnostic {
109 pub level: PluginDiagnosticLevel,
110 pub code: &'static str,
111 pub message: String,
112 pub path: Option<PathBuf>,
113 }
114
115 impl PluginDiagnostic {
116 #[must_use]
117 pub fn warning(code: &'static str, message: impl Into<String>, path: Option<PathBuf>) -> Self {
118 Self {
119 level: PluginDiagnosticLevel::Warning,
120 code,
121 message: message.into(),
122 path,
123 }
124 }
125
126 #[must_use]
127 pub fn error(code: &'static str, message: impl Into<String>, path: Option<PathBuf>) -> Self {
128 Self {
129 level: PluginDiagnosticLevel::Error,
130 code,
131 message: message.into(),
132 path,
133 }
134 }
135 }
136
137 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
138 pub enum PluginTrustStatus {
139 Trusted,
140 NeverReviewed,
141 ContentChanged,
142 CapabilitiesChanged,
143 }
144
145 impl PluginTrustStatus {
146 #[must_use]
147 pub fn as_str(self) -> &'static str {
148 match self {
149 Self::Trusted => "trusted",
150 Self::NeverReviewed => "not-reviewed",
151 Self::ContentChanged => "content-changed",
152 Self::CapabilitiesChanged => "capabilities-changed",
153 }
154 }
155 }
156
157 #[derive(Debug, Clone)]
158 pub struct PluginSkillSnapshot {
159 pub name: String,
160 pub legacy_activation_name: Option<String>,
161 pub description: String,
162 pub localized_descriptions: HashMap<String, String>,
163 pub invocation: crate::skills::SkillInvocation,
164 pub aliases: Vec<String>,
165 pub argument_hint: Option<String>,
166 pub body: String,
167 pub path: PathBuf,
168 /// Digest of the exact UTF-8 bytes parsed into this snapshot. This is the
169 /// corresponding entry in the reviewed bundle's file-hash inventory.
170 pub source_hash: String,
171 }
172
173 #[derive(Debug, Clone)]
174 pub struct LoadedPlugin {
175 pub id: PluginId,
176 pub manifest: PluginManifest,
177 pub base_path: PathBuf,
178 pub canonical_root: PathBuf,
179 /// Codewhale-owned, content-addressed copy used for active execution.
180 /// `None` means the reviewed bundle has not been staged safely and cannot
181 /// become active even if an older state file says it was enabled.
182 pub staged_root: Option<PathBuf>,
183 pub scope: PluginScope,
184 pub origin: PluginOrigin,
185 pub enabled: bool,
186 pub trust_status: PluginTrustStatus,
187 pub applicable: bool,
188 pub inventory: PluginInventory,
189 pub components: ResolvedPluginComponents,
190 pub content_hash: String,
191 pub capability_hash: String,
192 pub state_generation: u64,
193 pub skill_snapshots: Vec<PluginSkillSnapshot>,
194 pub diagnostics: Vec<PluginDiagnostic>,
195 }
196
197 impl LoadedPlugin {
198 #[must_use]
199 pub fn name(&self) -> &str {
200 &self.manifest.plugin.name
201 }
202
203 #[must_use]
204 pub fn trusted(&self) -> bool {
205 self.trust_status == PluginTrustStatus::Trusted
206 }
207
208 /// Explicit-review confirmation token binding a trust confirmation to
209 /// both complete SHA-256 receipts, so a same-inventory bundle cannot
210 /// collide through a short content prefix. The TUI `/plugin trust` flow
211 /// and the Runtime API trust endpoint both compare against this value.
212 #[must_use]
213 pub fn review_token(&self) -> String {
214 format!("{}.{}", self.content_hash, self.capability_hash)
215 }
216
217 #[must_use]
218 pub fn compatibility(&self) -> PluginCompatibility {
219 self.inventory.compatibility()
220 }
221
222 #[must_use]
223 pub fn active(&self) -> bool {
224 self.enabled
225 && self.trusted()
226 && self.staged_root.is_some()
227 && self.applicable
228 && self.inventory.can_activate_supported_components()
229 && !self
230 .diagnostics
231 .iter()
232 .any(|diagnostic| diagnostic.level == PluginDiagnosticLevel::Error)
233 }
234
235 /// Bundle-wide `active()` is not enough at a consumption boundary. Skills
236 /// must request Skills; each MCP server must request its stdio or remote
237 /// transport. A later policy that drops an adapter refuses that adapter
238 /// even if the bundle stays active for another supported surface.
239 #[must_use]
240 pub fn component_active(&self, capability: PluginActivationCapability) -> bool {
241 self.active() && PluginActivationPolicy::current().is_supported(capability)
242 }
243
244 #[must_use]
245 pub fn authority(&self, state_path: PathBuf, workspace: PathBuf) -> Option<PluginAuthority> {
246 let staged_root = self.staged_root.as_ref()?;
247 // The staged tree mirrors the source bundle byte-for-byte, so both
248 // share one manifest file name (`plugin.json` or legacy
249 // `plugin.toml`). Fall back to the legacy name for synthetic
250 // instances whose roots are not on disk.
251 let manifest_name = super::agent_plugin::resolve_manifest_path(&self.canonical_root)
252 .and_then(|path| path.file_name().map(|name| name.to_os_string()))
253 .unwrap_or_else(|| std::ffi::OsString::from("plugin.toml"));
254 Some(PluginAuthority {
255 plugin_id: self.id.clone(),
256 plugin_name: self.name().to_string(),
257 workspace,
258 state_path,
259 source_manifest: self.canonical_root.join(&manifest_name),
260 staged_manifest: staged_root.join(&manifest_name),
261 content_hash: self.content_hash.clone(),
262 capability_hash: self.capability_hash.clone(),
263 state_generation: self.state_generation,
264 })
265 }
266
267 #[must_use]
268 pub fn state_label(&self) -> &'static str {
269 if self.active() {
270 "active"
271 } else if !self.enabled {
272 "disabled"
273 } else if !self.trusted() {
274 "enabled-untrusted"
275 } else if self.staged_root.is_none() {
276 "unstaged"
277 } else if !self.applicable {
278 "inapplicable"
279 } else if !self.inventory.can_activate_supported_components() {
280 "unsupported"
281 } else {
282 "inactive"
283 }
284 }
285 }
286
286 lines RUST