vetto 0.2.8

Daemon-less sandbox + security layer for AI coding agents (Landlock/Seatbelt, TUI statusline, post-session audit reports)
Documentation
//! Dynamic toolchain binary shim registry (Step 18).
//!
//! Maintains default intercepted developer binaries and generates fast
//! shims for host binaries across Rust, Node.js, Python, Go, Docker, etc.

use anyhow::{Context, Result};
use std::fs;
use std::path::{Path, PathBuf};

/// Default developer toolchain binaries intercepted by Vetto shims.
pub const DEFAULT_BINARIES: &[&str] = &[
    "bash", "zsh", "sh", "git", "node", "nodejs", "npm", "npx", "pnpm", "yarn", "bun", "deno",
    "python", "python3", "pip", "pip3", "cargo", "rustc", "go", "docker", "podman",
];

/// Information about an active shim in the shims directory.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ShimInfo {
    /// Name of the intercepted toolchain binary (e.g. "node", "cargo").
    pub name: String,
    /// Path to the shim file on disk.
    pub path: PathBuf,
    /// Whether the shim is a symbolic link.
    pub is_symlink: bool,
    /// Whether the shim file is valid and executable.
    pub is_executable: bool,
    /// Target binary or description if inspectable.
    pub target: Option<String>,
}

/// Registry responsible for detecting project ecosystems and creating/managing binary shims.
pub struct ShimRegistry;

impl ShimRegistry {
    /// Returns the default list of intercepted binaries.
    pub fn default_binaries() -> Vec<&'static str> {
        DEFAULT_BINARIES.to_vec()
    }

    /// Dynamically detects intercepted binaries needed for a specific project root.
    pub fn detect_for_project(project_root: &Path) -> Vec<String> {
        let mut binaries = Vec::new();

        // Always include core shell wrappers
        binaries.push("sh".to_string());
        binaries.push("bash".to_string());

        // Git
        if project_root.join(".git").exists() {
            binaries.push("git".to_string());
        }

        // Rust
        if project_root.join("Cargo.toml").exists() {
            binaries.push("cargo".to_string());
            binaries.push("rustc".to_string());
        }

        // Node.js / TypeScript / JavaScript
        let is_node = project_root.join("package.json").exists()
            || project_root.join("pnpm-lock.yaml").exists()
            || project_root.join("yarn.lock").exists()
            || project_root.join("bun.lockb").exists();
        if is_node {
            binaries.push("node".to_string());
            binaries.push("nodejs".to_string());
            binaries.push("npm".to_string());
            binaries.push("npx".to_string());
            if project_root.join("pnpm-lock.yaml").exists() {
                binaries.push("pnpm".to_string());
            }
            if project_root.join("yarn.lock").exists() {
                binaries.push("yarn".to_string());
            }
            if project_root.join("bun.lockb").exists() {
                binaries.push("bun".to_string());
            }
            if project_root.join("deno.json").exists() || project_root.join("deno.jsonc").exists() {
                binaries.push("deno".to_string());
            }
        }

        // Python
        let is_python = project_root.join("pyproject.toml").exists()
            || project_root.join("requirements.txt").exists()
            || project_root.join("Pipfile").exists()
            || project_root.join("poetry.lock").exists()
            || project_root.join("setup.py").exists();
        if is_python {
            binaries.push("python".to_string());
            binaries.push("python3".to_string());
            binaries.push("pip".to_string());
            binaries.push("pip3".to_string());
        }

        // Go
        if project_root.join("go.mod").exists() {
            binaries.push("go".to_string());
        }

        // Docker / Containers
        let is_docker = project_root.join("Dockerfile").exists()
            || project_root.join("docker-compose.yml").exists()
            || project_root.join("docker-compose.yaml").exists()
            || project_root.join("compose.yaml").exists()
            || project_root.join("compose.yml").exists();
        if is_docker {
            binaries.push("docker".to_string());
            binaries.push("podman".to_string());
        }

        binaries.sort();
        binaries.dedup();
        binaries
    }

    /// Generates the content for a POSIX shell shim script on Unix systems.
    pub fn generate_unix_shim_script(binary_name: &str, vetto_bin_hint: Option<&Path>) -> String {
        let vetto_bin = vetto_bin_hint.and_then(|p| p.to_str()).unwrap_or("vetto");

        format!(
            r#"#!/bin/sh
# Vetto transparent binary shim for: {binary_name}
# Automatically generated by `vetto hook install`. Do not edit.

if [ -n "$VETTO_SANDBOXED" ] || [ -n "$VETTO_SHIM_ACTIVE" ]; then
    # Recursion barrier active — resolve real host binary outside vetto shims
    REAL_BIN=""
    _OLD_IFS="$IFS"
    IFS=:
    for _dir in $PATH; do
        case "$_dir" in
            *".vetto/shims"*|*"/vetto/shims"*) continue ;;
            "") continue ;;
            *)
                if [ -f "$_dir/{binary_name}" ] && [ -x "$_dir/{binary_name}" ]; then
                    REAL_BIN="$_dir/{binary_name}"
                    break
                fi
                ;;
        esac
    done
    IFS="$_OLD_IFS"
    unset _OLD_IFS
    if [ -n "$REAL_BIN" ]; then
        exec "$REAL_BIN" "$@"
    fi
