magi-code 0.102.2

Repository-aware CLI coding agent for terminal work
Documentation
use crate::support::path::lexical_normalize;
use std::fs;
use std::path::{Path, PathBuf};

#[derive(Debug, Clone, Copy)]
pub(in crate::tools) struct ExistingPathPolicy {
    allow_absolute_paths: bool,
    setting_name: &'static str,
}

impl ExistingPathPolicy {
    pub(in crate::tools) fn read(allow_absolute_paths: bool) -> Self {
        Self {
            allow_absolute_paths,
            setting_name: "tools.read.absolute_paths",
        }
    }

    pub(in crate::tools) fn view_image(allow_absolute_paths: bool) -> Self {
        Self {
            allow_absolute_paths,
            setting_name: "tools.view_image.absolute_paths",
        }
    }

    pub(in crate::tools) fn hash_edit(allow_absolute_paths: bool) -> Self {
        Self {
            allow_absolute_paths,
            setting_name: "tools.hash_edit.absolute_paths",
        }
    }

    pub(in crate::tools) fn grep(allow_absolute_paths: bool) -> Self {
        Self {
            allow_absolute_paths,
            setting_name: "tools.grep.absolute_paths",
        }
    }

    pub(in crate::tools) fn find(allow_absolute_paths: bool) -> Self {
        Self {
            allow_absolute_paths,
            setting_name: "tools.find.absolute_paths",
        }
    }

    pub(in crate::tools) fn list_files(allow_absolute_paths: bool) -> Self {
        Self {
            allow_absolute_paths,
            setting_name: "tools.list_files.absolute_paths",
        }
    }

    pub(in crate::tools) fn ast_grep(allow_absolute_paths: bool) -> Self {
        Self {
            allow_absolute_paths,
            setting_name: "tools.ast_grep.absolute_paths",
        }
    }
}

pub(in crate::tools) fn resolve_existing_path_with_policy(
    cwd: &Path,
    cwd_canonical: &Path,
    path: &str,
    policy: ExistingPathPolicy,
) -> anyhow::Result<PathBuf> {
    let user_path = PathBuf::from(path);
    let is_absolute = user_path.is_absolute();
    let candidate = if is_absolute {
        user_path
    } else {
        cwd.join(user_path)
    };
    let canonical = candidate.canonicalize().map_err(|error| {
        super::path_suggestions::with_suggestions(
            error,
            &candidate,
            cwd_canonical,
            is_absolute && policy.allow_absolute_paths,
        )
    })?;
    if (!is_absolute || !policy.allow_absolute_paths) && !canonical.starts_with(cwd_canonical) {
        anyhow::bail!(
            "path '{}' escapes cwd '{}' ({} does not allow this path)",
            canonical.display(),
            cwd_canonical.display(),
            policy.setting_name
        );
    }
    Ok(canonical)
}

#[derive(Clone, Copy)]
pub(in crate::tools) struct PathContext<'a> {
    pub(in crate::tools) cwd: &'a PathBuf,
    pub(in crate::tools) cwd_canonical: &'a PathBuf,
}

impl PathContext<'_> {
    pub(in crate::tools) fn resolve_existing_path(
        &self,
        path: &str,
        policy: ExistingPathPolicy,
    ) -> anyhow::Result<PathBuf> {
        resolve_existing_path_with_policy(self.cwd, self.cwd_canonical, path, policy)
    }
    pub(in crate::tools) fn ensure_inside_with_setting(
        &self,
        canonical: &Path,
        setting_name: &str,
    ) -> anyhow::Result<()> {
        if !canonical.starts_with(self.cwd_canonical) {
            anyhow::bail!(
                "path '{}' escapes cwd '{}' ({} does not allow this path)",
                canonical.display(),
                self.cwd_canonical.display(),
                setting_name
            );
        }
        Ok(())
    }
}

impl super::WriteContext<'_> {
    pub(in crate::tools) fn resolve_for_write(&self, path: &str) -> anyhow::Result<PathBuf> {
        let user_path = PathBuf::from(path);
        let is_absolute = user_path.is_absolute();
        let allow_absolute = self.settings.absolute_paths && is_absolute;
        let candidate = if is_absolute {
            user_path
        } else {
            self.paths.cwd.join(user_path)
        };
        if let Ok(metadata) = fs::symlink_metadata(&candidate) {
            if metadata.file_type().is_symlink() {
                anyhow::bail!(
                    "write target '{}' must not be a symlink",
                    candidate.display()
                );
            }
            let canonical = candidate.canonicalize()?;
            if !allow_absolute {
                self.paths
                    .ensure_inside_with_setting(&canonical, "tools.write.absolute_paths")?;
            }
            return Ok(canonical);
        }

        let normalized = lexical_normalize(&candidate);
        if !allow_absolute && !normalized.starts_with(self.paths.cwd_canonical) {
            anyhow::bail!(
                "path '{}' escapes cwd '{}' (set tools.write.absolute_paths=true to allow absolute paths)",
                normalized.display(),
                self.paths.cwd_canonical.display()
            );
        }

        let parent = normalized
            .parent()
            .ok_or_else(|| anyhow::anyhow!("path has no parent"))?;
        let existing_parent = nearest_existing_ancestor(parent)?;
        let parent_canonical = existing_parent.canonicalize()?;
        if !allow_absolute {
            self.paths
                .ensure_inside_with_setting(&parent_canonical, "tools.write.absolute_paths")?;
        }
        Ok(normalized)
    }
    pub(super) fn ensure_parent_inside_after_creation(
        &self,
        parent: &Path,
        allow_absolute_paths: bool,
    ) -> anyhow::Result<()> {
        // Pragmatic TOCTOU hardening: re-check after directory creation and just
        // before replacement. This reduces accidental/symlink escapes but is not
        // a full OS sandbox against a malicious concurrent local filesystem race.
        let canonical = parent.canonicalize()?;
        if allow_absolute_paths {
            return Ok(());
        }
        self.paths
            .ensure_inside_with_setting(&canonical, "tools.write.absolute_paths")
    }
}

fn nearest_existing_ancestor(path: &Path) -> anyhow::Result<PathBuf> {
    let mut cursor = path;
    loop {
        if cursor.exists() {
            return Ok(cursor.to_path_buf());
        }
        cursor = cursor
            .parent()
            .ok_or_else(|| anyhow::anyhow!("no existing ancestor for '{}'", path.display()))?;
    }
}