magi-code 0.96.1

Repository-aware CLI coding agent for terminal work
Documentation
use super::{
    ToolRuntime, args::FILE_READ_MAX_BYTES, contract::metadata_key as meta,
    skill_provenance::digest_hex,
};
use cap_fs_ext::{DirExt, FollowSymlinks, OpenOptionsFollowExt};
use cap_std::{
    ambient_authority,
    fs::{Dir as CapDir, OpenOptions as CapOpenOptions},
};
use serde_json::{Map, Value, json};
use std::{
    fs,
    path::{Component, Path, PathBuf},
};

#[derive(Debug)]
pub(super) struct LoadedSkillFile {
    pub(super) kind: &'static str,
    pub(super) reference: Option<String>,
    pub(super) content: String,
    pub(super) metadata: Map<String, Value>,
}

impl ToolRuntime {
    pub(super) fn load_skill_markdown(&self, name: &str) -> anyhow::Result<LoadedSkillFile> {
        let skill = self.skills.get(name).ok_or_else(|| {
            anyhow::anyhow!(
                "unknown skill '{name}'; use one of the skills listed in the system prompt"
            )
        })?;
        if skill.path.file_name().and_then(|name| name.to_str()) != Some("SKILL.md") {
            anyhow::bail!("selected skill file must be named SKILL.md");
        }
        load_primary_skill_file(
            name,
            &skill.path,
            &digest_hex(skill.path.as_os_str().as_encoded_bytes()),
        )
    }

    pub(super) fn load_skill_reference(
        &self,
        name: &str,
        reference: &str,
    ) -> anyhow::Result<LoadedSkillFile> {
        let skill = self.skills.get(name).ok_or_else(|| {
            anyhow::anyhow!(
                "unknown skill '{name}'; use one of the skills listed in the system prompt"
            )
        })?;
        let skill_dir = skill
            .path
            .parent()
            .ok_or_else(|| anyhow::anyhow!("selected skill has no directory"))?;
        let canonical_skill_dir = canonical_skill_dir(skill_dir)?;
        load_skill_file(
            name,
            "reference",
            Some(reference),
            &canonical_skill_dir,
            resolve_reference_path(skill_dir, reference)?,
            &digest_hex(skill.path.as_os_str().as_encoded_bytes()),
        )
    }
}

fn canonical_skill_dir(skill_dir: &Path) -> anyhow::Result<PathBuf> {
    let metadata = fs::symlink_metadata(skill_dir)?;
    if metadata.file_type().is_symlink() {
        anyhow::bail!("selected skill directory must not be a symlink");
    }
    if !metadata.is_dir() {
        anyhow::bail!("selected skill directory is not a directory");
    }
    Ok(skill_dir.canonicalize()?)
}

fn resolve_reference_path(skill_dir: &Path, reference: &str) -> anyhow::Result<PathBuf> {
    if reference.trim().is_empty() {
        anyhow::bail!("reference path must not be empty");
    }
    let reference_path = PathBuf::from(reference);
    if reference_path.is_absolute() {
        anyhow::bail!("reference path must be relative to the selected skill directory");
    }
    for component in reference_path.components() {
        match component {
            Component::Normal(_) | Component::CurDir => {}
            Component::ParentDir => anyhow::bail!("reference path must not contain '..'"),
            Component::RootDir => anyhow::bail!("reference path must not contain a root component"),
            Component::Prefix(_) => anyhow::bail!("reference path must not contain a path prefix"),
        }
    }
    Ok(skill_dir.join(reference_path))
}

