car-reason 0.6.0

Code reasoning engine for Common Agent Runtime — adaptive, graph-driven, learning
Documentation
//! Verification of code suggestions using real tools.
//!
//! Instead of asking an LLM to "review" code, we use actual verification:
//! 1. Syntax check — try to parse the code (language-specific)
//! 2. Compile check — run rustc/python/node with --check or equivalent
//! 3. Formal verification — use car-verify if the suggestion maps to an ActionProposal
//!
//! Falls back to LLM review only when no real tools are applicable.

use std::process::Command;

use crate::types::{CodeSuggestion, VerificationStatus};

/// Verify a code suggestion using available tools.
pub fn verify_suggestion(suggestion: &mut CodeSuggestion) {
    let code = &suggestion.suggested;

    // Detect language from code content or file path
    let lang = detect_language(code, suggestion.file_path.as_deref());

    let status = match lang {
        Language::Rust => verify_rust(code),
        Language::Python => verify_python(code),
        Language::JavaScript | Language::TypeScript => verify_js(code),
        Language::Unknown => VerificationStatus::NotVerified,
    };

    suggestion.verification = status;
}

#[derive(Debug, Clone, Copy)]
enum Language {
    Rust,
    Python,
    JavaScript,
    TypeScript,
    Unknown,
}

fn detect_language(code: &str, file_path: Option<&str>) -> Language {
    // Check file extension first
    if let Some(path) = file_path {
        if path.ends_with(".rs") { return Language::Rust; }
        if path.ends_with(".py") { return Language::Python; }
        if path.ends_with(".js") { return Language::JavaScript; }
        if path.ends_with(".ts") || path.ends_with(".tsx") { return Language::TypeScript; }
    }

    // Heuristic from code content
    if code.contains("fn ") && (code.contains("->") || code.contains("let ") || code.contains("pub ")) {
        return Language::Rust;
    }
    if code.contains("def ") || code.contains("import ") && code.contains(":") {
        return Language::Python;
    }
    if code.contains("function ") || code.contains("const ") || code.contains("=>") {
        return Language::JavaScript;
    }

    Language::Unknown
}

/// Verify Rust code by writing to a temp file and compiling.
fn verify_rust(code: &str) -> VerificationStatus {
    let tmp_dir = std::env::temp_dir().join("car-verify");
    let _ = std::fs::create_dir_all(&tmp_dir);
    let src_file = tmp_dir.join("check.rs");
    let out_file = tmp_dir.join("check.rlib");

    if std::fs::write(&src_file, code).is_err() {
        return VerificationStatus::NotVerified;
    }

    let result = Command::new("rustc")
        .args([
            "--edition", "2021",
            "--crate-type", "lib",
            "-o", &out_file.to_string_lossy(),
            &src_file.to_string_lossy(),
        ])
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::piped())
        .output();

    // Clean up
    let _ = std::fs::remove_file(&src_file);
    let _ = std::fs::remove_file(&out_file);

    match result {
        Ok(output) if output.status.success() => VerificationStatus::Passed,
        Ok(_) => VerificationStatus::PartiallyVerified, // Parsed but didn't compile (might need context)
        Err(_) => VerificationStatus::NotVerified,
    }
}

/// Verify Python code by attempting to compile it.
///
/// Writes the suggestion to a per-process temp file and reads it back
/// from `sys.argv[1]`. Earlier versions interpolated `code` into a
/// `python3 -c "..."` command string with only `'''` escaping, which
/// was defeatable via backslash escapes (closes #124). Code now never
/// touches the command line — `code` ends up in a file, the file path
/// is the only thing the shell sees, and Python reads the contents
/// directly.
fn verify_python(code: &str) -> VerificationStatus {
    let tmp_dir = std::env::temp_dir().join("car-verify");
    let _ = std::fs::create_dir_all(&tmp_dir);
    use std::sync::atomic::{AtomicU64, Ordering};
    static COUNTER: AtomicU64 = AtomicU64::new(0);
    let id = COUNTER.fetch_add(1, Ordering::Relaxed);
    let tmp_file = tmp_dir.join(format!("check_{}_{}.py", std::process::id(), id));

    if std::fs::write(&tmp_file, code).is_err() {
        return VerificationStatus::NotVerified;
    }

    let result = Command::new("python3")
        .args([
            "-c",
            "import ast, sys; ast.parse(open(sys.argv[1]).read())",
        ])
        .arg(&tmp_file)
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::piped())
        .output();

    let _ = std::fs::remove_file(&tmp_file);

    match result {
        Ok(output) if output.status.success() => VerificationStatus::Passed,
        Ok(_) => VerificationStatus::Failed,
        Err(_) => VerificationStatus::NotVerified,
    }
}

/// Verify JavaScript/TypeScript by attempting to parse with node.
fn verify_js(code: &str) -> VerificationStatus {
    // Use node's --check flag (syntax only, no execution)
    let result = Command::new("node")
        .args(["--check", "-e", code])
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::piped())
        .output();

    match result {
        Ok(output) if output.status.success() => VerificationStatus::Passed,
        Ok(_) => VerificationStatus::Failed,
        Err(_) => VerificationStatus::NotVerified,
    }
}

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

    #[test]
    fn detect_rust() {
        assert!(matches!(
            detect_language("fn main() { let x = 5; }", None),
            Language::Rust
        ));
        assert!(matches!(
            detect_language("code here", Some("src/main.rs")),
            Language::Rust
        ));
    }

    #[test]
    fn detect_python() {
        assert!(matches!(
            detect_language("def hello():\n    print('hi')", None),
            Language::Python
        ));
    }

    #[test]
    fn verify_valid_rust() {
        let status = verify_rust("pub fn add(a: i32, b: i32) -> i32 { a + b }");
        // May be Passed or NotVerified (if rustc not available in test env)
        assert!(matches!(status, VerificationStatus::Passed | VerificationStatus::NotVerified));
    }

    #[test]
    fn verify_valid_python() {
        let status = verify_python("def add(a, b):\n    return a + b");
        assert!(matches!(status, VerificationStatus::Passed | VerificationStatus::NotVerified));
    }

    /// Regression test for #124. A payload designed to break out of the
    /// old `format!("import ast; ast.parse('''{}''')", ...)` interpolation
    /// must not execute `os.system`. We pick a sentinel file path the
    /// payload would touch on success; if it appears on disk after the
    /// call returned, the injection landed.
    #[test]
    fn verify_python_injection_does_not_execute() {
        let sentinel = std::env::temp_dir().join("car-verify-injection-pwned");
        // Best-effort cleanup if a previous run left it around.
        let _ = std::fs::remove_file(&sentinel);

        let payload = format!(
            "''')\nimport os; os.system('touch {}')#",
            sentinel.display()
        );
        let _ = verify_python(&payload);
        assert!(
            !sentinel.exists(),
            "verify_python executed an injected `os.system(...)`; the sandbox is broken"
        );
    }
}