Skip to main content

pleiades_agent_plugins/
plugin.rs

1use std::path::{Path, PathBuf};
2use std::process::Command;
3
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7use crate::manifest::{
8    PluginError, PluginHooks, PluginLifecycle, PluginManifest, PluginToolPermission,
9};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12pub enum PluginKind {
13    Builtin,
14    Bundled,
15    External,
16}
17
18impl PluginKind {
19    pub fn marketplace(self) -> &'static str {
20        match self {
21            Self::Builtin => "builtin",
22            Self::Bundled => "bundled",
23            Self::External => "external",
24        }
25    }
26}
27
28impl std::fmt::Display for PluginKind {
29    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        match self {
31            Self::Builtin => write!(f, "builtin"),
32            Self::Bundled => write!(f, "bundled"),
33            Self::External => write!(f, "external"),
34        }
35    }
36}
37
38#[derive(Debug, Clone)]
39pub struct PluginMetadata {
40    pub id: String,
41    pub name: String,
42    pub version: String,
43    pub description: String,
44    pub kind: PluginKind,
45    pub source: String,
46    pub default_enabled: bool,
47    pub root: Option<PathBuf>,
48}
49
50#[derive(Debug, Clone)]
51pub struct PluginTool {
52    pub plugin_id: String,
53    pub plugin_name: String,
54    pub name: String,
55    pub description: String,
56    pub input_schema: Value,
57    pub command: String,
58    pub args: Vec<String>,
59    pub required_permission: PluginToolPermission,
60    pub root: Option<PathBuf>,
61}
62
63impl PluginTool {
64    pub fn execute(&self, input: &Value) -> Result<String, PluginError> {
65        let input_json = input.to_string();
66        let mut cmd = Command::new(&self.command);
67        cmd.args(&self.args)
68            .stdin(std::process::Stdio::piped())
69            .stdout(std::process::Stdio::piped())
70            .stderr(std::process::Stdio::piped())
71            .env("PLEIADES_PLUGIN_ID", &self.plugin_id)
72            .env("PLEIADES_PLUGIN_NAME", &self.plugin_name)
73            .env("PLEIADES_TOOL_NAME", &self.name)
74            .env("PLEIADES_TOOL_INPUT", &input_json);
75
76        if let Some(root) = &self.root {
77            cmd.current_dir(root)
78                .env("PLEIADES_PLUGIN_ROOT", root.display().to_string());
79        }
80
81        let mut child = cmd.spawn()?;
82        if let Some(mut stdin) = child.stdin.take() {
83            use std::io::Write;
84            let _ = stdin.write_all(input_json.as_bytes());
85        }
86
87        let output = child.wait_with_output()?;
88        if output.status.success() {
89            Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
90        } else {
91            let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
92            Err(PluginError::CommandFailed(format!(
93                "plugin tool `{}` from `{}` failed: {}",
94                self.name,
95                self.plugin_id,
96                if stderr.is_empty() {
97                    format!("exit status {}", output.status)
98                } else {
99                    stderr
100                },
101            )))
102        }
103    }
104}
105
106pub trait Plugin: std::fmt::Debug {
107    fn metadata(&self) -> &PluginMetadata;
108    fn hooks(&self) -> &PluginHooks;
109    fn lifecycle(&self) -> &PluginLifecycle;
110    fn tools(&self) -> &[PluginTool];
111}
112
113#[derive(Debug, Clone)]
114pub struct BuiltinPlugin {
115    pub metadata: PluginMetadata,
116    pub hooks: PluginHooks,
117    pub lifecycle: PluginLifecycle,
118    pub tools: Vec<PluginTool>,
119}
120
121impl Plugin for BuiltinPlugin {
122    fn metadata(&self) -> &PluginMetadata {
123        &self.metadata
124    }
125    fn hooks(&self) -> &PluginHooks {
126        &self.hooks
127    }
128    fn lifecycle(&self) -> &PluginLifecycle {
129        &self.lifecycle
130    }
131    fn tools(&self) -> &[PluginTool] {
132        &self.tools
133    }
134}
135
136#[derive(Debug, Clone)]
137pub struct BundledPlugin {
138    pub metadata: PluginMetadata,
139    pub hooks: PluginHooks,
140    pub lifecycle: PluginLifecycle,
141    pub tools: Vec<PluginTool>,
142}
143
144impl Plugin for BundledPlugin {
145    fn metadata(&self) -> &PluginMetadata {
146        &self.metadata
147    }
148    fn hooks(&self) -> &PluginHooks {
149        &self.hooks
150    }
151    fn lifecycle(&self) -> &PluginLifecycle {
152        &self.lifecycle
153    }
154    fn tools(&self) -> &[PluginTool] {
155        &self.tools
156    }
157}
158
159#[derive(Debug, Clone)]
160pub struct ExternalPlugin {
161    pub metadata: PluginMetadata,
162    pub hooks: PluginHooks,
163    pub lifecycle: PluginLifecycle,
164    pub tools: Vec<PluginTool>,
165}
166
167impl Plugin for ExternalPlugin {
168    fn metadata(&self) -> &PluginMetadata {
169        &self.metadata
170    }
171    fn hooks(&self) -> &PluginHooks {
172        &self.hooks
173    }
174    fn lifecycle(&self) -> &PluginLifecycle {
175        &self.lifecycle
176    }
177    fn tools(&self) -> &[PluginTool] {
178        &self.tools
179    }
180}
181
182#[derive(Debug, Clone)]
183pub enum PluginDefinition {
184    Builtin(BuiltinPlugin),
185    Bundled(BundledPlugin),
186    External(ExternalPlugin),
187}
188
189impl Plugin for PluginDefinition {
190    fn metadata(&self) -> &PluginMetadata {
191        match self {
192            Self::Builtin(p) => p.metadata(),
193            Self::Bundled(p) => p.metadata(),
194            Self::External(p) => p.metadata(),
195        }
196    }
197
198    fn hooks(&self) -> &PluginHooks {
199        match self {
200            Self::Builtin(p) => p.hooks(),
201            Self::Bundled(p) => p.hooks(),
202            Self::External(p) => p.hooks(),
203        }
204    }
205
206    fn lifecycle(&self) -> &PluginLifecycle {
207        match self {
208            Self::Builtin(p) => p.lifecycle(),
209            Self::Bundled(p) => p.lifecycle(),
210            Self::External(p) => p.lifecycle(),
211        }
212    }
213
214    fn tools(&self) -> &[PluginTool] {
215        match self {
216            Self::Builtin(p) => p.tools(),
217            Self::Bundled(p) => p.tools(),
218            Self::External(p) => p.tools(),
219        }
220    }
221}
222
223impl PluginDefinition {
224    pub fn load_from_directory(
225        root: &Path,
226        kind: PluginKind,
227        source: String,
228        marketplace: &str,
229    ) -> Result<Self, PluginError> {
230        let manifest = PluginManifest::load_from_directory(root)?;
231        let plugin_id = format!("{}-{}", manifest.name, marketplace);
232        let metadata = PluginMetadata {
233            id: plugin_id,
234            name: manifest.name,
235            version: manifest.version,
236            description: manifest.description,
237            kind,
238            source,
239            default_enabled: manifest.default_enabled,
240            root: Some(root.to_path_buf()),
241        };
242        let hooks = manifest.hooks;
243        let lifecycle = manifest.lifecycle;
244        let tools = manifest
245            .tools
246            .into_iter()
247            .map(|t| PluginTool {
248                plugin_id: metadata.id.clone(),
249                plugin_name: metadata.name.clone(),
250                name: t.name,
251                description: t.description,
252                input_schema: t.input_schema,
253                command: resolve_path(root, &t.command),
254                args: t.args,
255                required_permission: t.required_permission,
256                root: Some(root.to_path_buf()),
257            })
258            .collect();
259
260        Ok(match kind {
261            PluginKind::Builtin => Self::Builtin(BuiltinPlugin {
262                metadata,
263                hooks,
264                lifecycle,
265                tools,
266            }),
267            PluginKind::Bundled => Self::Bundled(BundledPlugin {
268                metadata,
269                hooks,
270                lifecycle,
271                tools,
272            }),
273            PluginKind::External => Self::External(ExternalPlugin {
274                metadata,
275                hooks,
276                lifecycle,
277                tools,
278            }),
279        })
280    }
281}
282
283fn resolve_path(root: &Path, path: &str) -> String {
284    if Path::new(path).is_absolute() {
285        path.to_string()
286    } else {
287        root.join(path).to_string_lossy().to_string()
288    }
289}