mentra 0.6.0

An agent runtime for tool-using LLM applications
Documentation
use serde_json::{Value, json};
use std::path::{Component, Path, PathBuf};

use crate::tool::{ParallelToolContext, RuntimeToolDescriptor, ToolAuthorizationPreview};

use super::{
    input::{ensure_files_have_operations, parse_files_input},
    schema::{self, FileOperation},
};

pub(crate) fn build_files_authorization_preview(
    descriptor: RuntimeToolDescriptor,
    ctx: &ParallelToolContext,
    input: &Value,
) -> Result<ToolAuthorizationPreview, String> {
    let raw_input = input.clone();
    let input = parse_files_input(input)?;
    ensure_files_have_operations(&input)?;

    let working_directory = match input.working_directory.as_deref() {
        Some(directory) => ctx.resolve_working_directory(Some(directory))?,
        None => ctx
            .runtime
            .resolve_working_directory(&ctx.agent_id, None)
            .unwrap_or_else(|_| ctx.working_directory().to_path_buf()),
    };

    let operations = input
        .operations
        .into_iter()
        .map(|operation| preview_file_operation(&working_directory, operation))
        .collect::<Result<Vec<_>, _>>()?;

    Ok(ToolAuthorizationPreview {
        working_directory: working_directory.clone(),
        capabilities: descriptor.capabilities,
        side_effect_level: descriptor.side_effect_level,
        durability: descriptor.durability,
        execution_category: descriptor.execution_category,
        approval_category: descriptor.approval_category,
        raw_input,
        structured_input: json!({
            "kind": "files",
            "working_directory": working_directory,
            "operations": operations,
        }),
    })
}

fn preview_file_operation(
    working_directory: &Path,
    operation: FileOperation,
) -> Result<Value, String> {
    match operation {
        FileOperation::Read {
            path,
            offset,
            limit,
        } => Ok(json!({
            "op": "read",
            "resolved_path": resolve_preview_path(working_directory, &path)?,
            "offset": offset,
            "limit": limit,
        })),
        FileOperation::List { path, depth, limit } => Ok(json!({
            "op": "list",
            "resolved_path": resolve_preview_path(working_directory, &path)?,
            "depth": depth,
            "limit": limit,
        })),
        FileOperation::Search {
            path,
            pattern,
            limit,
        } => Ok(json!({
            "op": "search",
            "resolved_path": resolve_preview_path(working_directory, &path)?,
            "pattern": pattern,
            "limit": limit,
        })),
        FileOperation::Create { path, .. } => Ok(json!({
            "op": "create",
            "resolved_path": resolve_preview_path(working_directory, &path)?,
        })),
        FileOperation::Set { path, .. } => Ok(json!({
            "op": "set",
            "resolved_path": resolve_preview_path(working_directory, &path)?,
        })),
        FileOperation::Replace {
            path,
            replace_all,
            expected_replacements,
            ..
        } => Ok(json!({
            "op": "replace",
            "resolved_path": resolve_preview_path(working_directory, &path)?,
            "replace_all": replace_all,
            "expected_replacements": expected_replacements,
        })),
        FileOperation::Insert {
            path,
            position,
            occurrence,
            ..
        } => Ok(json!({
            "op": "insert",
            "resolved_path": resolve_preview_path(working_directory, &path)?,
            "position": match position {
                schema::InsertPosition::Before => "before",
                schema::InsertPosition::After => "after",
            },
            "occurrence": occurrence,
        })),
        FileOperation::Move { from, to } => Ok(json!({
            "op": "move",
            "from_resolved_path": resolve_preview_path(working_directory, &from)?,
            "to_resolved_path": resolve_preview_path(working_directory, &to)?,
        })),
        FileOperation::Delete { path } => Ok(json!({
            "op": "delete",
            "resolved_path": resolve_preview_path(working_directory, &path)?,
        })),
    }
}

fn resolve_preview_path(working_directory: &Path, raw: &str) -> Result<PathBuf, String> {
    let candidate = PathBuf::from(raw);
    let path = if candidate.is_absolute() {
        candidate
    } else {
        working_directory.join(candidate)
    };
    normalize_preview_path(path)
}

fn normalize_preview_path(path: PathBuf) -> Result<PathBuf, String> {
    let mut normalized = if path.is_absolute() {
        PathBuf::new()
    } else {
        return Err(format!(
            "Path '{}' must resolve to an absolute path",
            path.display()
        ));
    };

    for component in path.components() {
        match component {
            Component::Prefix(prefix) => normalized.push(prefix.as_os_str()),
            Component::RootDir => normalized.push(component.as_os_str()),
            Component::CurDir => {}
            Component::ParentDir => {
                if !normalized.pop() || !normalized.is_absolute() {
                    return Err(format!(
                        "Path '{}' escapes the filesystem root",
                        path.display()
                    ));
                }
            }
            Component::Normal(segment) => normalized.push(segment),
        }
    }

    if !normalized.is_absolute() {
        return Err(format!(
            "Path '{}' must resolve to an absolute path",
            path.display()
        ));
    }

    Ok(normalized)
}