fi

# Not sandboxed yet: invoke Vetto native shim dispatcher
VETTO_EXE="{vetto_bin}"
if command -v "$VETTO_EXE" >/dev/null 2>&1; then
    exec "$VETTO_EXE" shim "{binary_name}" -- "$@"
elif command -v vetto >/dev/null 2>&1; then
    exec vetto shim "{binary_name}" -- "$@"
else
    echo "vetto: error: could not locate vetto binary to execute shim for {binary_name}" >&2
    exit 127
fi
"#
        )
    }

    /// Generates the content for a Windows CMD shim batch file (.cmd).
    pub fn generate_windows_cmd_shim(binary_name: &str, vetto_bin_hint: Option<&Path>) -> String {
        let vetto_bin = vetto_bin_hint.and_then(|p| p.to_str()).unwrap_or("vetto");

        format!(
            r#"@echo off
rem Vetto transparent binary shim for: {binary_name}
rem Automatically generated by `vetto hook install`. Do not edit.

if "%VETTO_SANDBOXED%"=="1" goto passthrough
if "%VETTO_SHIM_ACTIVE%"=="1" goto passthrough

"{vetto_bin}" shim "{binary_name}" -- %*
exit /b %ERRORLEVEL%

:passthrough
rem Fall back to real binary
setlocal enabledelayedexpansion
for %%i in ({binary_name}.exe {binary_name}.cmd {binary_name}.bat {binary_name}) do (
    set "REAL_BIN=%%~$PATH:i"
    if defined REAL_BIN (
        echo !REAL_BIN! | findstr /i /c:".vetto\shims" >nul
        if errorlevel 1 (
            endlocal
            "%%REAL_BIN%%" %*
            exit /b !ERRORLEVEL!
        )
    )
)
endlocal
"{binary_name}" %*
"#
        )
    }

    /// Creates or updates shims in the specified target directory.
    pub fn create_shims(
        target_dir: &Path,
        binaries: &[String],
        vetto_bin_hint: Option<&Path>,
    ) -> Result<Vec<PathBuf>> {
        fs::create_dir_all(target_dir)
            .with_context(|| format!("failed to create shims dir: {}", target_dir.display()))?;

        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            let mut perms = fs::metadata(target_dir)?.permissions();
            perms.set_mode(0o755);
            let _ = fs::set_permissions(target_dir, perms);
        }

        let mut created = Vec::new();

        for name in binaries {
            let shim_path = target_dir.join(name);
            let content = Self::generate_unix_shim_script(name, vetto_bin_hint);
            fs::write(&shim_path, content)
                .with_context(|| format!("failed to write shim: {}", shim_path.display()))?;

            #[cfg(unix)]
            {
                use std::os::unix::fs::PermissionsExt;
                let mut perms = fs::metadata(&shim_path)?.permissions();
                perms.set_mode(0o755);
                fs::set_permissions(&shim_path, perms).with_context(|| {
                    format!("failed to make shim executable: {}", shim_path.display())
                })?;
            }

            // On Windows, also write the .cmd wrapper
            #[cfg(windows)]
            {
                let cmd_path = target_dir.join(format!("{name}.cmd"));
                let cmd_content = Self::generate_windows_cmd_shim(name, vetto_bin_hint);
                fs::write(&cmd_path, cmd_content).with_context(|| {
                    format!("failed to write windows shim: {}", cmd_path.display())
                })?;
                created.push(cmd_path);
            }

            created.push(shim_path);
        }

        Ok(created)
    }

    /// Removes shims from the target directory. If `binaries` is empty/None, removes all known shims.
    pub fn remove_shims(target_dir: &Path, binaries: Option<&[String]>) -> Result<Vec<PathBuf>> {
        if !target_dir.exists() {
            return Ok(Vec::new());
        }

        let mut removed = Vec::new();

        if let Some(list) = binaries {
            for name in list {
                let shim_path = target_dir.join(name);
                if shim_path.exists() {
                    let _ = fs::remove_file(&shim_path);
                    removed.push(shim_path);
                }
                let cmd_path = target_dir.join(format!("{name}.cmd"));
                if cmd_path.exists() {
                    let _ = fs::remove_file(&cmd_path);
                    removed.push(cmd_path);
                }
            }
        } else {
            // Remove all files in the shims directory
            for entry in fs::read_dir(target_dir)? {
                let entry = entry?;
                let path = entry.path();
                if path.is_file() || path.is_symlink() {
                    let _ = fs::remove_file(&path);
                    removed.push(path);
                }
            }
        }

        Ok(removed)
    }

    /// Lists all active shims in the target directory.
    pub fn list_active_shims(target_dir: &Path) -> Result<Vec<ShimInfo>> {
        if !target_dir.exists() {
            return Ok(Vec::new());
        }

        let mut shims = Vec::new();
        for entry in fs::read_dir(target_dir)? {
            let entry = entry?;
            let path = entry.path();
            let name = path
                .file_name()
                .map(|n| n.to_string_lossy().to_string())
                .unwrap_or_default();

            if name.starts_with('.') {
                continue;
            }

            let symlink_meta = fs::symlink_metadata(&path)?;
            let is_symlink = symlink_meta.file_type().is_symlink();

            let is_executable = {
                #[cfg(unix)]
                {
                    use std::os::unix::fs::PermissionsExt;
                    symlink_meta.permissions().mode() & 0o111 != 0
                }
                #[cfg(windows)]
                {
                    true
                }
            };

            shims.push(ShimInfo {
                name,
                path,
                is_symlink,
                is_executable,
                target: None,
            });
        }

        shims.sort_by(|a, b| a.name.cmp(&b.name));
        Ok(shims)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use std::time::{SystemTime, UNIX_EPOCH};

    fn temp_test_dir(name: &str) -> PathBuf {
        let dir = std::env::temp_dir().join(format!(
            "vetto-shim-reg-{name}-{}",
            SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap()
                .as_nanos()
        ));
        let _ = fs::remove_dir_all(&dir);
        fs::create_dir_all(&dir).unwrap();
        dir
    }

    #[test]
    fn default_binaries_contain_essential_tools() {
        let list = ShimRegistry::default_binaries();
        assert!(list.contains(&"cargo"));
        assert!(list.contains(&"node"));
        assert!(list.contains(&"git"));
        assert!(list.contains(&"python"));
        assert!(list.contains(&"bash"));
    }

    #[test]
    fn detects_rust_and_node_manifests() {
        let dir = temp_test_dir("detect-stack");
        fs::write(dir.join("Cargo.toml"), "[package]\nname = \"foo\"").unwrap();
        fs::write(dir.join("package.json"), "{}").unwrap();
        fs::write(dir.join("pnpm-lock.yaml"), "lockfileVersion: '9.0'").unwrap();

        let detected = ShimRegistry::detect_for_project(&dir);
        assert!(detected.contains(&"cargo".to_string()));
        assert!(detected.contains(&"rustc".to_string()));
        assert!(detected.contains(&"node".to_string()));
        assert!(detected.contains(&"pnpm".to_string()));
        assert!(detected.contains(&"bash".to_string()));

        let _ = fs::remove_dir_all(&dir);
    }

    #[test]
    fn creates_and_removes_shims_correctly() {
        let dir = temp_test_dir("create-remove");
        let shims_dir = dir.join("shims");

        let bins = vec!["node".to_string(), "git".to_string()];
        let created = ShimRegistry::create_shims(&shims_dir, &bins, None).unwrap();
        assert!(created.len() >= 2);
        assert!(shims_dir.join("node").exists());
        assert!(shims_dir.join("git").exists());

        let active = ShimRegistry::list_active_shims(&shims_dir).unwrap();
        assert!(active.iter().any(|s| s.name.starts_with("git")));
        assert!(active.iter().any(|s| s.name.starts_with("node")));

        let removed = ShimRegistry::remove_shims(&shims_dir, Some(&bins)).unwrap();
        assert!(removed.len() >= 2);
        assert!(!shims_dir.join("node").exists());

        let _ = fs::remove_dir_all(&dir);
    }
}