rae 0.1.4

Renderer-neutral widget, layout, state, and GLSL shader primitives for agentic Rust desktop tools.
Documentation
use std::path::{Component, Path, PathBuf};

#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
pub enum AgenticBackendKind {
    AevSidecar,
    AevStdio,
}

#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
pub enum ThinkingSetting {
    None,
    Low,
    Medium,
    High,
    XHigh,
}

#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct AgenticSettings {
    pub enabled: bool,
    pub backend: AgenticBackendKind,
    pub aev_bin: PathBuf,
    pub workspace: Option<PathBuf>,
    pub allow_file_writes: bool,
    pub require_validation_after_writes: bool,
    pub open_in_external_terminal: bool,
    pub default_thinking: ThinkingSetting,
}

impl Default for AgenticSettings {
    fn default() -> Self {
        Self {
            enabled: false,
            backend: AgenticBackendKind::AevSidecar,
            aev_bin: PathBuf::from("aev"),
            workspace: None,
            allow_file_writes: false,
            require_validation_after_writes: true,
            open_in_external_terminal: true,
            default_thinking: ThinkingSetting::None,
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum WritePermission {
    Disabled,
    AllowedInsideWorkspace,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SafetyViolation {
    WritesDisabled,
    EmptyTarget,
    UrlTarget,
    AbsoluteTarget,
    ParentTraversal,
    RuntimeStateTarget,
    DirectoryTarget,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SidecarCommand {
    pub program: PathBuf,
    pub args: Vec<String>,
}

impl SidecarCommand {
    pub fn aev_scan(workspace: &Path, aev_bin: impl Into<PathBuf>) -> Self {
        Self {
            program: aev_bin.into(),
            args: vec![
                "--workspace".to_string(),
                workspace.display().to_string(),
                "--thinking".to_string(),
                "none".to_string(),
                "scan".to_string(),
            ],
        }
    }

    pub fn aev_export(workspace: &Path, format: &str, aev_bin: impl Into<PathBuf>) -> Self {
        Self {
            program: aev_bin.into(),
            args: vec![
                "--workspace".to_string(),
                workspace.display().to_string(),
                "export".to_string(),
                "--format".to_string(),
                format.to_string(),
            ],
        }
    }

    pub fn preview(&self) -> String {
        let mut parts = vec![shell_word(&self.program.display().to_string())];
        parts.extend(self.args.iter().map(|arg| shell_word(arg)));
        parts.join(" ")
    }
}

pub fn validate_write_target(
    workspace: &Path,
    target: &str,
    permission: WritePermission,
) -> Result<PathBuf, SafetyViolation> {
    if permission == WritePermission::Disabled {
        return Err(SafetyViolation::WritesDisabled);
    }
    let trimmed = target.trim();
    if trimmed.is_empty() {
        return Err(SafetyViolation::EmptyTarget);
    }
    if trimmed.contains("://") {
        return Err(SafetyViolation::UrlTarget);
    }

    let target_path = Path::new(trimmed);
    if target_path.is_absolute() {
        return Err(SafetyViolation::AbsoluteTarget);
    }

    for component in target_path.components() {
        match component {
            Component::ParentDir => return Err(SafetyViolation::ParentTraversal),
            Component::Normal(value) if value == ".aev" || value == ".xrae" => {
                return Err(SafetyViolation::RuntimeStateTarget)
            }
            _ => {}
        }
    }

    if target_path.file_name().is_none() {
        return Err(SafetyViolation::DirectoryTarget);
    }

    Ok(workspace.join(target_path))
}

fn shell_word(value: &str) -> String {
    if value
        .chars()
        .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '.' | '/' | ':'))
    {
        value.to_string()
    } else {
        format!("'{}'", value.replace('\'', "'\\''"))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn settings_default_to_safe_disabled() {
        let settings = AgenticSettings::default();

        assert!(!settings.enabled);
        assert!(!settings.allow_file_writes);
        assert!(settings.require_validation_after_writes);
    }

    #[test]
    fn write_guard_rejects_unsafe_targets() {
        let workspace = Path::new("/tmp/work");

        assert_eq!(
            validate_write_target(
                workspace,
                "../secret",
                WritePermission::AllowedInsideWorkspace
            ),
            Err(SafetyViolation::ParentTraversal)
        );
        assert_eq!(
            validate_write_target(
                workspace,
                ".aev/session.json",
                WritePermission::AllowedInsideWorkspace
            ),
            Err(SafetyViolation::RuntimeStateTarget)
        );
        assert_eq!(
            validate_write_target(
                workspace,
                "https://example.com/file",
                WritePermission::AllowedInsideWorkspace
            ),
            Err(SafetyViolation::UrlTarget)
        );
    }

    #[test]
    fn sidecar_preview_quotes_spaces() {
        let command = SidecarCommand::aev_scan(Path::new("/tmp/my work"), "aev");

        assert!(command.preview().contains("'/tmp/my work'"));
    }
}