sekuire 0.1.0

The official SDK for the Sekuire Agent Identity Protocol
Documentation
//! Sekuire Agent SDK - Config-first agent implementation
//!
//! This module provides a secure, config-driven agent that loads everything
//! from sekuire.yml for immutability and auditability.

use crate::identity::AgentIdentity;
use crate::logger::{SekuireLogger, LoggerConfig};
use crate::llm::{LLMProvider, OpenAIProvider, AnthropicProvider, GeminiProvider};
use anyhow::{Result, Context};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};

/// Main agent configuration loaded from sekuire.yml
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct AgentConfig {
    pub name: String,
    pub version: String,
    pub description: Option<String>,
    pub llm: LLMConfig,
    #[serde(default)]
    pub llm_providers: Vec<LLMConfig>,
    #[serde(default = "default_system_prompt_path")]
    pub system_prompt_path: PathBuf,
    #[serde(default = "default_tools_directory")]
    pub tools_directory: PathBuf,
    #[serde(default = "default_private_key_path")]
    pub private_key_path: PathBuf,
    #[serde(default)]
    pub capabilities: Vec<String>,
    #[serde(default)]
    pub compliance: ComplianceConfig,
}

fn default_system_prompt_path() -> PathBuf {
    PathBuf::from("system.md")
}

fn default_tools_directory() -> PathBuf {
    PathBuf::from("tools")
}

