Skip to main content

tf_types/
plugin.rs

1//! PluginRegistry — native-plugin mirror of
2//! `tools/tf-types-ts/src/core/plugin.ts`.
3//!
4//! Native plugins in Rust register a trait implementation up-front (plugins
5//! are compiled in, not dlopen'd — safe subset for the prototype). The
6//! registry validates the manifest's ed25519 signature over its canonical
7//! JSON form with signature.signature cleared.
8//!
9//! WASM plugins are handled by the sibling `plugin_wasm` module; instantiate
10//! a `WasmPlugin` for capability-gated WASM execution. Native Rust plugins
11//! continue to be compiled in (the registry doesn't dlopen).
12
13use std::collections::HashMap;
14use std::fs;
15use std::path::Path;
16use std::sync::Arc;
17
18use serde_json::Value;
19
20use crate::canonical::canonicalize;
21use crate::crypto::{b64decode, ed25519_verify, CryptoError};
22
23#[derive(Debug, thiserror::Error)]
24pub enum PluginError {
25    #[error("plugin I/O error: {0}")]
26    Io(String),
27    #[error("plugin parse error: {0}")]
28    Parse(String),
29    #[error("plugin signature invalid: {0}")]
30    BadSignature(String),
31    #[error("unknown plugin kind: {0}")]
32    UnknownKind(String),
33    #[error("crypto error: {0}")]
34    Crypto(String),
35}
36
37impl From<CryptoError> for PluginError {
38    fn from(e: CryptoError) -> Self {
39        PluginError::Crypto(e.to_string())
40    }
41}
42
43/// Opaque handler. Native Rust plugins register concrete implementations.
44pub type NativeHandler = Arc<dyn Fn(&Value) -> Result<Value, String> + Send + Sync + 'static>;
45
46pub struct LoadedPlugin {
47    pub plugin_id: String,
48    pub actor_id: String,
49    pub kind: String,
50    pub capabilities: Vec<String>,
51    /// Map from capability name → handler. Populated for native plugins that
52    /// supplied handlers via `register_handler`.
53    pub handlers: HashMap<String, NativeHandler>,
54}
55
56impl std::fmt::Debug for LoadedPlugin {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        f.debug_struct("LoadedPlugin")
59            .field("plugin_id", &self.plugin_id)
60            .field("actor_id", &self.actor_id)
61            .field("kind", &self.kind)
62            .field("capabilities", &self.capabilities)
63            .field("handler_count", &self.handlers.len())
64            .finish()
65    }
66}
67
68pub struct PluginRegistry {
69    plugins: Vec<LoadedPlugin>,
70}
71
72impl PluginRegistry {
73    pub fn new() -> Self {
74        PluginRegistry {
75            plugins: Vec::new(),
76        }
77    }
78
79    /// Verify a manifest's signature and register it. Native Rust plugins
80    /// supply their handlers in `handlers` keyed by capability name.
81    pub fn load_native<P: AsRef<Path>>(
82        &mut self,
83        manifest_path: P,
84        handlers: HashMap<String, NativeHandler>,
85    ) -> Result<&LoadedPlugin, PluginError> {
86        let raw = fs::read_to_string(manifest_path.as_ref())
87            .map_err(|e| PluginError::Io(e.to_string()))?;
88        let manifest: Value = {
89            let yaml: serde_yaml::Value =
90                serde_yaml::from_str(&raw).map_err(|e| PluginError::Parse(e.to_string()))?;
91            serde_json::to_value(yaml).map_err(|e| PluginError::Parse(e.to_string()))?
92        };
93        let kind = manifest
94            .get("kind")
95            .and_then(|v| v.as_str())
96            .unwrap_or("")
97            .to_string();
98        if kind != "native" {
99            return Err(PluginError::UnknownKind(kind));
100        }
101        let plugin_id = manifest
102            .get("plugin_id")
103            .and_then(|v| v.as_str())
104            .unwrap_or("")
105            .to_string();
106        verify_signature_value(&manifest, &plugin_id)?;
107        let actor_id = manifest
108            .get("actor_id")
109            .and_then(|v| v.as_str())
110            .unwrap_or("")
111            .to_string();
112        let capability_names: Vec<String> = manifest
113            .get("capabilities")
114            .and_then(|v| v.as_array())
115            .map(|arr| {
116                arr.iter()
117                    .filter_map(|v| v.get("name").and_then(|n| n.as_str()).map(str::to_string))
118                    .collect()
119            })
120            .unwrap_or_default();
121        let plugin = LoadedPlugin {
122            plugin_id,
123            actor_id,
124            kind,
125            capabilities: capability_names,
126            handlers,
127        };
128        self.plugins.push(plugin);
129        Ok(self.plugins.last().unwrap())
130    }
131
132    pub fn list(&self) -> &[LoadedPlugin] {
133        &self.plugins
134    }
135
136    pub fn invoke(
137        &self,
138        plugin_id: &str,
139        capability: &str,
140        request: &Value,
141    ) -> Result<Value, PluginError> {
142        let plugin = self
143            .plugins
144            .iter()
145            .find(|p| p.plugin_id == plugin_id)
146            .ok_or_else(|| PluginError::Parse(format!("plugin not loaded: {}", plugin_id)))?;
147        let handler = plugin.handlers.get(capability).ok_or_else(|| {
148            PluginError::Parse(format!("no handler for capability: {}", capability))
149        })?;
150        handler(request).map_err(PluginError::Parse)
151    }
152}
153
154/// Standalone signature verifier for tooling (a CLI's `tf plugin verify`).
155pub fn verify_plugin_signature<P: AsRef<Path>>(manifest_path: P) -> Result<String, PluginError> {
156    let raw =
157        fs::read_to_string(manifest_path.as_ref()).map_err(|e| PluginError::Io(e.to_string()))?;
158    let yaml: serde_yaml::Value =
159        serde_yaml::from_str(&raw).map_err(|e| PluginError::Parse(e.to_string()))?;
160    let manifest: Value =
161        serde_json::to_value(yaml).map_err(|e| PluginError::Parse(e.to_string()))?;
162    let plugin_id = manifest
163        .get("plugin_id")
164        .and_then(|v| v.as_str())
165        .unwrap_or("")
166        .to_string();
167    verify_signature_value(&manifest, &plugin_id)?;
168    Ok(plugin_id)
169}
170
171fn verify_signature_value(manifest: &Value, plugin_id: &str) -> Result<(), PluginError> {
172    let sig_b64 = manifest
173        .get("signature")
174        .and_then(|s| s.get("signature"))
175        .and_then(|v| v.as_str())
176        .ok_or_else(|| PluginError::BadSignature(plugin_id.to_string()))?
177        .to_string();
178    let ident_pub = manifest
179        .get("identity_pub")
180        .and_then(|v| v.as_str())
181        .ok_or_else(|| PluginError::BadSignature(plugin_id.to_string()))?
182        .to_string();
183
184    // Clone manifest and clear signature.signature.
185    let mut cleared = manifest.clone();
186    if let Some(sig) = cleared.get_mut("signature").and_then(|s| s.as_object_mut()) {
187        sig.insert("signature".to_string(), Value::String(String::new()));
188    }
189    let canonical = canonicalize(&cleared).map_err(|e| PluginError::Parse(e.to_string()))?;
190    let sig_bytes = b64decode(&sig_b64)?;
191    let pubkey_bytes = b64decode(&ident_pub)?;
192    ed25519_verify(&pubkey_bytes, canonical.as_bytes(), &sig_bytes)
193        .map_err(|_| PluginError::BadSignature(plugin_id.to_string()))?;
194    Ok(())
195}
196
197impl Default for PluginRegistry {
198    fn default() -> Self {
199        PluginRegistry::new()
200    }
201}