fn load_primary_skill_file(
    skill_name: &str,
    candidate: &Path,
    descriptor_path_sha256: &str,
) -> anyhow::Result<LoadedSkillFile> {
    let skill_dir = candidate
        .parent()
        .ok_or_else(|| anyhow::anyhow!("selected skill has no directory"))?;
    let skill_parent = skill_dir
        .parent()
        .ok_or_else(|| anyhow::anyhow!("selected skill directory has no parent"))?;
    let skill_dir_name = skill_dir
        .file_name()
        .ok_or_else(|| anyhow::anyhow!("selected skill directory has no name"))?;
    let parent = CapDir::open_ambient_dir(skill_parent, ambient_authority())?;
    let skill_dir = parent.open_dir_nofollow(skill_dir_name).map_err(|error| {
        anyhow::anyhow!(
            "selected skill directory could not be opened without following symlinks: {error}"
        )
    })?;
    let mut options = CapOpenOptions::new();
    options.read(true).follow(FollowSymlinks::No);
    let file = skill_dir.open_with("SKILL.md", &options).map_err(|error| {
        anyhow::anyhow!("skill target could not be opened without following symlinks: {error}")
    })?;
    if !file.metadata()?.is_file() {
        anyhow::bail!("skill target must be a regular file");
    }
    let file = crate::prompt_file::read_prompt_text(file, candidate, FILE_READ_MAX_BYTES).map_err(
        |error| {
            let message = error.to_string();
            if message.contains("exceeds") {
                anyhow::anyhow!(
                    "skill target exceeds the skill read limit of {FILE_READ_MAX_BYTES} bytes"
                )
            } else if message.contains("UTF-8") {
                anyhow::anyhow!("skill target must be UTF-8 text")
            } else {
                error
            }
        },
    )?;
    Ok(loaded_skill_file(
        skill_name,
        "skill",
        None,
        file.text,
        descriptor_path_sha256,
    ))
}

/// Maps a missing reference target to a user-facing message; other IO errors pass through.
fn skill_reference_io_error(error: std::io::Error, reference: Option<&str>) -> anyhow::Error {
    match reference {
        Some(reference) if error.kind() == std::io::ErrorKind::NotFound => {
            anyhow::anyhow!("reference '{reference}' was not found in the selected skill directory")
        }
        _ => error.into(),
    }
}

fn load_skill_file(
    skill_name: &str,
    kind: &'static str,
    reference: Option<&str>,
    canonical_skill_dir: &Path,
    candidate: PathBuf,
    descriptor_path_sha256: &str,
) -> anyhow::Result<LoadedSkillFile> {
    let canonical_candidate = candidate
        .canonicalize()
        .map_err(|error| skill_reference_io_error(error, reference))?;
    if !canonical_candidate.starts_with(canonical_skill_dir) {
        anyhow::bail!("{kind} path escapes the selected skill directory");
    }

    let metadata = fs::symlink_metadata(&candidate)
        .map_err(|error| skill_reference_io_error(error, reference))?;
    if metadata.file_type().is_symlink() {
        anyhow::bail!("{kind} target must not be a symlink");
    }
    if metadata.is_dir() {
        anyhow::bail!("{kind} target must be a regular file, not a directory");
    }
    if !metadata.is_file() {
        anyhow::bail!("{kind} target must be a regular file");
    }
    if metadata.len() > FILE_READ_MAX_BYTES {
        anyhow::bail!(
            "{kind} target is {} bytes; skill read limit is {FILE_READ_MAX_BYTES} bytes",
            metadata.len()
        );
    }
    let content = fs::read_to_string(&canonical_candidate)
        .map_err(|error| anyhow::anyhow!("{kind} target must be UTF-8 text: {error}"))?;
    Ok(loaded_skill_file(
        skill_name,
        kind,
        reference,
        content,
        descriptor_path_sha256,
    ))
}

fn loaded_skill_file(
    skill_name: &str,
    kind: &'static str,
    reference: Option<&str>,
    content: String,
    descriptor_path_sha256: &str,
) -> LoadedSkillFile {
    let bytes = content.len();
    let reference = reference.map(str::to_string);
    let mut metadata = Map::new();
    metadata.insert(meta::SKILL.to_string(), json!(skill_name));
    metadata.insert(meta::KIND.to_string(), json!(kind));
    metadata.insert(meta::REFERENCE.to_string(), json!(reference.clone()));
    metadata.insert(meta::BYTES.to_string(), json!(bytes));
    metadata.insert(meta::BYTE_LIMIT.to_string(), json!(FILE_READ_MAX_BYTES));
    metadata.insert(
        "_skill_provenance".to_string(),
        json!({
            "access_policy_version": 1,
            "descriptor_path_sha256": descriptor_path_sha256,
            "snapshot_sha256": digest_hex(content.as_bytes()),
            "skill": skill_name,
            "reference": reference,
        }),
    );
    LoadedSkillFile {
        kind,
        reference,
        content,
        metadata,
    }
}