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            crate::yaml::parse(&raw).map_err(|e| PluginError::Parse(e.to_string()))?;
90        let kind = manifest
91            .get("kind")
92            .and_then(|v| v.as_str())
93            .unwrap_or("")
94            .to_string();
95        if kind != "native" {
96            return Err(PluginError::UnknownKind(kind));
97        }
98        let plugin_id = manifest
99            .get("plugin_id")
100            .and_then(|v| v.as_str())
101            .unwrap_or("")
102            .to_string();
103        verify_signature_value(&manifest, &plugin_id)?;
104        let actor_id = manifest
105            .get("actor_id")
106            .and_then(|v| v.as_str())
107            .unwrap_or("")
108            .to_string();
109        let capability_names: Vec<String> = manifest
110            .get("capabilities")
111            .and_then(|v| v.as_array())
112            .map(|arr| {
113                arr.iter()
114                    .filter_map(|v| v.get("name").and_then(|n| n.as_str()).map(str::to_string))
115                    .collect()
116            })
117            .unwrap_or_default();
118        let plugin = LoadedPlugin {
119            plugin_id,
120            actor_id,
121            kind,
122            capabilities: capability_names,
123            handlers,
124        };
125        self.plugins.push(plugin);
126        Ok(self.plugins.last().unwrap())
127    }
128
129    pub fn list(&self) -> &[LoadedPlugin] {
130        &self.plugins
131    }
132
133    pub fn invoke(
134        &self,
135        plugin_id: &str,
136        capability: &str,
137        request: &Value,
138    ) -> Result<Value, PluginError> {
139        let plugin = self
140            .plugins
141            .iter()
142            .find(|p| p.plugin_id == plugin_id)
143            .ok_or_else(|| PluginError::Parse(format!("plugin not loaded: {}", plugin_id)))?;
144        let handler = plugin.handlers.get(capability).ok_or_else(|| {
145            PluginError::Parse(format!("no handler for capability: {}", capability))
146        })?;
147        handler(request).map_err(PluginError::Parse)
148    }
149}
150
151/// Standalone signature verifier for tooling (a CLI's `tf plugin verify`).
152pub fn verify_plugin_signature<P: AsRef<Path>>(manifest_path: P) -> Result<String, PluginError> {
153    let raw =
154        fs::read_to_string(manifest_path.as_ref()).map_err(|e| PluginError::Io(e.to_string()))?;
155    let manifest: Value =
156        crate::yaml::parse(&raw).map_err(|e| PluginError::Parse(e.to_string()))?;
157    let plugin_id = manifest
158        .get("plugin_id")
159        .and_then(|v| v.as_str())
160        .unwrap_or("")
161        .to_string();
162    verify_signature_value(&manifest, &plugin_id)?;
163    Ok(plugin_id)
164}
165
166fn verify_signature_value(manifest: &Value, plugin_id: &str) -> Result<(), PluginError> {
167    let sig_b64 = manifest
168        .get("signature")
169        .and_then(|s| s.get("signature"))
170        .and_then(|v| v.as_str())
171        .ok_or_else(|| PluginError::BadSignature(plugin_id.to_string()))?
172        .to_string();
173    let ident_pub = manifest
174        .get("identity_pub")
175        .and_then(|v| v.as_str())
176        .ok_or_else(|| PluginError::BadSignature(plugin_id.to_string()))?
177        .to_string();
178
179    // Clone manifest and clear signature.signature.
180    let mut cleared = manifest.clone();
181    if let Some(sig) = cleared.get_mut("signature").and_then(|s| s.as_object_mut()) {
182        sig.insert("signature".to_string(), Value::String(String::new()));
183    }
184    let canonical = canonicalize(&cleared).map_err(|e| PluginError::Parse(e.to_string()))?;
185    let sig_bytes = b64decode(&sig_b64)?;
186    let pubkey_bytes = b64decode(&ident_pub)?;
187    ed25519_verify(&pubkey_bytes, canonical.as_bytes(), &sig_bytes)
188        .map_err(|_| PluginError::BadSignature(plugin_id.to_string()))?;
189    Ok(())
190}
191
192impl Default for PluginRegistry {
193    fn default() -> Self {
194        PluginRegistry::new()
195    }
196}