pub mod external;
pub mod hooks;
pub mod registry;
use echo_core::tools::Tool;
use std::collections::HashMap;
use std::path::{Component, Path};
use std::sync::Arc;
use crate::sandbox::SandboxExecutor;
pub use echo_core::error::{ReactError, Result, ToolError};
pub fn is_path_safe(base: &Path, sub: &Path) -> bool {
for component in sub.components() {
if matches!(component, Component::ParentDir) {
return false;
}
}
if sub.is_absolute() {
return false;
}
if let (Ok(canonical_base), Ok(canonical_sub)) =
(base.canonicalize(), base.join(sub).canonicalize())
{
canonical_sub.starts_with(&canonical_base)
} else {
false
}
}
pub fn minimal_env(
skill_dir: &str,
session_id: &str,
extra: HashMap<String, String>,
) -> HashMap<String, String> {
let mut env = HashMap::new();
if let Ok(path) = std::env::var("PATH") {
env.insert("PATH".to_string(), path);
}
env.insert("SKILL_DIR".to_string(), skill_dir.to_string());
env.insert("SESSION_ID".to_string(), session_id.to_string());
for (k, v) in extra {
env.insert(k, v);
}
env
}
pub fn minimal_hook_env(skill_dir: &str, session_id: &str) -> HashMap<String, String> {
let mut env = HashMap::new();
if let Ok(path) = std::env::var("PATH") {
env.insert("PATH".to_string(), path);
}
env.insert("SKILL_DIR".to_string(), skill_dir.to_string());
env.insert("SESSION_ID".to_string(), session_id.to_string());
env
}
pub trait Skill: Send + Sync {
fn name(&self) -> &str;
fn description(&self) -> &str;
fn tools(&self) -> Vec<Box<dyn Tool>>;
fn tools_with_sandbox(&self, _sandbox: Option<Arc<dyn SandboxExecutor>>) -> Vec<Box<dyn Tool>> {
self.tools()
}
fn system_prompt_injection(&self) -> Option<String> {
None
}
fn shutdown(&self) {}
}
#[derive(Debug, Clone)]
pub struct SkillInfo {
pub name: String,
pub description: String,
pub tool_names: Vec<String>,
pub has_prompt_injection: bool,
}
pub use registry::SkillRegistry;
#[allow(deprecated)]
pub use registry::SkillManager;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_skill_info_structure() {
let info = SkillInfo {
name: "calculator".to_string(),
description: "Performs calculations".to_string(),
tool_names: vec!["add".to_string(), "subtract".to_string()],
has_prompt_injection: true,
};
assert_eq!(info.name, "calculator");
assert_eq!(info.description, "Performs calculations");
assert_eq!(info.tool_names.len(), 2);
assert!(info.has_prompt_injection);
}
#[test]
fn test_is_path_safe_basic() {
let base =
std::env::temp_dir().join(format!("echo-execution-skill-test-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
std::fs::create_dir_all(base.join("scripts")).unwrap();
std::fs::write(base.join("scripts/run.py"), "print('ok')").unwrap();
std::fs::write(base.join("README.md"), "docs").unwrap();
assert!(is_path_safe(&base, Path::new("scripts/run.py")));
assert!(is_path_safe(&base, Path::new("README.md")));
assert!(!is_path_safe(&base, Path::new("../secret.txt")));
assert!(!is_path_safe(&base, Path::new("foo/../../escape")));
assert!(!is_path_safe(&base, Path::new("/etc/passwd")));
assert!(!is_path_safe(&base, Path::new("missing.py")));
let _ = std::fs::remove_dir_all(&base);
}
#[test]
fn test_minimal_env_contains_expected_keys() {
let env = minimal_env("/tmp/skill", "sess-1", HashMap::new());
assert!(env.contains_key("PATH") || env.is_empty()); assert_eq!(env.get("SKILL_DIR").unwrap(), "/tmp/skill");
assert_eq!(env.get("SESSION_ID").unwrap(), "sess-1");
assert!(!env.contains_key("HOME"));
}
}