fn default_private_key_path() -> PathBuf {
    PathBuf::from("sekuire.key")
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct LLMConfig {
    pub provider: String,
    pub model: String,
    pub api_key: Option<String>,
    #[serde(default = "default_temperature")]
    pub temperature: f32,
    #[serde(default = "default_max_tokens")]
    pub max_tokens: u32,
}

fn default_temperature() -> f32 {
    0.7
}

fn default_max_tokens() -> u32 {
    2000
}

#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct ComplianceConfig {
    #[serde(default)]
    pub logging: LoggerConfig,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ToolConfig {
    pub name: String,
    pub description: String,
    pub parameters: serde_json::Value,
    pub implementation: Option<PathBuf>,
}

/// Config-first Sekuire Agent
///
/// Security Features:
/// - Immutable configuration from YAML
/// - Cryptographic identity verification
/// - Automatic audit logging
/// - Tool execution sandboxing
pub struct SekuireAgent {
    config: AgentConfig,
    identity: AgentIdentity,
    logger: Option<SekuireLogger>,
    system_prompt: String,
    tools: Vec<ToolConfig>,
    llm: Box<dyn LLMProvider>,
}

impl SekuireAgent {
    /// Initialize agent from configuration file
    pub async fn new(config_path: impl AsRef<Path>) -> Result<Self> {
        let config = Self::load_config(config_path)?;
        
        // Initialize identity
        let identity = AgentIdentity::new(&config.private_key_path)?;
        
        // Initialize logger if enabled
        let logger = if config.compliance.logging.enabled {
            Some(SekuireLogger::new(
                identity.sekuire_id.clone(),
                config.compliance.logging.clone(),
            ))
        } else {
            None
        };
        
        // Load system prompt
        let system_prompt = fs::read_to_string(&config.system_prompt_path)
            .context("Failed to load system prompt")?;
        
        // Load tools
        let tools = Self::load_tools(&config.tools_directory)?;
        
        // Initialize LLM provider
        let llm = Self::init_llm(&config.llm)?;
        
        Ok(Self {
            config,
            identity,
            logger,
            system_prompt,
            tools,
            llm,
        })
    }
    
    fn load_config(path: impl AsRef<Path>) -> Result<AgentConfig> {
        let content = fs::read_to_string(path)?;
        let config: AgentConfig = serde_yaml::from_str(&content)?;
        Ok(config)
    }
    
    fn load_tools(tools_dir: &Path) -> Result<Vec<ToolConfig>> {
        if !tools_dir.exists() {
            return Ok(Vec::new());
        }
        
        let mut tools = Vec::new();
        for entry in fs::read_dir(tools_dir)? {
            let entry = entry?;
            let path = entry.path();
            if path.extension().and_then(|s| s.to_str()) == Some("yml") {
                let content = fs::read_to_string(&path)?;
                let tool: ToolConfig = serde_yaml::from_str(&content)?;
                tools.push(tool);
            }
        }
        
        Ok(tools)
    }
    
    fn init_llm(config: &LLMConfig) -> Result<Box<dyn LLMProvider>> {
        let provider: Box<dyn LLMProvider> = match config.provider.to_lowercase().as_str() {
            "openai" => Box::new(OpenAIProvider::new(config.clone())?),
            "anthropic" => Box::new(AnthropicProvider::new(config.clone())?),
            "gemini" => Box::new(GeminiProvider::new(config.clone())?),
            _ => anyhow::bail!("Unknown LLM provider: {}", config.provider),
        };
        
        Ok(provider)
    }
    
    /// Execute agent with user message
    pub async fn run(
        &mut self,
        user_message: &str,
        context: Option<HashMap<String, serde_json::Value>>,
    ) -> Result<AgentResponse> {
        // Log request if enabled
        if let Some(logger) = &mut self.logger {
            logger.log_tool_call(
                "agent_run",
                serde_json::json!({
                    "message": user_message,
                    "context": context,
                }),
                None,
                true,
            ).await?;
        }
        
        // Generate response using LLM
        let response = self.llm.generate(
            &self.system_prompt,
            user_message,
            &self.tools,
            context.as_ref(),
        ).await?;
        
        // Log response if enabled
        if let Some(logger) = &mut self.logger {
            logger.log_tool_call(
                "agent_run",
                serde_json::json!({"message": user_message}),
                Some(serde_json::json!({"response": &response})),
                true,
            ).await?;
        }
        
        Ok(AgentResponse {
            response,
            agent_id: self.identity.sekuire_id.clone(),
            metadata: ResponseMetadata {
                provider: self.llm.provider_name().to_string(),
                model: self.config.llm.model.clone(),
                tools_available: self.tools.len(),
            },
        })
    }
    
    /// Get agent manifest for registry publishing
    pub fn get_manifest(&self) -> AgentManifest {
        AgentManifest {
            name: self.config.name.clone(),
            version: self.config.version.clone(),
            sekuire_id: self.identity.sekuire_id.clone(),
            model: Some(self.config.llm.model.clone()),
            tools: self.tools.iter().map(|t| t.name.clone()).collect(),
            capabilities: self.config.capabilities.clone(),
            compliance: serde_json::to_value(&self.config.compliance).ok(),
        }
    }
    
    /// Get agent identity
    pub fn identity(&self) -> &AgentIdentity {
        &self.identity
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentResponse {
    pub response: String,
    pub agent_id: String,
    pub metadata: ResponseMetadata,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResponseMetadata {
    pub provider: String,
    pub model: String,
    pub tools_available: usize,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentManifest {
    pub name: String,
    pub version: String,
    pub sekuire_id: String,
    pub model: Option<String>,
    pub tools: Vec<String>,
    pub capabilities: Vec<String>,
    pub compliance: Option<serde_json::Value>,
}

/// Multi-agent orchestrator for running with different LLM providers
pub struct MultiAgentOrchestrator {
    base_config: AgentConfig,
    agents: HashMap<String, SekuireAgent>,
}

impl MultiAgentOrchestrator {
    /// Initialize orchestrator with base config
    pub async fn new(config_path: impl AsRef<Path>) -> Result<Self> {
        let base_config = SekuireAgent::load_config(&config_path)?;
        let mut agents = HashMap::new();
        
        // Initialize agent for each LLM provider
        for provider_config in &base_config.llm_providers {
            let mut agent_config = base_config.clone();
            agent_config.llm = provider_config.clone();
            
            // TODO: Create agent with this config
            // For now, just store the config
        }
        
        Ok(Self {
            base_config,
            agents,
        })
    }
    
    /// Run agent with specific LLM provider
    pub async fn run_with_provider(
        &mut self,
        provider: &str,
        user_message: &str,
        context: Option<HashMap<String, serde_json::Value>>,
    ) -> Result<AgentResponse> {
        let agent = self.agents.get_mut(provider)
            .context(format!("Provider not configured: {}", provider))?;
        
        agent.run(user_message, context).await
    }
    
    /// Run with all providers and compare responses
    pub async fn run_all(
        &mut self,
        user_message: &str,
        context: Option<HashMap<String, serde_json::Value>>,
    ) -> Result<HashMap<String, Result<AgentResponse>>> {
        let mut results = HashMap::new();
        
        for (provider, agent) in &mut self.agents {
            let result = agent.run(user_message, context.clone()).await;
            results.insert(provider.clone(), result);
        }
        
        Ok(results)
    }
}