pleiades-agent-plugins 2.0.0

Pleiades plugin manifests, lifecycle management, and shell hooks
Documentation
use std::path::{Path, PathBuf};
use std::process::Command;

use serde::{Deserialize, Serialize};
use serde_json::Value;

use crate::manifest::{
    PluginError, PluginHooks, PluginLifecycle, PluginManifest, PluginToolPermission,
};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PluginKind {
    Builtin,
    Bundled,
    External,
}

impl PluginKind {
    pub fn marketplace(self) -> &'static str {
        match self {
            Self::Builtin => "builtin",
            Self::Bundled => "bundled",
            Self::External => "external",
        }
    }
}

impl std::fmt::Display for PluginKind {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Builtin => write!(f, "builtin"),
            Self::Bundled => write!(f, "bundled"),
            Self::External => write!(f, "external"),
        }
    }
}

#[derive(Debug, Clone)]
pub struct PluginMetadata {
    pub id: String,
    pub name: String,
    pub version: String,
    pub description: String,
    pub kind: PluginKind,
    pub source: String,
    pub default_enabled: bool,
    pub root: Option<PathBuf>,
}

#[derive(Debug, Clone)]
pub struct PluginTool {
    pub plugin_id: String,
    pub plugin_name: String,
    pub name: String,
    pub description: String,
    pub input_schema: Value,
    pub command: String,
    pub args: Vec<String>,
    pub required_permission: PluginToolPermission,
    pub root: Option<PathBuf>,
}

impl PluginTool {
    pub fn execute(&self, input: &Value) -> Result<String, PluginError> {
        let input_json = input.to_string();
        let mut cmd = Command::new(&self.command);
        cmd.args(&self.args)
            .stdin(std::process::Stdio::piped())
            .stdout(std::process::Stdio::piped())
            .stderr(std::process::Stdio::piped())
            .env("PLEIADES_PLUGIN_ID", &self.plugin_id)
            .env("PLEIADES_PLUGIN_NAME", &self.plugin_name)
            .env("PLEIADES_TOOL_NAME", &self.name)
            .env("PLEIADES_TOOL_INPUT", &input_json);

        if let Some(root) = &self.root {
            cmd.current_dir(root)
                .env("PLEIADES_PLUGIN_ROOT", root.display().to_string());
        }

        let mut child = cmd.spawn()?;
        if let Some(mut stdin) = child.stdin.take() {
            use std::io::Write;
            let _ = stdin.write_all(input_json.as_bytes());
        }

        let output = child.wait_with_output()?;
        if output.status.success() {
            Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
        } else {
            let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
            Err(PluginError::CommandFailed(format!(
                "plugin tool `{}` from `{}` failed: {}",
                self.name,
                self.plugin_id,
                if stderr.is_empty() {
                    format!("exit status {}", output.status)
                } else {
                    stderr
                },
            )))
        }
    }
}

pub trait Plugin: std::fmt::Debug {
    fn metadata(&self) -> &PluginMetadata;
    fn hooks(&self) -> &PluginHooks;
    fn lifecycle(&self) -> &PluginLifecycle;
    fn tools(&self) -> &[PluginTool];
}

#[derive(Debug, Clone)]
pub struct BuiltinPlugin {
    pub metadata: PluginMetadata,
    pub hooks: PluginHooks,
    pub lifecycle: PluginLifecycle,
    pub tools: Vec<PluginTool>,
}

impl Plugin for BuiltinPlugin {
    fn metadata(&self) -> &PluginMetadata {
        &self.metadata
    }
    fn hooks(&self) -> &PluginHooks {
        &self.hooks
    }
    fn lifecycle(&self) -> &PluginLifecycle {
        &self.lifecycle
    }
    fn tools(&self) -> &[PluginTool] {
        &self.tools
    }
}

#[derive(Debug, Clone)]
pub struct BundledPlugin {
    pub metadata: PluginMetadata,
    pub hooks: PluginHooks,
    pub lifecycle: PluginLifecycle,
    pub tools: Vec<PluginTool>,
}

