//! Skill runners for different execution modes use async_trait::async_trait; use serde_json::Value; use std::process::Command; use std::time::Instant; use tracing::warn; use zclaw_types::Result; use super::{Skill, SkillContext, SkillManifest, SkillResult}; /// Prompt-only skill execution pub struct PromptOnlySkill { manifest: SkillManifest, prompt_template: String, } impl PromptOnlySkill { pub fn new(manifest: SkillManifest, prompt_template: String) -> Self { Self { manifest, prompt_template } } fn format_prompt(&self, input: &Value) -> String { let mut prompt = self.prompt_template.clone(); if let Value::String(s) = input { prompt = prompt.replace("{{input}}", s); } else { prompt = prompt.replace("{{input}}", &serde_json::to_string_pretty(input).unwrap_or_default()); } prompt } } #[async_trait] impl Skill for PromptOnlySkill { fn manifest(&self) -> &SkillManifest { &self.manifest } async fn execute(&self, context: &SkillContext, input: Value) -> Result { let prompt = self.format_prompt(&input); // If an LLM completer is available, generate an AI response if let Some(completer) = &context.llm { match completer.complete(&prompt).await { Ok(response) => return Ok(SkillResult::success(Value::String(response))), Err(e) => { warn!("[PromptOnlySkill] LLM completion failed: {}, falling back to raw prompt", e); // Fall through to return raw prompt } } } // No LLM available — return formatted prompt (backward compatible) Ok(SkillResult::success(Value::String(prompt))) } } /// Python script skill execution pub struct PythonSkill { manifest: SkillManifest, script_path: std::path::PathBuf, } impl PythonSkill { pub fn new(manifest: SkillManifest, script_path: std::path::PathBuf) -> Self { Self { manifest, script_path } } } #[async_trait] impl Skill for PythonSkill { fn manifest(&self) -> &SkillManifest { &self.manifest } async fn execute(&self, context: &SkillContext, input: Value) -> Result { let start = Instant::now(); let input_json = serde_json::to_string(&input).unwrap_or_default(); let output = Command::new("python3") .arg(&self.script_path) .env("SKILL_INPUT", &input_json) .env("AGENT_ID", &context.agent_id) .env("SESSION_ID", &context.session_id) .output() .map_err(|e| zclaw_types::ZclawError::ToolError(format!("Failed to execute Python: {}", e)))?; let duration_ms = start.elapsed().as_millis() as u64; if output.status.success() { let stdout = String::from_utf8_lossy(&output.stdout); let result = serde_json::from_str(&stdout) .map(|v| SkillResult { success: true, output: v, error: None, duration_ms: Some(duration_ms), tokens_used: None, }) .unwrap_or_else(|_| SkillResult::success(Value::String(stdout.to_string()))); Ok(result) } else { let stderr = String::from_utf8_lossy(&output.stderr); Ok(SkillResult::error(stderr)) } } } /// Shell command skill execution pub struct ShellSkill { manifest: SkillManifest, command: String, } impl ShellSkill { pub fn new(manifest: SkillManifest, command: String) -> Self { Self { manifest, command } } } #[async_trait] impl Skill for ShellSkill { fn manifest(&self) -> &SkillManifest { &self.manifest } async fn execute(&self, context: &SkillContext, input: Value) -> Result { let start = Instant::now(); let mut cmd = self.command.clone(); if let Value::String(s) = input { // Shell-quote the input to prevent command injection let quoted = shlex::try_quote(&s) .map_err(|_| zclaw_types::ZclawError::ToolError( "Input contains null bytes and cannot be safely quoted".to_string() ))?; cmd = cmd.replace("{{input}}", "ed); } #[cfg(target_os = "windows")] let output = { Command::new("cmd") .args(["/C", &cmd]) .current_dir(context.working_dir.as_ref().unwrap_or(&std::path::PathBuf::from("."))) .output() .map_err(|e| zclaw_types::ZclawError::ToolError(format!("Failed to execute shell: {}", e)))? }; #[cfg(not(target_os = "windows"))] let output = { Command::new("sh") .args(["-c", &cmd]) .current_dir(context.working_dir.as_ref().unwrap_or(&std::path::PathBuf::from("."))) .output() .map_err(|e| zclaw_types::ZclawError::ToolError(format!("Failed to execute shell: {}", e)))? }; let _duration_ms = start.elapsed().as_millis() as u64; if output.status.success() { let stdout = String::from_utf8_lossy(&output.stdout); Ok(SkillResult::success(Value::String(stdout.to_string()))) } else { let stderr = String::from_utf8_lossy(&output.stderr); Ok(SkillResult::error(stderr)) } } }