pub mod executor;
pub mod hooks;
pub mod markdown_parser;
pub mod registry;
pub mod validator;
pub use executor::CommandExecutor;
pub use registry::CommandRegistry;
pub use validator::CommandValidator;
#[cfg(test)]
mod tests;
#[cfg(feature = "repl")]
pub mod modes;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum ExecutionMode {
#[default]
Local,
Firecracker,
Hybrid,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct CommandParameter {
pub name: String,
#[serde(rename = "type")]
pub param_type: String,
#[serde(default)]
pub required: bool,
pub description: Option<String>,
#[serde(default)]
pub default_value: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub validation: Option<ParameterValidation>,
#[serde(default, skip_serializing)]
pub allowed_values: Option<Vec<String>>,
}
impl CommandParameter {
pub fn get_validation(&self) -> Option<ParameterValidation> {
match (&self.validation, &self.allowed_values) {
(Some(v), Some(values)) => {
let mut merged = v.clone();
if merged.allowed_values.is_none() {
merged.allowed_values = Some(values.clone());
}
Some(merged)
}
(Some(v), None) => Some(v.clone()),
(None, Some(values)) => Some(ParameterValidation {
allowed_values: Some(values.clone()),
..Default::default()
}),
(None, None) => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct ParameterValidation {
pub min: Option<f64>,
pub max: Option<f64>,
pub allowed_values: Option<Vec<String>>,
pub pattern: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct CommandDefinition {
pub name: String,
pub description: String,
pub usage: Option<String>,
#[serde(default)]
pub parameters: Vec<CommandParameter>,
#[serde(default)]
pub execution_mode: ExecutionMode,
#[serde(default)]
pub permissions: Vec<String>,
#[serde(default)]
pub knowledge_graph_required: Vec<String>,
pub category: Option<String>,
#[serde(default = "default_version")]
pub version: String,
pub namespace: Option<String>,
#[serde(default)]
pub aliases: Vec<String>,
#[serde(default = "default_risk_level")]
pub risk_level: RiskLevel,
#[serde(default)]
pub timeout: Option<u64>,
#[serde(default)]
pub resource_limits: Option<ResourceLimits>,
}
fn default_version() -> String {
"1.0.0".to_string()
}
fn default_risk_level() -> RiskLevel {
RiskLevel::Low
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum RiskLevel {
#[default]
Low,
Medium,
High,
Critical,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct ResourceLimits {
pub max_memory_mb: Option<u64>,
pub max_cpu_time: Option<u64>,
pub max_disk_mb: Option<u64>,
#[serde(default)]
pub network_access: bool,
}
#[derive(Debug, Clone)]
pub struct ParsedCommand {
pub definition: CommandDefinition,
pub content: String,
pub source_path: std::path::PathBuf,
pub modified: std::time::SystemTime,
}
#[derive(Debug, thiserror::Error)]
pub enum CommandValidationError {
#[error("Command '{0}' not found")]
CommandNotFound(String),
#[error("Missing required parameter: {0}")]
MissingParameter(String),
#[error("Invalid parameter '{0}': {1}")]
InvalidParameter(String, String),
#[error("Insufficient permissions for command '{0}'")]
InsufficientPermissions(String),
#[error("Command '{0}' requires knowledge graph concepts: {1:?}")]
MissingKnowledgeGraphConcepts(String, Vec<String>),
#[error("Execution mode '{0}' not available for command '{1}'")]
ExecutionModeUnavailable(String, String),
#[error("Command '{0}' exceeds risk level for current role")]
RiskLevelExceeded(String),
#[error("Command validation failed: {0}")]
ValidationFailed(String),
}
#[derive(Debug, thiserror::Error)]
pub enum CommandExecutionError {
#[error("Command '{0}' execution failed: {1}")]
ExecutionFailed(String, String),
#[error("VM execution error: {0}")]
VmExecutionError(String),
#[error("Local execution error: {0}")]
LocalExecutionError(String),
#[error("Command timeout after {0} seconds")]
Timeout(u64),
#[error("Resource limit exceeded: {0}")]
ResourceLimitExceeded(String),
#[error("Command execution was cancelled")]
Cancelled,
#[error("Pre-command hook failed: {0}")]
PreHookFailed(String),
#[error("Post-command hook failed: {0}")]
PostHookFailed(String),
}
#[derive(Debug, Clone)]
pub struct CommandExecutionResult {
pub command: String,
pub execution_mode: ExecutionMode,
pub exit_code: i32,
pub stdout: String,
pub stderr: String,
pub duration_ms: u64,
pub resource_usage: Option<ResourceUsage>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResourceUsage {
pub memory_mb: f64,
pub cpu_time_seconds: f64,
pub disk_mb: f64,
pub network_bytes_sent: u64,
pub network_bytes_received: u64,
}
#[derive(Debug, thiserror::Error)]
pub enum CommandRegistryError {
#[error("Failed to parse command file '{0}': {1}")]
ParseError(String, String),
#[error("Invalid frontmatter in '{0}': {1}")]
InvalidFrontmatter(String, String),
#[error("Duplicate command definition: {0}")]
DuplicateCommand(String),
#[error("Command file not found: {0}")]
FileNotFound(String),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("YAML parsing error: {0}")]
YamlError(#[from] serde_yaml::Error),
#[error("Automata processing error: {0}")]
AutomataError(String),
#[error("Command not found: {0}")]
CommandNotFound(String),
#[error("Validation error: {0}")]
ValidationError(String),
}
#[derive(Debug, Clone)]
pub struct HookContext {
pub command: String,
pub parameters: std::collections::HashMap<String, String>,
pub user: String,
pub role: String,
pub execution_mode: ExecutionMode,
pub working_directory: std::path::PathBuf,
}
#[derive(Debug, Clone)]
pub struct HookResult {
pub success: bool,
pub message: String,
pub data: Option<serde_json::Value>,
pub should_continue: bool, }
#[async_trait::async_trait]
pub trait CommandHook {
fn name(&self) -> &str;
fn priority(&self) -> i32 {
0
}
async fn execute(&self, context: &HookContext) -> Result<HookResult, CommandExecutionError>;
}
pub struct HookManager {
pre_hooks: Vec<Box<dyn CommandHook + Send + Sync>>,
post_hooks: Vec<Box<dyn CommandHook + Send + Sync>>,
}
impl HookManager {
pub fn new() -> Self {
Self {
pre_hooks: Vec::new(),
post_hooks: Vec::new(),
}
}
pub fn add_pre_hook(&mut self, hook: Box<dyn CommandHook + Send + Sync>) {
self.pre_hooks.push(hook);
self.pre_hooks
.sort_by_key(|b| std::cmp::Reverse(b.priority()));
}
pub fn add_post_hook(&mut self, hook: Box<dyn CommandHook + Send + Sync>) {
self.post_hooks.push(hook);
self.post_hooks
.sort_by_key(|b| std::cmp::Reverse(b.priority()));
}
pub async fn execute_pre_hooks(
&self,
context: &HookContext,
) -> Result<(), CommandExecutionError> {
for hook in &self.pre_hooks {
match hook.execute(context).await {
Ok(result) => {
if !result.should_continue {
return Err(CommandExecutionError::PreHookFailed(format!(
"Pre-hook '{}' blocked execution: {}",
hook.name(),
result.message
)));
}
}
Err(e) => {
return Err(CommandExecutionError::PreHookFailed(format!(
"Pre-hook '{}' failed: {}",
hook.name(),
e
)));
}
}
}
Ok(())
}
pub async fn execute_post_hooks(
&self,
context: &HookContext,
_result: &CommandExecutionResult,
) -> Result<(), CommandExecutionError> {
for hook in &self.post_hooks {
match hook.execute(context).await {
Ok(_) => {
}
Err(e) => {
return Err(CommandExecutionError::PostHookFailed(format!(
"Post-hook '{}' failed: {}",
hook.name(),
e
)));
}
}
}
Ok(())
}
}
impl Default for HookManager {
fn default() -> Self {
Self::new()
}
}
impl CommandRegistryError {
pub fn parse_error(path: impl AsRef<std::path::Path>, error: impl Into<String>) -> Self {
CommandRegistryError::ParseError(path.as_ref().to_string_lossy().to_string(), error.into())
}
pub fn invalid_frontmatter(
path: impl AsRef<std::path::Path>,
error: impl Into<String>,
) -> Self {
CommandRegistryError::InvalidFrontmatter(
path.as_ref().to_string_lossy().to_string(),
error.into(),
)
}
}