ai-agent 0.88.0

Idiomatic agent sdk inspired by the claude code source leak
Documentation
// Source: ~/claudecode/openclaudecode/src/utils/settings/permissionValidation.ts
//! Validates permission rule format and content.

use crate::services::mcp::mcp_info_from_string;
use crate::utils::settings::tool_validation_config::{
    get_custom_validation, is_bash_prefix_tool, is_file_pattern_tool,
};

/// Checks if a character at a given index is escaped (preceded by odd number of backslashes).
fn is_escaped(s: &str, index: usize) -> bool {
    let mut backslash_count = 0;
    let bytes = s.as_bytes();
    let mut j = index;
    while j > 0 && bytes[j - 1] == b'\\' {
        backslash_count += 1;
        j -= 1;
    }
    backslash_count % 2 != 0
}

/// Counts unescaped occurrences of a character in a string.
fn count_unescaped_char(s: &str, ch: char) -> usize {
    let mut count = 0;
    let mut i = 0;
    for c in s.chars() {
        if c == ch && !is_escaped(s, i) {
            count += 1;
        }
        i += c.len_utf8();
    }
    count
}

/// Checks if a string contains unescaped empty parentheses "()".
fn has_unescaped_empty_parens(s: &str) -> bool {
    let bytes = s.as_bytes();
    for i in 0..bytes.len().saturating_sub(1) {
        if bytes[i] == b'(' && bytes[i + 1] == b')' {
            if !is_escaped(s, i) {
                return true;
            }
        }
    }
    false
}

/// Capitalizes the first character of a string.
fn capitalize(s: &str) -> String {
    let mut chars = s.chars();
    match chars.next() {
        None => String::new(),
        Some(c) => c.to_uppercase().to_string() + chars.as_str(),
    }
}

/// Parses a permission rule value into tool name and optional content.
/// Extracts "ToolName" or "ToolName(content)" format.
fn parse_permission_rule_value(rule: &str) -> (String, Option<String>) {
    let paren_pos = rule.find('(');
    let tool_name = match paren_pos {
        Some(pos) => rule[..pos].trim().to_string(),
        None => rule.trim().to_string(),
    };

    let rule_content = if let Some(pos) = rule.find('(') {
        let rest = &rule[pos + 1..];
        if let Some(close) = rest.rfind(')') {
            let inner = &rest[..close];
            if inner.is_empty() {
                None
            } else {
                Some(inner.to_string())
            }
        } else {
            Some(rest.to_string())
        }
    } else {
        None
    };

    (tool_name, rule_content)
}

/// Permission rule validation result.
#[derive(Debug, Clone, Default)]
pub struct PermissionRuleResult {
    pub valid: bool,
    pub error: Option<String>,
    pub suggestion: Option<String>,
    pub examples: Option<Vec<String>>,
}