impl Plugin for BundledPlugin {
    fn metadata(&self) -> &PluginMetadata {
        &self.metadata
    }
    fn hooks(&self) -> &PluginHooks {
        &self.hooks
    }
    fn lifecycle(&self) -> &PluginLifecycle {
        &self.lifecycle
    }
    fn tools(&self) -> &[PluginTool] {
        &self.tools
    }
}

#[derive(Debug, Clone)]
pub struct ExternalPlugin {
    pub metadata: PluginMetadata,
    pub hooks: PluginHooks,
    pub lifecycle: PluginLifecycle,
    pub tools: Vec<PluginTool>,
}

impl Plugin for ExternalPlugin {
    fn metadata(&self) -> &PluginMetadata {
        &self.metadata
    }
    fn hooks(&self) -> &PluginHooks {
        &self.hooks
    }
    fn lifecycle(&self) -> &PluginLifecycle {
        &self.lifecycle
    }
    fn tools(&self) -> &[PluginTool] {
        &self.tools
    }
}

#[derive(Debug, Clone)]
pub enum PluginDefinition {
    Builtin(BuiltinPlugin),
    Bundled(BundledPlugin),
    External(ExternalPlugin),
}

impl Plugin for PluginDefinition {
    fn metadata(&self) -> &PluginMetadata {
        match self {
            Self::Builtin(p) => p.metadata(),
            Self::Bundled(p) => p.metadata(),
            Self::External(p) => p.metadata(),
        }
    }

    fn hooks(&self) -> &PluginHooks {
        match self {
            Self::Builtin(p) => p.hooks(),
            Self::Bundled(p) => p.hooks(),
            Self::External(p) => p.hooks(),
        }
    }

    fn lifecycle(&self) -> &PluginLifecycle {
        match self {
            Self::Builtin(p) => p.lifecycle(),
            Self::Bundled(p) => p.lifecycle(),
            Self::External(p) => p.lifecycle(),
        }
    }

    fn tools(&self) -> &[PluginTool] {
        match self {
            Self::Builtin(p) => p.tools(),
            Self::Bundled(p) => p.tools(),
            Self::External(p) => p.tools(),
        }
    }
}

impl PluginDefinition {
    pub fn load_from_directory(
        root: &Path,
        kind: PluginKind,
        source: String,
        marketplace: &str,
    ) -> Result<Self, PluginError> {
        let manifest = PluginManifest::load_from_directory(root)?;
        let plugin_id = format!("{}-{}", manifest.name, marketplace);
        let metadata = PluginMetadata {
            id: plugin_id,
            name: manifest.name,
            version: manifest.version,
            description: manifest.description,
            kind,
            source,
            default_enabled: manifest.default_enabled,
            root: Some(root.to_path_buf()),
        };
        let hooks = manifest.hooks;
        let lifecycle = manifest.lifecycle;
        let tools = manifest
            .tools
            .into_iter()
            .map(|t| PluginTool {
                plugin_id: metadata.id.clone(),
                plugin_name: metadata.name.clone(),
                name: t.name,
                description: t.description,
                input_schema: t.input_schema,
                command: resolve_path(root, &t.command),
                args: t.args,
                required_permission: t.required_permission,
                root: Some(root.to_path_buf()),
            })
            .collect();

        Ok(match kind {
            PluginKind::Builtin => Self::Builtin(BuiltinPlugin {
                metadata,
                hooks,
                lifecycle,
                tools,
            }),
            PluginKind::Bundled => Self::Bundled(BundledPlugin {
                metadata,
                hooks,
                lifecycle,
                tools,
            }),
            PluginKind::External => Self::External(ExternalPlugin {
                metadata,
                hooks,
                lifecycle,
                tools,
            }),
        })
    }
}

fn resolve_path(root: &Path, path: &str) -> String {
    if Path::new(path).is_absolute() {
        path.to_string()
    } else {
        root.join(path).to_string_lossy().to_string()
    }
}