/// Validates permission rule format and content.
pub fn validate_permission_rule(rule: &str) -> PermissionRuleResult {
    // Empty rule check
    if rule.is_empty() || rule.trim().is_empty() {
        return PermissionRuleResult {
            valid: false,
            error: Some("Permission rule cannot be empty".into()),
            suggestion: None,
            examples: None,
        };
    }

    // Check parentheses matching
    let open_count = count_unescaped_char(rule, '(');
    let close_count = count_unescaped_char(rule, ')');
    if open_count != close_count {
        return PermissionRuleResult {
            valid: false,
            error: Some("Mismatched parentheses".into()),
            suggestion: Some(
                "Ensure all opening parentheses have matching closing parentheses".into(),
            ),
            examples: None,
        };
    }

    // Check for empty parentheses (escape-aware)
    if has_unescaped_empty_parens(rule) {
        let paren_pos = rule.find('(');
        let tool_name = paren_pos
            .map(|p| rule[..p].trim().to_string())
            .unwrap_or_default();
        if tool_name.is_empty() {
            return PermissionRuleResult {
                valid: false,
                error: Some("Empty parentheses with no tool name".into()),
                suggestion: Some("Specify a tool name before the parentheses".into()),
                examples: None,
            };
        }
        return PermissionRuleResult {
            valid: false,
            error: Some("Empty parentheses".into()),
            suggestion: Some(format!(
                "Either specify a pattern or use \"{}\" without parentheses",
                tool_name
            )),
            examples: Some(vec![tool_name.clone(), format!("{}(some-pattern)", tool_name)]),
        };
    }

    // Parse the rule
    let (parsed_tool_name, parsed_rule_content) = parse_permission_rule_value(rule);

    // MCP validation
    if let Some(mcp_info) = mcp_info_from_string(&parsed_tool_name) {
        // MCP rules cannot have any pattern/content (parentheses)
        if parsed_rule_content.is_some() || count_unescaped_char(rule, '(') > 0 {
            return PermissionRuleResult {
                valid: false,
                error: Some("MCP rules do not support patterns in parentheses".into()),
                suggestion: Some(format!(
                    "Use \"{}\" without parentheses, or use \"mcp__{}__*\" for all tools",
                    parsed_tool_name, mcp_info.server_name
                )),
                examples: Some({
                    let mut ex = vec![
                        format!("mcp__{}", mcp_info.server_name),
                        format!("mcp__{}__*", mcp_info.server_name),
                    ];
                    if let Some(ref tool) = mcp_info.tool_name {
                        if !tool.is_empty() && *tool != "*" {
                            ex.push(format!("mcp__{}__{}", mcp_info.server_name, tool));
                        }
                    }
                    ex
                }),
            };
        }
        return PermissionRuleResult {
            valid: true,
            error: None,
            suggestion: None,
            examples: None,
        };
    }

    // Tool name validation (for non-MCP tools)
    if parsed_tool_name.is_empty() {
        return PermissionRuleResult {
            valid: false,
            error: Some("Tool name cannot be empty".into()),
            suggestion: None,
            examples: None,
        };
    }

    // Check tool name starts with uppercase
    if let Some(first_char) = parsed_tool_name.chars().next() {
        if first_char != first_char.to_ascii_uppercase() {
            return PermissionRuleResult {
                valid: false,
                error: Some("Tool names must start with uppercase".into()),
                suggestion: Some(format!("\"{}\"", capitalize(&parsed_tool_name))),
                examples: None,
            };
        }
    }

    // Custom validation
    if let Some(custom_result) = get_custom_validation(rule) {
        if !custom_result.valid {
            return PermissionRuleResult {
                valid: false,
                error: custom_result.error,
                suggestion: custom_result.suggestion,
                examples: custom_result.examples,
            };
        }
    }

    // Bash-specific validation
    if is_bash_prefix_tool(&parsed_tool_name) {
        if let Some(ref content) = parsed_rule_content {
            // Check for :* not at end
            if content.contains(":*") && !content.ends_with(":*") {
                return PermissionRuleResult {
                    valid: false,
                    error: Some("The :* pattern must be at the end".into()),
                    suggestion: Some(
                        "Move :* to the end for prefix matching, or use * for wildcard matching"
                            .into(),
                    ),
                    examples: Some(vec![
                        "Bash(npm run:*) - prefix matching (legacy)".into(),
                        "Bash(npm run *) - wildcard matching".into(),
                    ]),
                };
            }
            // :* without prefix
            if content == ":*" {
                return PermissionRuleResult {
                    valid: false,
                    error: Some("Prefix cannot be empty before :*".into()),
                    suggestion: Some("Specify a command prefix before :*".into()),
                    examples: Some(vec!["Bash(npm:*)".into(), "Bash(git:*)".into()]),
                };
            }
        }
    }

    // File tool validation
    if is_file_pattern_tool(&parsed_tool_name) {
        if let Some(ref content) = parsed_rule_content {
            // Check for :* in file patterns
            if content.contains(":*") {
                return PermissionRuleResult {
                    valid: false,
                    error: Some("The \":*\" syntax is only for Bash prefix rules".into()),
                    suggestion: Some("Use glob patterns like \"*\" or \"**\" for file matching".into()),
                    examples: Some(vec![
                        format!("{}(*.ts) - matches .ts files", parsed_tool_name),
                        format!("{}(src/**) - matches all files in src", parsed_tool_name),
                        format!(
                            "{}(**/*.test.ts) - matches test files",
                            parsed_tool_name
                        ),
                    ]),
                };
            }
        }
    }

    PermissionRuleResult {
        valid: true,
        error: None,
        suggestion: None,
        examples: None,
    }
}

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

    #[test]
    fn test_empty_rule() {
        let result = validate_permission_rule("");
        assert!(!result.valid);
        let result = validate_permission_rule("   ");
        assert!(!result.valid);
    }

    #[test]
    fn test_mismatched_parens() {
        let result = validate_permission_rule("Read(*.ts");
        assert!(!result.valid);
        let result = validate_permission_rule("Read*.ts)");
        assert!(!result.valid);
    }

    #[test]
    fn test_empty_parens() {
        let result = validate_permission_rule("Read()");
        assert!(!result.valid);
        assert!(result.suggestion.is_some());
    }

    #[test]
    fn test_lowercase_tool_name() {
        let result = validate_permission_rule("read(*.ts)");
        assert!(!result.valid);
    }

    #[test]
    fn test_valid_simple_rule() {
        let result = validate_permission_rule("Read");
        assert!(result.valid);
    }

    #[test]
    fn test_valid_pattern_rule() {
        let result = validate_permission_rule("Read(*.ts)");
        assert!(result.valid);
    }

    #[test]
    fn test_valid_bash_rule() {
        let result = validate_permission_rule("Bash(npm run:*)");
        assert!(result.valid);
    }

    #[test]
    fn test_bash_colon_star_at_end() {
        let result = validate_permission_rule("Bash(npm:*)");
        assert!(result.valid);
    }

    #[test]
    fn test_bash_colon_star_middle() {
        let result = validate_permission_rule("Bash(npm:*) install)");
        // Should fail due to :* not at end (assuming parens match)
        // Actually this has mismatched parens... let me use a simpler case
        assert!(!result.valid);
    }

    #[test]
    fn test_file_tool_colon_star() {
        let result = validate_permission_rule("Read(:*)");
        assert!(!result.valid);
    }

    #[test]
    fn test_escaped_parens() {
        let result = validate_permission_rule("Bash(grep '\\(test\\')");
        // Escaped parens should be fine
        assert!(result.valid);
    }

    #[test]
    fn test_mcp_rule_no_parens() {
        // mcp__server format should be valid
        let result = validate_permission_rule("mcp__my-server");
        assert!(result.valid);
    }

    #[test]
    fn test_mcp_rule_wildcard() {
        let result = validate_permission_rule("mcp__my-server__*");
        assert!(result.valid);
    }

    #[test]
    fn test_is_escaped() {
        assert!(!is_escaped("abc(')", 3)); // '(' at index 3 not escaped
        assert!(is_escaped("abc\\(')", 4)); // '(' at index 4 escaped
        assert!(!is_escaped("abc\\\\(')", 5)); // '(' at index 5 not escaped (double backslash)
    }

    #[test]
    fn test_count_unescaped_char() {
        assert_eq!(count_unescaped_char("a(b)c(", '('), 2);
        assert_eq!(count_unescaped_char("a\\(b)c(", '('), 1);
    }
}